我正在构建一个聊天应用程序,我正在使用 ContextAPI 来保存我需要从不同的不相关组件访问的状态。
由于上下文而发生了很多重新渲染,每次我在输入中键入一个字母时,所有组件都会重新渲染,当我切换
RightBar
时也是如此,它的状态也驻留在上下文中,因为我需要从按钮切换它在Navbar
。
我尝试在每个组件上使用
memo
,但每次我与任何组件的上下文中的状态交互时,所有组件仍然会重新渲染。
我将简化的整个代码添加到此沙箱链接:https://codesandbox.io/s/interesting-sky-fzmc6
这是已部署的 Netlify 链接:https://csb-fzmc6.netlify.app/
我尝试将我的代码分成一些自定义钩子,例如
useChatSerice
、useUsersService
来简化代码并使实际组件干净,我也将感谢任何有关如何更好地构造这些钩子以及在何处放置 CRUD 函数的见解同时避免过度重新渲染。
我发现一些“解决方案”表明使用多个上下文应该有所帮助,但我不知道在我的具体情况下如何做到这一点,已经被这个问题困扰了一个星期。
编辑:
RightBar
切换按钮,它也会导致完全重新渲染。将导航栏和聊天状态拆分为两个单独的 React 上下文实际上是 React 推荐的方法。通过将所有状态嵌套到新的对象引用中,只要任何单个状态更新,它必然会触发所有消费者的重新渲染。
<ChatContext.Provider
value={{ // <-- new object reference each render
rightBarValue: [rightBarIsOpen, setRightBarIsOpen],
chatState: {
editValue,
setEditValue,
editingId,
setEditingId,
inputValue,
setInputValue,
},
}}
>
{children}
</ChatContext.Provider>
我建议将
rightBarValue
和状态设置器雕刻到自己的上下文中。
导航栏上下文
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>
);
};
导航栏 - 使用
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"
仅在聊天文本区域中键入时记录。
我建议使用jotai或其他状态管理库。
上下文不适合高频变化。
并且,
RightBar
的状态外观可以与其他钩子/上下文分开。
有一个棘手的解决方案可以解决一些渲染问题: https://codesandbox.io/s/stoic-mclaren-x6yfv?file=/src/context/ChatContext.js
你的代码需要重构,
useChatService
中的ChatSection
也依赖于你的useChat
,所以ChatSection
会在文本改变时重新渲染。
看起来您正在更改输入字段数据更改的全局上下文。如果您的全局上下文是在父组件级别上定义的(相对于您的输入组件),则父组件和所有子组件都必须重新渲染。 您有多种选择可以避免这种行为:
如果您在使用 Context API 时遇到不必要的重新渲染,切换到 useSyncExternalStore 挂钩可以解决您的问题。
这个钩子允许您以更有效的方式管理状态,防止组件在不需要时重新渲染。
我最近写了一篇关于这个主题的博客,解释了它的工作原理以及为什么它是用于全局状态管理的 Context API 的绝佳替代方案。