【问题标题】:Cannot update a component (xxx) while rendering a different component (yyy)渲染不同的组件 (yyy) 时无法更新组件 (xxx)
【发布时间】:2021-06-17 07:00:33
【问题描述】:

在我的 react 应用程序中,我收到了这个奇怪的错误(“在渲染不同的组件 (yyy) 时无法更新组件 (xxx)”)。我理解是什么导致了错误,但我不理解“为什么”或如何在不重组大部分逻辑的情况下修复它。所以生命周期中的组件和底层逻辑如下: “App”是顶层组件,它包含一个名为“grid”的对象的状态。此状态及其设置器被传递给名为“Grid2”的组件。 Grid2 也有自己的状态,由 reducer 接口(React.useReducer 不是 React.useState)。这个 reducer 被传递了 App 状态(和状态内部的网格 obj)以及这个状态的设置器。所以reducer不仅返回Grid2状态的更新状态,还可以调用App状态的setter。 React 不喜欢这样,但我唯一直观的解决方案是将调用 App 的 setter 的所有逻辑移动到 useEffects 中,后者将监听 Grid2 状态的变化。

//--------------- App.tsx ---------------------
export const AppContext = React.createContext<AppContextType>({refs: initAppRefs, state: initAppState, setState: () => {}});

export function App() {
  let { current: refs } = React.useRef<Refs>(initAppRefs);
  const [state, setState] = React.useState<State>(initAppState);
  return (
    <AppContext.Provider value={{refs, state, setState}}>
      <Home />
    </AppContext.Provider>
  );
}

//---------------- Grid2.tsx --------------------
import { AppContext, AppContextType, State } from "../App";
const gridStateReducer = (last: GridState, action: GridReducerAction): GridState => {
  const newState: GridState = Helpers.deepCopy(last);
  // centralized setter for tile.mouseDown, returns if change was made
  const mouseDownOverride = (tile: string, value: boolean): boolean => {
    // force tile to exist in newState.grid
    if (!(tile in newState.grid)) {
      newState.grid[tile] = {mouseDown: false, mouseOver: false};
    }
    // check to see if change is needed
    if (newState.grid[tile].mouseDown !== value) {
      newState.grid[tile].mouseDown = value;
      // update appState grid fills
      if (value) { //mousedown
        if (tile in action.appState.grid) {
          if (action.appState.currTool === "wall" && action.appState.grid[tile].fill === "empty") {
            const newAppState: State = Helpers.deepCopy(action.appState);
            newAppState.grid[tile].fill = "wall";
            action.setAppState(newAppState);
          }
        }
      }
      return true;
    } else {
      return false;
    }
  }
  if (action.type === GridReducerActionType.SetTileDown && action.data instanceof Array 
  && typeof action.data[0] === "string" && typeof action.data[1] === "boolean") {
    return mouseDownOverride(...(action.data as [string, boolean])) ? newState : last;
  }
}
export const Grid2: React.FC<{}> = () => {
  const { state: appState, setState: setAppState, refs: appRefs } = React.useContext<AppContextType>(AppContext);

  const [gridState, gridStateDispatch] = React.useReducer(gridStateReducer, initGridState);
}

代码是来自实际项目的一组非常有选择性的逻辑,您可能会注意到很多引用似乎不知从何而来,但我省略了这段代码,因为它只会使代码膨胀并远离逻辑路径。所以我的问题是,为什么会发生这种情况(寻找引擎盖下的解释),以及如何在不进行过多重构的情况下解决这个问题?

