Selectors defined outside of a render function do not always return the associated store value
See original GitHub issueI’m having a problem where store values output via a custom hook do not all have the values from the current store. Some have values from the previous store. Here’s a brief explanation, I’ve also pasted more complete code at the end. Appreciate your help!
The crux of the issue is that if I have my selectors in an object useStore(selectors.blah), values from the store are incorrect. If I do this useStore(state => state.blah) there’s no problem. More info:
I’ve grouped my selectors like this (living in a module with the store):
export const selectors = {
status: (state) => state.status,
profile: (state) => state.profile,
};
And use them like this in a custom hook in a separate module
const status = useStore(selectors.status);
const profile = useStore(selectors.profile);
A login and a logout button change store status and profile together from
{ status: statusType.LOGGED_OUT, groups: [], profile: null }
to
{
status: statusType.LOGGED_IN,
profile: {
familyName: 'bob',
},
}
The values output in the custom hook are not what I was expecting. status has the current value of the store (statusType.LOGGED_IN), profile has the previous value of the store (null). Odd!
If instead I do this in the custom hook
const status = useStore((state) => state.status);
const profile = useStore(state => state.profile);
All is well. status and profile values have the current values of the store on every render
Here’s some more complete code:
Custom hook
import { useStore, selectors, mutators, statusType } from './store';
export function useCustom() {
const status = useStore(selectors.status);
const profile = useStore(selectors.profile);
const login = useStore(mutators.login);
const logout = useStore(mutators.logout);
// these do not match the current version of the store on every render
console.log('profile', profile); // null
console.log('status', status); // statusType.LOGGED_IN
switch (status) {
case statusType.LOGGED_IN:
return {
status: statusType.LOGGED_IN,
isLoggedIn: true,
profile,
logout,
};
case statusType.LOGGED_OUT:
return {
status: statusType.LOGGED_OUT,
isLoggedIn: false,
profile,
login,
};
default:
return {};
}
}
store
import create from 'zustand';
export const statusType = {
LOADING: 'loading',
LOGGED_IN: 'logged_in',
LOGGED_OUT: 'logged_out',
};
export const [useStore, store] = create((set) => ({
status: statusType.LOADING,
profile: null,
login: async () => {
set({
status: statusType.LOGGED_IN,
profile: {
familyName: 'bob',
},
});
},
logout: async () => {
set({ status: statusType.LOGGED_OUT, groups: [], profile: null });
},
}));
if (typeof window !== 'undefined') {
init();
}
async function init() {
store.setState({
status: statusType.LOGGED_IN,
profile: {
familyName: 'bob',
},
});
}
export const selectors = {
status: (state) => state.status,
profile: (state) => state.profile,
};
export const mutators = {
login: (state) => state.login,
logout: (state) => state.logout,
};
Issue Analytics
- State:
- Created 3 years ago
- Comments:12 (4 by maintainers)
Top GitHub Comments
Thanks for fixing this - coincidentally, we had an issue filed similar to this one today, where a useEffect updates the state while an async request is in-flight, and then the async request updates the store with outdated data (or so). We also extract selectors to stable functions.
Updating from 3.1.0 to 3.1.1 fixed the issue for us as well, so thank you for taking the time to address this issue @dai-shi 👍
Hi @acd02, Our actual codebase is more complex, this is an example of how we were considering using Zustand. It doesn’t seem unusual to use custom hooks to hide a store away. We can of course re-architect, and will do for now. I wanted to share the issue I have in case others come across it.