Zustand选择器函数状态更新时未触发,跨store依赖问题如何解决?
我有两个Zustand store:一个存储配置信息(如模态框中当前查看的用户):
export const useConfigStore = create<ConfigStoreState>((set) => ({ selectedUser: undefined, setSelectedUser: (user: User) => { set( produce((draft) => { draft.selectedUser = user; }) ); } }));
另一个存储其他应用数据:
export const usePermissionsStore = create<PermissionsStoreState>((set, get) => ({ userPermissions: [], createDefaultPermissions: (users: Array<Users>) => { set( produce((draft) => { draft.userPermissions = users.map(x => new UserPermission(x)) }) ); }, getPermissionsForCurrentUser: () => { const selectedUser = useConfigStore.getState().selectedUser; return get().userPermissions.find(x => x.user.id === selectedUser.id); } }));
在React组件中,我调用API获取用户并填充userPermissions,同时自动选中列表中的第一个用户。
问题在于,当userPermissions更新时,getPermissionsForCurrentUser()函数不会重新触发,也不会导致组件重新渲染:
const MyPage = () => { const { users, isLoading } = useFetchUsers(); const setSelectedUser = useConfigStore((state) => state.setSelectedUser); // 当`useConfigStore.selectedUser`更新时,此选择器不会触发 const permissionsForSelectedUser = usePermissionsStore((state) => state.getPermissionsForCurrentUser()); React.useEffect(() => { if(users.length > 0) { setSelectedUser(users[0]); } }, [users]); React.useEffect(() => { // 此useEffect仅触发一次 console.log("permissionsForSelectedUser updated...") }, [permissionsForSelectedUser]); return ( <> {isLoading || !users || !permissionsForSelectedUser ? ( <div>Loading...</div> ) : ( <div>Permissions are available!: {permissionsForSelectedUser.xyz}</div> )} </> ); };
显然getPermissionsForCurrentUser()依赖另一个store中的selectedUser,但当该状态变化时它不会更新,我也无法指定它应该更新。
请问问题是否源于拆分到两个不同的store?该如何解决此问题?
问题根源
拆分store本身不是问题,核心问题在于:
getPermissionsForCurrentUser内部用useConfigStore.getState()获取状态,这种方式不会让Zustand追踪到selectedUser的变化。组件订阅的是usePermissionsStore的状态,但selectedUser属于另一个store,Zustand无法自动关联两者的依赖。- 当
selectedUser变化时,usePermissionsStore本身的状态没有改变,所以组件不会收到更新通知。
解决方案
这里提供几种可行的解决方式:
1. 将依赖的外部状态作为参数传入
修改getPermissionsForCurrentUser,让它接收selectedUser作为参数,在组件中同时获取两个store的状态并组合:
修改PermissionsStore:
export const usePermissionsStore = create<PermissionsStoreState>((set, get) => ({ userPermissions: [], createDefaultPermissions: (users: Array<Users>) => { set( produce((draft) => { draft.userPermissions = users.map(x => new UserPermission(x)) }) ); }, // 改为接收参数,不再直接依赖外部store getPermissionsForUser: (selectedUser: User | undefined) => { if (!selectedUser) return undefined; return get().userPermissions.find(x => x.user.id === selectedUser.id); } }));
组件中使用:
const MyPage = () => { const { users, isLoading } = useFetchUsers(); const setSelectedUser = useConfigStore((state) => state.setSelectedUser); const selectedUser = useConfigStore((state) => state.selectedUser); const getPermissionsForUser = usePermissionsStore((state) => state.getPermissionsForUser); const userPermissions = usePermissionsStore((state) => state.userPermissions); // 组合两个状态,当任意一个变化时重新计算 const permissionsForSelectedUser = React.useMemo(() => { return getPermissionsForUser(selectedUser); }, [selectedUser, userPermissions, getPermissionsForUser]); React.useEffect(() => { if(users.length > 0) { setSelectedUser(users[0]); } }, [users]); React.useEffect(() => { console.log("permissionsForSelectedUser updated...") }, [permissionsForSelectedUser]); return ( <> {isLoading || !users || !permissionsForSelectedUser ? ( <div>Loading...</div> ) : ( <div>Permissions are available!: {permissionsForSelectedUser.xyz}</div> )} </> ); };
2. 在store中订阅外部store的变化
如果希望逻辑保留在store内部,可以在usePermissionsStore中订阅useConfigStore的selectedUser变化,当它改变时主动触发store的更新:
修改PermissionsStore:
export const usePermissionsStore = create<PermissionsStoreState>((set, get) => ({ userPermissions: [], createDefaultPermissions: (users: Array<Users>) => { set( produce((draft) => { draft.userPermissions = users.map(x => new UserPermission(x)) }) ); }, getPermissionsForCurrentUser: () => { const selectedUser = useConfigStore.getState().selectedUser; if (!selectedUser) return undefined; return get().userPermissions.find(x => x.user.id === selectedUser.id); } })); // 订阅config store的变化,当selectedUser改变时,触发permissions store的更新 useConfigStore.subscribe( (state) => state.selectedUser, () => { // 空set触发订阅者更新 usePermissionsStore.setState({}, false); } );
这样当selectedUser变化时,usePermissionsStore的订阅者(包括组件)会重新计算选择器,getPermissionsForCurrentUser也会重新执行。
3. 使用组合选择器跨store追踪依赖
利用createSelector(需安装@reduxjs/toolkit或reselect)创建跨两个store的选择器,自动追踪依赖:
import { createSelector } from '@reduxjs/toolkit'; // 定义基础选择器 const selectSelectedUser = (state: ReturnType<typeof useConfigStore.getState>) => state.selectedUser; const selectUserPermissions = (state: ReturnType<typeof usePermissionsStore.getState>) => state.userPermissions; // 组合选择器 const selectPermissionsForSelectedUser = createSelector( [selectSelectedUser, selectUserPermissions], (selectedUser, userPermissions) => { if (!selectedUser) return undefined; return userPermissions.find(x => x.user.id === selectedUser.id); } ); // 组件中使用 const MyPage = () => { const { users, isLoading } = useFetchUsers(); const setSelectedUser = useConfigStore((state) => state.setSelectedUser); // 使用组合选择器,自动追踪两个store的依赖 const permissionsForSelectedUser = useConfigStore( usePermissionsStore.withStore(selectPermissionsForSelectedUser) ); React.useEffect(() => { if(users.length > 0) { setSelectedUser(users[0]); } }, [users]); React.useEffect(() => { console.log("permissionsForSelectedUser updated...") }, [permissionsForSelectedUser]); return ( <> {isLoading || !users || !permissionsForSelectedUser ? ( <div>Loading...</div> ) : ( <div>Permissions are available!: {permissionsForSelectedUser.xyz}</div> )} </> ); };
内容的提问来源于stack exchange,提问作者MSOACC