【问题讨论】:

    标签: reactjs typescript


    【解决方案1】:

    据我估计,问题可能是由于gridStateReducer 中的副作用。传递给useReducer 的reducer 函数不应该有副作用(即调用任何setter 或改变任何全局状态)。 reducer 函数的要点是获取当前状态,应用动作负载,然后返回一个新状态,这将提示 React 生命周期执行任何必要的重新渲染。

    由于您在 reducer 中调用 action.setAppState(newAppState),并且由于这是一个 React 状态设置器,我的猜测是这会导致 React 在 reducer 完成之前启动一个新的渲染周期。由于新的渲染周期会导致组件更新,因此它可能会“导致组件更新(可能是App),同时渲染不同的组件(无论调用gridStateDispatch 或调用该reducer,可能是Grid2)”

    在重构方面,要求gridStateReducer返回一个新的GridState,并且不产生任何副作用。首先可能是重构 reducer 以消除副作用并返回一个新状态:

    const gridStateReducer = (last: GridState, action: GridReducerAction): GridState => {
      const newState: GridState = Helpers.deepCopy(last);
      // centralized setter for tile.mouseDown, returns if change was made
      const mouseDownOverride = (tile: string, value: boolean): boolean => {
        // force tile to exist in newState.grid
        if (!(tile in newState.grid)) {
          newState.grid[tile] = {mouseDown: false, mouseOver: false};
        }
        // check to see if change is needed
        if (newState.grid[tile].mouseDown !== value) {
          newState.grid[tile].mouseDown = value;
          // update appState grid fills
          return true;
        } else {
          return false;
        }
      }
      if (action.type === GridReducerActionType.SetTileDown && action.data instanceof Array 
      && typeof action.data[0] === "string" && typeof action.data[1] === "boolean") {
        return mouseDownOverride(...(action.data as [string, boolean])) ? newState : last;
      }
    }
    

    现在,看起来那个副作用对if (tile in action.appState.grid) 很感兴趣,所以我需要一些方法来同时拥有tileappState 的上下文。由于我不确定结构到底是什么,我假设AppContextaction.appState 中的appState 是同一个对象。如果不是,则忽略这句话之后的所有内容。

    查看reducer,看起来我们将tile作为传递给gridStateDispatch的动作中元组中的第一个元素传递,这意味着该函数的调用者,看起来像@987654338 @,必须知道在调用 dispatch 函数时tile 应该是什么。由于该组件在上下文中也有 AppContext,因此您应该能够执行以下操作:

    export const Grid2: React.FC<{}> = () => {
      const { state: appState, setState: setAppState, refs: appRefs } = React.useContext<AppContextType>(AppContext);
    
      const [gridState, gridStateDispatch] = React.useReducer(gridStateReducer, initGridState);
    
      const handleSomethingWithTile = (tile: string, someBool: boolean) => {
        gridStateDispatch({ type: GridReducerActionType.SetTileDown, data: [ tile, someBool ] })
        if (tile in appState.grid) {
          if (appState.currTool === "wall" && appState.grid[tile].fill === "empty") {
            const newAppState: State = Helpers.deepCopy(appState);
            newAppState.grid[tile].fill = "wall";
            setAppState(newAppState);
          }
        }
      }
    }
    

    这应该是可能的,因为if (tile in appState.grid) 语句似乎不需要reducer 中的中间状态值,因此可以在此处将该决定移出reducer 范围。这应该可以防止您遇到的那种“状态更新中的状态更新”问题。

    我应该提一下:我可能想在这里做一些额外的重构来帮助简化状态逻辑。看起来你可能真的很接近想要一个像 redux 这样的工具来帮助管理这里的状态。还应该包括一个警告,如果你不小心,通过本机 React 上下文使用 setter 传递完整的应用程序状态可能会出现非常严重的性能问题。

    【讨论】:

    • 是的,其实action.appState === AppState。你强化了我的怀疑,但是你带来了两个有趣的观点,我想你可能知道一些我在这里不知道的事情。所以首先你说我正处于需要 redux 的临界点。但是您也知道我正在使用 useContext 和 useReducer 钩子,那么为什么 redux 会是比这些更好的选择?您还提到了与通过 useContext 传递(大?有状态?)对象相关的性能问题,但我不知道这里有任何潜在危险。如果您愿意详细说明这些要点!
    • @CameronHonis 当然! Redux 本身并不是绝对必要的,但它和 Redux 生态系统中的其他工具(特别考虑像 sagas 之类的东西)可以帮助解决像这样的复杂状态交互。 W/r/t 上下文性能,重要的是要意识到任何时候上下文值发生变化,也就是每次应用程序状态发生变化时,任何消耗该上下文的东西也会重新渲染。因此,如果您应用中的每个组件都从该上下文中读取,则每个组件都会在应用状态更改时重新呈现,即使该更改与给定组件无关。
    • 哦,好吧。在这一点上,我在编写代码时会下意识地监控重新渲染的成本,所以我什至没有将这个因素考虑在内。但是感谢您的洞察力,您的帮助非常大!
    • 没问题!如果你用谷歌搜索的话,会有很多关于上下文的资源。 Redux 建议的一部分是利用选择器,这有助于处理仅消耗大状态的一部分而无需重新渲染所有内容。但这不是唯一的解决方案。神速!
    • Redux 不会解决这个特定问题,因为 Redux 具有相同的要求,即 reducer 没有任何副作用,并且动作只是纯数据对象。因此,在您的操作上调用函数仍然不行。也就是说,我确实喜欢 Redux,而且 Redux Toolkit 有一些出色的助手,所以你不需要做像deepCopy 这样低效的事情。
    猜你喜欢
    • 2020-09-25
    • 1970-01-01
    • 1970-01-01
    • 2021-07-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-09-24
    相关资源
    最近更新 更多