【问题标题】:React function components re-renders when anything changes当有任何变化时,React 函数组件会重新渲染
【发布时间】:2021-10-20 21:27:48
【问题描述】:

这个 react 的东西让我发疯了,我不记得很久以前我在做 react native 时遇到过这些问题。我在这里一定做错了什么,这是我遇到的问题之一,我有另一个组件也有类似的问题。我是新的新钩子系统,即使阅读了一些我仍然不太清楚的东西。

我遇到了一些本地状态和 redux 状态的问题,这些问题导致我的对话框闪烁。每次父组件在该循环中设置日志状态时,对话框都会关闭和打开。 (所以在这种情况下,100 次取决于循环长度)每次状态更新时都会重新渲染。

库 react/redux/redux-thunk/material-ui

示例代码

function ToolbarModal(props){
 const [progressLog, setProgressLog] = useState("");

 const openProgressDialog = useSelector(getOpenProgressDialog);
 const handleCloseLongProgressDialog = () => {
   dispatch(handleOpenBatchRouteProgressDialog(false))
 }

 const handleSomeLogic = () => {
  for(let i=0; i<0; i++){
    //The real one is concated with prev message
   setProgressLog("SAMPLE MESSAGE")
  }
 }

 return (
  <div>
   <LongProgressDialog
     open={openProgressDialog}
     onClose={handleCloseLongProgressDialog}
     onEntered={handleSomeLogic}
     log={progressLog}
   />
  </div>
 )
}

log prop 使用 material-ui 的对话框组件传递给子组件,并仅显示在文本字段中。

function LongProgressDialog(props){
    const {open, onClose, onEntered, log} = props

    return (
        <Dialog
            open={open}
            onClose={onClose}
            onEntered={onEntered}
            maxWidth='sm'
            fullWidth
        >
            <DialogTitle>Long Progress</DialogTitle>
            <DialogContent>
                <TextField 
                 multiline
                 fullWidth
                 rows={10}
                 rowsMax={10}
                 variant='outlined'
                 disabled
                 value={log}
                 />
            </DialogContent>
        </Dialog>
    )
}

【问题讨论】:

  • 每次调用setProgressLog,都会更改状态变量并重新渲染组件。这就是反应组件的工作方式。如果你不想让组件重新渲染,为什么要使用状态变量?
  • 有没有办法只重新渲染 LongProgressDialog 中的文本字段,而不是重新渲染父级?因为我正在尝试从该逻辑函数重定向消息以慢慢建立日志。
  • 不,如果文本字段在 LongProgressDialog 中,那么 LongProgressDialog 也必须重新渲染。虽然从那里你可以在 LongProgressDialog 的一些子组件上使用 React.memo 之类的东西来阻止 他们 不必要地重新渲染(取决于他们收到的道具)
  • 当您更改状态时,组件会重新渲染,但在您的示例中,我认为这不是问题。如果您的对话框闪烁,这可能是您的 LongProgressDialog 组件中的问题。我认为您也应该共享该组件。如果在使用 setprograss 函数时 openProgressDialog 变量发生变化,则可能是 reducer 有问题。
  • 你应该将forwardRef添加到LongProgressDialog,返回类似于changeText函数的东西。然后在ToolbarModal中,在LongProgressDialog的useRef对象中调用changeText

标签: reactjs redux material-ui redux-thunk


【解决方案1】:

我想你要使用的是useRef:

import { useRef } from 'react'

const Component = props => {

  const logRef = useRef()
  const handleSomeLogic = () => {
  for(let i=0; i<100; i++){
    logRef.current = "SAMPLE MESSAGE"
  }
 }
}

然后,将 ref 传递给子组件:

 <LongProgressDialog
     open={openProgressDialog}
     onClose={handleCloseLongProgressDialog}
     onEntered={handleSomeLogic}
     log={logRef.current}
   />

在您的子组件中:

const LongProgressDialog = ({open, onClose, onEntered, log}) => {

  ...
  return (
      <p> Log value is: {log} </p>
     
   )
}

然而,

