'Prevent rerender react array of objects

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>

    )

})


Solution 1:[1]

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.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 cbdeveloper