React:在useEffect中调用上下文函数时防止无限循环

时间:2020-09-30 07:55:35

标签: reactjs react-hooks use-effect react-context usecallback

在我的react应用中,我正在渲染<Item>组件的不同实例,并且希望它们在Context中注册/注销,具体取决于当前是否已安装它们。

我使用两个上下文(ItemContext提供了注册的项目,ItemContextSetters提供了注册/注销的功能)。

const ItemContext = React.createContext({});
const ItemContextSetters = React.createContext({
  registerItem: (id, data) => undefined,
  unregisterItem: (id) => undefined
});

function ContextController(props) {
  const [items, setItems] = useState({});

  const unregisterItem = useCallback(
    (id) => {
      const itemsUpdate = { ...items };
      delete itemsUpdate[id];
      setItems(itemsUpdate);
    },
    [items]
  );

  const registerItem = useCallback(
    (id, data) => {
      if (items.hasOwnProperty(id) && items[id] === data) {
        return;
      }

      const itemsUpdate = { ...items, [id]: data };
      setItems(itemsUpdate);
    },
    [items]
  );

  return (
    <ItemContext.Provider value={{ items }}>
      <ItemContextSetters.Provider value={{ registerItem, unregisterItem }}>
        {props.children}
      </ItemContextSetters.Provider>
    </ItemContext.Provider>
  );
} 

<Item>组件在挂载或props.data更改时应自行注册,而在卸载时应注销。因此,我认为可以使用useEffect非常干净地完成此操作:

function Item(props) {
  const itemContextSetters = useContext(ItemContextSetters);

  useEffect(() => {
    itemContextSetters.registerItem(props.id, props.data);

    return () => {
      itemContextSetters.unregisterItem(props.id);
    };
  }, [itemContextSetters, props.id, props.data]);

  ...
}

完整示例请参见此codesandbox

现在,问题在于这给了我无限循环,我不知道如何做得更好。循环是这样发生的(我相信):

  • <Item>个电话registerItem
  • 在上下文中,items被更改,因此registerItem被重新构建(因为它取决于[items]
  • 这会触发<Item>中的更改,因为itemContextSetters已更改,并且useEffect被再次执行。
  • 还执行了先前渲染的清理效果! (如here所述:“ React还会在下次运行效果之前清除以前的渲染中的效果”)
  • 这再次更改了上下文中的items
  • 依此类推...

我真的想不出一个可以避免此问题的干净解决方案。我是否滥用任何挂钩或上下文API?您可以通过任何一般模式帮助我如何编写由组件在其useEffect主体和useEffect-cleanup中调用的这种注册/注销上下文吗?

我不想做的事情

  • 完全摆脱上下文。在我的真实应用程序中,结构更加复杂,整个应用程序中的不同组件都需要此信息,因此我相信我要坚持上下文
  • 避免将<Item>组件从dom中强制删除(使用{renderFirstBlock && ),而应使用状态isHidden之类的东西。在我的真实应用中,这是我目前无法更改的任何内容。我的目标是跟踪所有现有组件实例的data

谢谢!

1 个答案:

答案 0 :(得分:2)

您可以使设置器具有稳定的引用,因为它们确实不需要依赖items

const ItemContext = React.createContext({});
const ItemContextSetters = React.createContext({
  registerItem: (id, data) => undefined,
  unregisterItem: (id) => undefined
});

function ContextController(props) {
  const [items, setItems] = useState({});

  const unregisterItem = useCallback(
    (id) => {
      setItems(currentItems => {
        const itemsUpdate = { ...currentItems };
        delete itemsUpdate[id];
        return itemsUpdate
      });
    },
    []
  );

  const registerItem = useCallback(
    (id, data) => {

      setItems(currentItems => {
        if (currentItems.hasOwnProperty(id) && currentItems[id] === data) {
          return currentItems;
        }
        return { ...currentItems, [id]: data }
      } );
    },
    []
  );
  
  const itemsSetters = useMemo(() => ({ registerItem, unregisterItem }), [registerItem, unregisterItem])

  return (
    <ItemContext.Provider value={{ items }}>
      <ItemContextSetters.Provider value={itemsSetters}>
        {props.children}
      </ItemContextSetters.Provider>
    </ItemContext.Provider>
  );
}

现在您的效果应该可以预期了