请注意,由于组件不会重新渲染,因此除非组件被渲染,否则您不会在子级中看到更新。因此,您必须根据您的情况决定是否应该更新状态,或者更新 ref 并重新渲染。您可以在此处查看example codesandbox

【讨论】:

  • 它可能可以工作,但正如你提到的,当它发生变化时需要通知它来更新 UI。我想继续看看我是否可以让它与州一起工作。
  • 那么没有办法,只能重新渲染状态。 React 无法更新值并将更新后的值渲染到 jsx 中而无需重新渲染。
【解决方案2】:

每次作为属性传递的对象的引用发生变化时,FC(函数组件)都会重新渲染。 因此,为防止这种情况发生,您传递的道具在每次重新渲染时必须具有相同的引用。

让我更详细地解释一下:

例如,这里:

const handleSomeLogic = () => {
//...

您正在创建一个新函数每次 FC 正在重新渲染(当您更新状态时会发生这种情况)。

你可以用useCallback记住函数。

函数、对象(和数组)每次创建时都会改变它们的值,所以:

const function1 = () => {}
const function2 = () => {}
function1 === function2 // false

const emptyObj = {}
const emptyObj2 = {}
emptyObj === emptyObj2 // false

// ... etc

为了记住值,你应该使用useMemo 钩子。

因此,作为一种解决方案:使用 useCallback 包装所有函数。 注意:我可以看到您正在将进度值传递给 LongProgressDialog 组件,因此从技术上讲,每次更新进度时,LongProgressDialog 组件都会重新渲染。

除此之外,您应该使用React.memo 包装您的组件。 当父级重新渲染时,子级也将重新渲染。 React.memo 可以防止这种情况发生,并且只会在组件的其中一个属性发生更改时重新渲染组件。

调查导致问题的道具

要找出您传递的道具导致重新渲染,您可以比较组件中传递的道具的引用,在您的情况下为LongProgressDialog 组件。

所以,例如:

// create variables outside of the FC in which we will store the reference
let prop1Ref;
let prop2Ref;

const LongProgressDialog = ({ prop1, prop2 }) => {

  // for each prop, use a effect which will be called when the prop's value/reference changes
  useEffect(() => {
    // compare the values by reference
    if (prop1Ref !== prop1) {
      // when prop ref has a value, the reference must have been changed:
      if(prop1Ref != null) {
        console.log("The reference of prop1 has changed. Memorize it properly");
      }
      prop1Ref = prop1;
    }
  }, [prop1])


}

您可以复制逻辑并将其应用于所有道具。 如果您不想手动检查,可以使用 https://github.com/welldone-software/why-did-you-render 等便利库

【讨论】:

  • 我确实将我的所有函数都包装在 useCallback 中,但它们似乎并没有做任何不同的事情。问题是,即使我没有将进度值传递给我的 LongProgressDialog 组件,只要我使用 setProgressLog 更新状态,它就会重新渲染,从而导致对话框关闭和打开。
  • 请编辑您的问题并添加 getOpenProgressDialog 所做的代码。我怀疑 this 的返回值没有被记住。您还可以尝试用React.memo 包裹LongProgressDialog。我将编辑我的答案,您如何进一步调查导致重新渲染的道具。
  • getOpenProgressDialog 仅从 redux 切片返回真/假值。一个简单的get方法。 const getOpeProgressDialog = state => state.ui.openProgressDialog
  • 好的,这很好。然后检查我更新的答案,看起来你需要进一步调查发生了什么
  • 我使用了你的日志。仅当值实际更改时,打开的 true/false reducer 才会更改。所以这个减速器工作正常......
【解决方案3】:

顾明峰在评论中回答了他的问题。 只需使用前向 ref 即可解决我的问题,从技术上讲,这是我一直在寻找的,但不知道它存在。

如果有人需要它,只需按照 react 文档中的 forward ref 文档进行操作即可。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-08-28
    • 1970-01-01
    • 1970-01-01
    • 2020-04-22
    • 2018-10-07
    • 1970-01-01
    • 2021-09-20
    • 2020-11-20
    相关资源
    最近更新 更多