【问题标题】:Prevent rerender react array of objects防止重新渲染反应对象数组
【发布时间】:2020-07-04 09:14:38
【问题描述】:

我有一个 Ul 组件,其中包含一组具有不同值的对象。此列表中名为 TestCase 的每个项目都有一个按钮,该按钮进行休息调用并更新其对象。然而,并不是所有的 TestItems 都需要更新。只有那些按钮被点击的人。该数组的状态存储在称为 TestCaseContainer 的父容器组件中。但是,我的按钮将根据数组中受影响的 TestItem 相应地更新状态。这会导致整个列表重新呈现。我怎样才能只渲染更改后的 TestItems,而不是每次更新元素时​​都渲染整个 ul。我阅读了有关使用 useMemo 以便组件可以记住传递下来的道具的信息,但是我不知道如何正确实现这一点。

我怎样才能停止所有的重新渲染!为乱码提前道歉!

这是我的代码

Regression.js - 保存所有状态

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('http://wind08132495.tdbfg.com:9999/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 - 从映射渲染的单个项目!

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>

    )

})

【问题讨论】:

  • 您正在映射项目,因此每当您的数组更改时,所有项目都会重新映射 - 这是无法避免的。但是,您使用索引作为项目键 key={index}。请阅读documentation on using keys,特别是this on how keys work。基本上,当您添加或删除一个项目时,React 认为大多数/所有项目都已更改,因为您使用索引作为键。为防止这种情况发生,请使用特定于项目的键,例如测试用例 ID。
  • 我在我的对象中添加了一个键,它被传递给我的 组件,所以它现在就像这样 key={item['id']},但它似乎并没有停止重新渲染整个数组
  • 重新渲染是什么意思?你怎么知道整个数组正在重新渲染?
  • 我在 TestItem 组件内部有一个很长的控制台。我可以看到控制台中出现了不变的项目
  • 我在之前的评论中说过“您正在映射项目,因此每当您的数组更改时,所有项目都会重新映射 - 这是无法避免的。”这意味着所有项目都将运行它们的渲染周期。如果渲染中有控制台日志,它将记录到控制台。这并不意味着实际的 DOM 元素会被重新渲染。如果父状态发生更改,或者子组件的道具发生更改,或者在您的情况下,如果您将数据映射到组件并且数组发生更改,则所有子组件都将被重新评估。这没有什么问题。

标签: javascript reactjs react-hooks render rerender


【解决方案1】:

这篇文章可能会帮助你更好地理解 React 的渲染行为:

Blogged Answers: A (Mostly) Complete Guide to React Rendering Behavior

React 的默认行为是,当父组件渲染时,React 会递归渲染其中的所有子组件!

要改变这种行为,您可以将一些组件包装在React.memo() 中。因此,React 将对 props 对象进行浅层比较,并且仅在 props 对象的顶级属性之一发生更改时重新渲染。

这并不总是可行或推荐的,尤其是在您使用 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"/>

没有React.memo() 电话。 App 组件的每次重新渲染都会触发它渲染的所有 TestItem 组件的重新渲染。

【讨论】:

  • 非常感谢!您分享的链接对于了解渲染如何在反应中发生以及如何优化它非常有价值!
  • @JaladhSinghal 应该接受它作为答案
  • @DᴀʀᴛʜVᴀᴅᴇʀ 我不是 OP,我认为只有他们有权接受答案
猜你喜欢
  • 2022-01-23
  • 1970-01-01
  • 2022-11-07
  • 1970-01-01
  • 2021-01-07
  • 2021-12-05
  • 2019-11-23
  • 1970-01-01
  • 2020-12-07
相关资源
最近更新 更多