I have an Ul component which contains an array of objects with different values in it. Each item called TestCase in this list has a button which makes a restcall and updates its object. However not all TestItems need to be updated. Only those whose button are clicked. The state of this array is stored in a parent continer component called TestCaseContainer. My button will updates the state accordingly to the effected TestItem in the array, however. This causes the whole list to rerender. How can I only have the changed TestItems rendered, instead of having the entire ul rendered every time an element is updated. I read about using useMemo so the component can memoize the passed down props, however I don't know how to implement this properly.
How can I stop all the rerenders?
Regression.js - Holds all the state
const Testing = forwardRef((props,ref) => {
  const templateTestItem = {id:0,formData:{date:'',env:'',assetClass:'',metric:'',nodeLevel:'',nodeName:'',testName:'',dataType:'',tradeId:''},results:[],isLoading:false}
  const testCaseRef = useRef()
  const [isRun, setIsRun] = useState(false)
  const [testItems, setTestItems] = useState([ templateTestItem])
  const [stats,setStats] = useState(null)
  const addTestItem = () => {
    const newIndex = testItems.length 
    // console.log(newIndex)
    const templateTestItem = {id:newIndex,formData:{date:'',env:'',assetClass:'',metric:'',nodeLevel:'',nodeName:'',testName:'',dataType:'',tradeId:''},results:[],isLoading:false}
    setTestItems([...testItems, templateTestItem])
  }
  const addUploadCases = (cases) => {
    setTestItems([])
    const UploadedItems = cases.map((item,index)=>{
        return{
          id:index,
          formData:{
            date:item['date'],
            env:item['env'],
            assetClass:item['asset_class'],
            metric:item['metric'],
            nodeLevel:item['node_level'],
            nodeName:item['node_name'],
            testName:item['test_name'],
            dataType:item['dataType'],
            tradeId:item['tradeId']
          },
          results:[]
        }
    })
    setTestItems(UploadedItems)
  }
  const runAllTests = () => {
    testCaseRef.current.runAll()
  }
  const clearTestCases = () => {
    // console.log('Clear Test cases')
    setTestItems([])
    if (testItems.length == 0) {
      setTestItems([templateTestItem])
    }
  }
  const extractAllResults =()=>{
    testCaseRef.current.extractAllResults()
  }
  const updateTestResults = useCallback( (result, index) => {
    console.log('Index:', index)
    setTestItems(prevObjs=>(prevObjs.map((item)=>{
      let updatedItem = { ...item, results: result }
      if(item.id==index) return updatedItem
      return item
    })))
  },[])
  return (
    <div style={{ 'backgroundColor': '#1b2829', 'display': 'flex', }} className={styles.dashboard}>
      <Grid>
        <Row stretched style={{}} className={styles.buttonConsole}>
            {<ButtonConsole addTest={addTestItem} addUploadCases={addUploadCases} runAllTests={runAllTests} clearTestCases={clearTestCases} extractAllResults={extractAllResults}  />}
        </Row>
        <Row centered>
          <TestRunStats stats={stats}/>
        </Row>
        <Row style={{ 'display': 'flex', 'flex-direction': 'column' }} ><TestCaseContainer countTestRunStats={countTestRunStats} updateTestResults={updateTestResults} isRun={isRun} ref={testCaseRef} testItems={testItems} /> </Row>
{/* 
        <Row></Row>
        <Row></Row> */}
      </Grid>
    </div>
  );
})
TestContainer.js
const TestCaseContainer = forwardRef((props, ref) => {
  const testCaseRef = useRef([])
  useImperativeHandle(ref, () => ({
    extractAllResults: async () => {
      const data = {
        data:[],
        summary:[]
      }
      testCaseRef.current.forEach(async (item, index) => {
        try {
          const workbook = item.extractAllResults()
          const summary = workbook['summary']
          workbook['data'].forEach(testData => {
            data['data'].push(testData)
          })
          data['summary'].push(summary)
        } catch (err) {
          console.log(err)
        }
      })
      await axios.post('http://localhost:9999/api/downloadresults', data).then(res => {
        console.log('res', res)
        const byteCharacters = atob(res.data);
        const byteNumbers = new Array(byteCharacters.length);
        for (let i = 0; i < byteCharacters.length; i++) {
          byteNumbers[i] = byteCharacters.charCodeAt(i);
        }
        const byteArray = new Uint8Array(byteNumbers);
        const blob = new Blob([byteArray], { type: 'application/vnd.ms-excel' });
        saveAs(blob, 'TestResults.xlsx')
      })
    },
    runAll: () => {
      testCaseRef.current.forEach(async (item, index) => {
        await item.runAll()
      })
    }
  }));
  const runTestCase = async (date, env, nodeLevel, nodeName, assetClass, metric, dataType, tradeId, testName, key) => {
    let testKey = key
    console.log('FEtCHING', testKey)
    try {
      const params = {
        nodeName,
        date,
        env,
        nodeLevel,
        assetClass,
        metric,
        dataType,
        tradeId,
        testName
      }
      const endpoint ={
        sensitivities:'sensitivities'
      }
      if (metric == 'DELTA_SENSITIVITIES') {
        const result = await axios.get('example.net/api/sensitivities', { params, }).then(response => {
          console.log('response.data', response.data)
          return response.data
        })
        if (result.data == 'none') {
          toast.error(`${date}-${metric}-${nodeName} failed queried! No valutations for trades`, {
            autoClose: 8000,
            position: toast.POSITION.TOP_RIGHT
          });
        } else if (result.data != 'none') {
          // setTestResult(result)
          props.updateTestResults(result, testKey)
          // updateTestResults(false,testKey,'isLoading')
          toast.success(`${date}-${metric}-${nodeName} Successfully queried!`, {
            autoClose: 8000,
            position: toast.POSITION.TOP_RIGHT
          });
        }
        // setTestResult(result.data)
      } else {
        await axios.get(`http://localhost:9999/api/metric/${metric}`, { params, }).then(response => {
          if (response.data != 'none') {
            props.updateTestResults(response.data, testKey)
            toast.success(`${date}-${metric}-${nodeName} Successfully queried!`, {
              autoClose: 8000,
              position: toast.POSITION.TOP_RIGHT
            });
          } else {
            toast.error(`${date}-${metric}-${nodeName} failed queried! No valutations for trades`, {
              autoClose: 8000,
              position: toast.POSITION.TOP_RIGHT
            });
          }
        })
      }
    } catch (error) {
      toast.error(`${date}-${metric}-${nodeName} failed queried! -${error
        }`, {
        autoClose: 8000,
        position: toast.POSITION.TOP_RIGHT
      });
    }
  }
  return (
    <Segment style={{ 'display': 'flex', 'width': 'auto', 'height': '100vh' }} className={styles.testCaseContainer}>
      <div style={{ 'display': 'flex', }}>
      </div>
      <ul style={{overflowY:'auto',height:'100%'}} className='testItemContainer'>
        {
          
          // memoTestTwo
          // testList
          props.testItems.map((item, index) => {
            let testName
            if (item['formData']['testName'] == '') {
              testName = `testRun-${index}`
            } else {
              testName = item['formData']['testName']
            }
            return <TestCase testResult={item['results']} runTestCase={runTestCase} isRun={props.isRun} ref={el => (testCaseRef.current[index] = el)} testKey={index} key={index} date={item['formData']['date']} env={item['formData']['env']} assetClass={item['formData']['assetClass']} metric={item['formData']['metric']} nodeLevel={item['formData']['nodeLevel']} nodeName={item['formData']['nodeName']} testName={testName} dataType={item['formData']['dataType']} tradeId={item['formData']['tradeId']} hierarchy={hierarchy} />
          })
        }
      </ul>
    </Segment>
  )
})
TestCase.js - the individual item rendered from mapping!
const TestCase = forwardRef((props, ref) => {
    const [isLoading, setIsLoading] = useState(false)
    const inputRefs = useRef()
    const outputRefs = useRef()
    useImperativeHandle(ref, () => ({
      extractAllResults: () => {
        return outputRefs.current.extractAllResults();
      },
      runAll: () => {
        inputRefs.current.runAll()
      },
    }));
    const runSingleTestCase = async (date, env, nodeLevel, nodeName, assetClass, metric, dataType, tradeId, testName, key) => {
      setIsLoading(true)
      await props.runTestCase(date, env, nodeLevel, nodeName, assetClass, metric, dataType, tradeId, testName, key)
      setIsLoading(false)
    }
    const convertDate = (date) => {
      if (date) {
        const newDate = date.split('/')[2] + '-' + date.split('/')[0] + '-' + date.split('/')[1]
        return newDate
      } else {
        return date
      }
    }
    return (
      <Segment color='green' style={{ 'display': 'flex', 'flexDirection': 'column', }}>
        <div style={{ 'display': 'flex', 'justify-content': 'space-between' }}>
          <div style={{ 'display': 'flex', 'height': '30px' }}>
            <Button
              // onClick={props.deleteSingleTest(props.testKey)}
              icon="close"
              inverted
              size="tiny"
              color='red'
            ></Button>
          </div>
          <RegressionInput runSingleTestCase={runSingleTestCase} isRun={props.isRun} testKey={props.testKey} ref={inputRefs} nodeNames={props.hierarchy} runTestCase={props.runTestCase} date={convertDate(props.date)} testName={props.testName} env={props.env} assetClass={props.assetClass} metric={props.metric} nodeLevel={props.nodeLevel} nodeName={props.nodeName} dataType={props.dataType} tradeId={props.tradeId} />
          <TestCheck pass={props.testResult ? props.testResult['CHECK'] : null} />
        </div>
        {
          isLoading ? (<Loading type={'circle'} style={{ 'display': 'flex', 'flexDirecton': 'column', 'justify-content': 'center', 'align-items': 'center', 'marginTop': '50' }} inline />) : (
            <RegressionOutput ref={outputRefs} testName={props.testName} testResult={props.testResult} />
          )
        }
      </Segment>
    )
})
                This article might help you understand React rendering behavior better:
