如何避免Zustand更新相同值副本时触发组件重渲染?
Zustand避免克隆副本触发不必要重渲染的解决方案
问题
在使用Zustand进行状态管理时,将状态变量更新为其值的克隆副本会触发组件不必要的重渲染。相关代码与现象如下:
Zustand Store 代码
export const useStore = create<State>((set) => ({ users: [], setUsers: (users: User[]) => set(state => ({...state, users })) }));
触发更新的组件代码
export default function App() { const users = useStore((state) => state.users); const setUsers = useStore((state) => state.setUsers); return ( <div className="App"> <button onClick={() => setUsers([ ...users, { name: "some name", age: Math.floor(Math.random() * 100) } ]) } > Add User </button> <button onClick={() => setUsers(users)}>Refresh exact</button> <button onClick={() => setUsers([...users])}>Refresh clone</button> <Users /> </div> ); }
订阅状态的Users组件代码
export const Users = () => { const users = useStore((state) => state.users); console.log("rerendered"); return ( <> {users.map((user, i) => ( <div key={i}>{JSON.stringify(user, null, 2)}</div> ))} </> ); };
现象说明
- 添加用户时,Users组件正常重渲染,符合预期;
- 调用
setUsers(users)(赋值自身引用)时,组件无重渲染,符合预期; - 调用
setUsers([...users])(赋值克隆副本)时,组件会触发重渲染——原因是克隆后的数组与原数组引用不相等(users !== [...users]),Zustand默认通过引用相等性判断是否更新状态,进而触发组件重渲染。
解决方案
1. 在Store层添加值相等性校验,拦截无意义的状态更新
修改setUsers方法,在更新前对比新旧值的内容是否一致,仅当内容不同时才执行状态更新。可根据数据复杂度选择合适的比较方式:
方案1.1:使用JSON.stringify(适合简单/中等复杂度数据)
export const useStore = create<State>((set) => ({ users: [], setUsers: (newUsers: User[]) => set(state => { // 对比数组内容是否一致 if (JSON.stringify(state.users) === JSON.stringify(newUsers)) { return state; // 内容一致,不更新状态 } return {...state, users: newUsers }; }) }));
方案1.2:使用深比较工具(如lodash.isEqual,适合复杂数据)
先安装lodash,再修改Store:
import isEqual from 'lodash.isEqual'; export const useStore = create<State>((set) => ({ users: [], setUsers: (newUsers: User[]) => set(state => { if (isEqual(state.users, newUsers)) { return state; // 内容一致,不更新状态 } return {...state, users: newUsers }; }) }));
2. 在组件层使用记忆化选择器,基于值相等性订阅状态
Zustand支持通过自定义比较函数优化选择器,让组件仅在状态内容真正变化时重渲染:
方案2.1:使用Zustand内置的shallow比较
import { shallow } from 'zustand/shallow'; export const Users = () => { // 使用shallow比较数组元素是否一致 const users = useStore((state) => state.users, shallow); console.log("rerendered"); return ( <> {users.map((user, i) => ( <div key={i}>{JSON.stringify(user, null, 2)}</div> ))} </> ); };
方案2.2:自定义深比较函数
如果需要更精细的控制,可自定义深比较逻辑:
import isEqual from 'lodash.isEqual'; export const Users = () => { // 仅当数组内容真正变化时,组件才重渲染 const users = useStore((state) => state.users, (prev, next) => isEqual(prev, next)); console.log("rerendered"); return ( <> {users.map((user, i) => ( <div key={i}>{JSON.stringify(user, null, 2)}</div> ))} </> ); };
3. 自定义中间件统一处理状态更新校验(可选)
如果多个状态都需要类似的校验,可以自定义Zustand中间件,统一拦截无意义的状态更新,避免在每个setter中重复编写比较逻辑。
内容的提问来源于stack exchange,提问作者Seth Lutske
相关产品推荐
相关产品推荐

