Home > Mobile >  Zustand selector function not firing when state updates
Zustand selector function not firing when state updates

Time:02-02

So I have two Zustand stores. One stores config information, like which user is currently being viewed in a modal:

export const useConfigStore = create<ConfigStoreState>((set) => ({
    selectedUser: undefined,
    setSelectedUser: (user: User) => {
        set(
            produce((draft) => {
                draft.selectedUser = user;
            })
        );
    }
}));

..and another stores other application data:

export const usePermissionsStore = create<PermissionsStoreState>((set) => ({
    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);
    }
}));

In my React component, I call an API which gets the users and populates the userPermissions and automatically selects the first user in the list.

The problem is that whenever userPermissions gets updated, the getPermissionsForCurrentUser() function doesn't re-fire or cause a re-render.

const MyPage = () => {
    const { users, isLoading } = useFetchUsers();
    const setSelectedUser = useConfigStore((state) => state.setSelectedUser);

    // This selector isn't firing when `useConfigStore.selectedUser` updates
    const permissionsForSelectedUser = usePermissionsStore((state) => state.getPermissionsForCurrentUser());

    React.useEffect(() => {
        if(users.length > 0) {
            setSelectedUser(users[0]); 
        }
    }, [users]);

    React.useEffect(() => {
        // This useEffect triggers only once
        console.log("permissionsForSelectedUser updated...")
    }, [permissionsForSelectedUser]);

    return (
        <>
            {isLoading || !users || !permissionsForSelectedUser ? (
                <div>Loading...</div>
            ) : (
                <div>Permissions are available!: {permissionsForSelectedUser.xyz}</div>
            )}
        </>
    );
};

So the getPermissionsForCurrentUser() clearly has a dependency on the selectedUser in another store, but it doesn't update when it changes and I can't see a way to specify that it should.

Is the issue that they're split into two different stores? How do I get around this?

CodePudding user response:

Is there a strong reason that there needs to be two stores? If not, generally when I've used Zustand I usually break down my state in one store using slices.

The following links demonstrate how to set this up:

There can be a little setup to get exactly how you would like.

But if I was going to attempt to rewrite your logic into one store it'd probably look something like:



export interface SliceContext<T extends object> {
  set: SetState<T>;
  get: GetState<T>;
  api: StoreApi<T>;
  getContext: () => Context;
}

/** Typings aren't the best here **/
export type SliceCreator<T extends object, U extends object> = (sliceContext: SliceContext<T>) => U;

export interface Context {
 /** 
 * This can be shared services between different mutations 
 * HTTP client, logging, etc.
 */
}

export interface ConfigSlice {
  user?: User;
  setSelectedUser: (user: User) => void;
}

export interface PermissionsSlice {
  userPermissions: UserPermission[];
  createDefaultPermissions: (users: Users[])  => void;
  getPermissionsForCurrentUser: () => UserPermission | undefined;
}

export interface StoreState {
  config: ConfigSlice;
  permissions: PermissionsSlice;
}


export const createConfigSlice: SliceCreator<StoreState, ConfigSlice> = ({ set }: Context)  => ({
    selectedUser: undefined,
    setSelectedUser: (user: User) => {
        set(
            // fyi: you could also create a immer middleware
            produce((draft) => {
                draft.selectedUser = user;
            })
        );
    }
});

export const createPermissionsSlice: SliceCreator<StoreState, PermissionSlice> = ({ set, get }: Context)  => ({
    userPermissions: [],
    createDefaultPermissions: (users: Array<Users>) => {
        set(
            produce((draft) => {
                draft.userPermissions = users.map(x => new UserPermission(x))
            });
        )
    },
    getPermissionsForCurrentUser: () => {
        const currentState = get();
        const userPermissions = currentState.permissions.userPermissions;
        const selectedUser = currentState.config.selectedUser;
        return userPermissions.find(x => x.user.id === selectedUser.id);
    }
});

const createStore: StoreApi<StoreState> | StateCreator<StoreState, SetState<StoreState>> = (set, get, api) => {
   const sliceContext: SliceContext = {
     set,
     get,
     api,
     getContext: () => {
       // Totally optional, but you could instantiate all your 
       // shared services.
     }
   }

   return {
     config: createConfigSlice(sliceContext),
     permissions: createPermissionsSlice(sliceContext)
   }
}

// You can apply all the other middleware here... 
export const useStore = create(createStore);

Then using it in the Page.tsx

const MyPage = () => {
    const { users, isLoading } = useFetchUsers();
    const { permissionsForSelectedUser, setSelectedUser } = useStore(state => ({
        permissionsForSelectedUser: state.permissions.getPermissionsForCurrentUser(),
        setSelectedUser: state.config.setSelectedUser
    }), shallow);


    React.useEffect(() => {
        if(users.length > 0) {
            setSelectedUser(users[0]); 
        }
    }, [users]);

    React.useEffect(() => {
        // This useEffect triggers only once
        console.log("permissionsForSelectedUser updated...")
    }, [permissionsForSelectedUser]);

    return (
        <>
            {isLoading || !users || !permissionsForSelectedUser ? (
                <div>Loading...</div>
            ) : (
                <div>Permissions are available!: {permissionsForSelectedUser.xyz}</div>
            )}
        </>
    );
};
  • Related