Blogged Answers: A (Mostly) Complete Guide to React Rendering Behavior
React's default behavior is that when a parent component renders, React will recursively render all child components inside of it!
To change that behavior you can wrap some of your components in React.memo(). So React will do a shallow compare on the props object and only re-render that if one of the top level properties of the props object has changed.
That's not always possible or recommended, especially if you are using props.children.
const TestItem = React.memo(({id,value}) => {
  console.log(`Rendering TestItem ${id}...`);
  return(
    <div>TestItem {id}. Value: {value}</div>
  );
});
const App = () => {
  console.log("Rendering App...");
  const [items,setItems] = React.useState([
    { id: 1, value: "INITIAL VALUE" },
    { id: 2, value: "INITIAL VALUE" },
    { id: 3, value: "INITIAL VALUE" },
  ]);
  
  const testItems = items.map((item,index) =>
    <TestItem key={index} id={item.id} value={item.value}/>
  );
  
  const updateTest = (index) => {
    console.clear();
    setItems((prevState) => {
      const newArray = Array.from(prevState);
      newArray[index].value = "NEW VALUE";
      return newArray
    });
  };
  
  return(
    <React.Fragment>
      <div>App</div>
      <button onClick={()=>{updateTest(0)}}>Update Test 1</button>
      <button onClick={()=>{updateTest(1)}}>Update Test 2</button>
      <button onClick={()=>{updateTest(2)}}>Update Test 3</button>
      <div>
        {testItems}
      </div>
    </React.Fragment>
  );
};
ReactDOM.render(<App/>, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"/>
Without the React.memo() call. Every re-render of the App component would trigger a re-render in all of the TestItem component that it renders.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With