Prevent rerender react array of objects
Asked Answered
A

1

10

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>

    )

})
Armin answered 23/3, 2020 at 21:3 Comment(4)
You are mapping the items, so whenever your array changes, all items get remapped - this can't be avoided. However, you are using the index as the item key key={index}. Please read the documentation on using keys, and specifically this on how keys work. Basically, when you add or remove an item, React thinks most / all the items have changed because you're using the index as the key. To prevent this, use a key that is specific to the item, like a test case id.Noonberg
What do you mean by re-render? How do you know the entire array is re-rendering?Noonberg
I said in an earlier comment "you are mapping the items, so whenever your array changes, all items get remapped - this can't be avoided." This means all items will run their render cycle. If you have a console log inside the render, it will log to the console. This doesn't mean the actual DOM element gets re-rendered. All child components will be re-evaluated if parent state changes, or if the child component's props change, or in your case, if you are mapping data to components and the array changes. There's nothing wrong with that.Noonberg
if you can create sandbox with small example , it would have been solved within seconds by anyone on stackoverflowTherewithal
P
10

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.

Plasmolysis answered 20/1, 2021 at 11:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.