在 React Context 中与全局状态交互时过度重新渲染

Excessive rerendering when interacting with global state in React Context

我正在构建一个聊天应用程序,我正在使用 ContextAPI 来保存我需要从不同的不相关组件访问的状态。

由于上下文而发生了很多重新渲染,每次我在输入中键入一个字母时,所有组件都会重新渲染,当我切换 RightBar 时,它的状态也存在于上下文中,因为我需要从 Navbar.

中的按钮切换它

我尝试在每个组件上使用 memo,但每次我在任何组件的上下文中与状态交互时,所有组件仍然会重新呈现。

我将我的整个代码简化到这个沙箱 link : https://codesandbox.io/s/interesting-sky-fzmc6

这是已部署的 Netlify link:https://csb-fzmc6.netlify.app/

我尝试将我的代码分离到一些自定义挂钩中,例如 useChatSericeuseUsersService 以简化代码并使实际组件干净,我也将感谢任何关于如何更好地构建结构的见解那些挂钩以及在哪里放置 CRUD 函数,同时避免过度重新渲染。

我找到了一些“解决方案”,表明使用多个上下文应该有所帮助,但我无法弄清楚在我的具体情况下如何做到这一点,这个问题已经困扰了一个星期。

编辑:

我建议使用 jotai 或其他状态管理库。
上下文不适合高频变化。
而且,RightBar的状态看起来可以与其他hook/context.

分开

有一个棘手的解决方案可以解决一些渲染问题: https://codesandbox.io/s/stoic-mclaren-x6yfv?file=/src/context/ChatContext.js

您的代码需要重构,ChatSection 中的 useChatService 也依赖于您的 useChat,因此 ChatSection 将在文本更改时重新呈现。

您似乎正在更改有关输入字段数据更改的全局上下文。如果您的全局上下文是在父组件级别上定义的(相对于您的输入组件),那么父组件和所有子组件都必须重新渲染。 您有多种选择来避免这种行为:

  1. 在较低级别使用上下文,例如通过将您的输入字段提取到外部组件并在那里使用 useContext 挂钩
  2. 将输入保存到组件的本地状态,并且仅在模糊或提交时将其同步到全局上下文

将导航栏和聊天状态拆分为两个单独的 React 上下文实际上是 React 推荐的方法。通过将所有状态嵌套到一个新的对象引用中,只要任何单个状态更新,它必然会触发所有消费者的重新渲染。

<ChatContext.Provider
  value={{ // <-- new object reference each render
    rightBarValue: [rightBarIsOpen, setRightBarIsOpen],
    chatState: {
      editValue,
      setEditValue,
      editingId,
      setEditingId,
      inputValue,
      setInputValue,
    },
  }}
>
  {children}
</ChatContext.Provider>

我建议将 rightBarValue 和状态 setter 雕刻到它自己的上下文中。

导航栏上下文

const NavBarContext = createContext([false, () => {}]);

const NavBarProvider = ({ children }) => {
  const [rightBarIsOpen, setRightBarIsOpen] = useState(true);
  return (
    <NavBarContext.Provider value={[rightBarIsOpen, setRightBarIsOpen]}>
      {children}
    </NavBarContext.Provider>
  );
};

const useNavBar = () => useContext(NavBarContext);

聊天上下文

const ChatContext = createContext({
  editValue: "",
  setEditValue: () => {},
  editingId: null,
  setEditingId: () => {},
  inputValue: "",
  setInputValue: () => {}
});

const ChatProvider = ({ children }) => {
  const [inputValue, setInputValue] = useState("");
  const [editValue, setEditValue] = useState("");
  const [editingId, setEditingId] = useState(null);

  const chatState = useMemo(
    () => ({
      editValue,
      setEditValue,
      editingId,
      setEditingId,
      inputValue,
      setInputValue
    }),
    [editValue, inputValue, editingId]
  );

  return (
    <ChatContext.Provider value={chatState}>{children}</ChatContext.Provider>
  );
};

const useChat = () => {
  return useContext(ChatContext);
};

主容器

const MainContainer = () => {
  return (
    <ChatProvider>
      <NavBarProvider>
        <Container>
          <NavBar />
          <ChatSection />
        </Container>
      </NavBarProvider>
    </ChatProvider>
  );
};

NavBar - 使用 useNavBar 钩子

const NavBar = () => {
  const [rightBarIsOpen, setRightBarIsOpen] = useNavBar();

  useEffect(() => {
    console.log("NavBar rendered"); // <-- log when rendered
  });

  return (
    <NavBarContainer>
      <span>MY NAVBAR</span>
      <button onClick={() => setRightBarIsOpen(!rightBarIsOpen)}>
        TOGGLE RIGHT-BAR
      </button>
    </NavBarContainer>
  );
};

聊天

const Chat = ({ chatLines }) => {
  const { addMessage, updateMessage, deleteMessage } = useChatService();
  const {
    editValue,
    setEditValue,
    editingId,
    setEditingId,
    inputValue,
    setInputValue
  } = useChat();

  useEffect(() => {
    console.log("Chat rendered"); // <-- log when rendered
  });

  return (
    ...
  );
};

当 运行 应用注意到 "NavBar rendered" 仅在切换导航栏时记录,而 "Chat rendered" 仅在聊天文本区域中输入时记录。