clunky combine store usage
See original GitHub issueI’ve followed the recommended way to combine stores in #228. It’s clunky to work with as inside the stores you have to set state as if you’re setting from the root of the state, this makes everything overly verbose. I imagine in many cases sub-stores won’t touch other stores state and having one root store would mainly be for simplifying co-dependent state queries outside of the stores.
Take a root store made up of sub-stores like this:
const useStore = create((set, get, api) => ({
users: { ...createUsersStore(set, get, api) },
restrictedUsers: { ...createRestrictedUsersStore(set, get, api) }
}));
Inside the sub-stores, it’d be intuitive to set state like this:
const createUsersStore = (set, get, api) => ({
data: [],
setUsers: users => set({data: users})
})
Example:
// expected state after calling useStore.getState().users.setUsers(['user1', 'user2']) :
{
users: {
data: ['user1', 'user2']
},
restrictedUsers: {...}
}
// instead, it looks like this:
{
data: ['user1', 'user2'],
users: {
data: []
},
restrictedUsers: {...}
}
To fix this issue, you need to prepend all set
object params with the sub-store key, and spread the current values:
const createUsersStore = (set, get, api) => ({
data: [],
setUsers: users => set({users: {...get().users, data: users}})
})
This feels like unnecessary ceremony. Immer could be a solution, but I like to have my async logic contained in store actions; Immer doesn’t like async. The solution I’ve come to is a helper like this:
const _setIn = (set, get, propName) => setObjArg => set({ [propName]: { ...get()[propName], ...setObjArg } });
This helper can then be passed down into the stores
const useStore = create((set, get, api) => {
const setIn = curry(_setIn)(set, get);
return {
users: { ...createUsersStore(set, get, api, setIn("users")) },
restrictedUsers: {...createRestrictedUsersStore(set, get, api, setIn("restrictedUsers"))}
};
});
How setting now looks, and works intuitively:
const createUsersStore = (set, get, api, setIn) => ({
data: [],
setUsers: users => setIn({data: users})
})
This works well, but only supports set(obj)
; no support for set(state => ...)
. Is there an alternative solution (or modification to mine) that matches this, and ideally doesn’t limit the api of set
. I feel a solution to this should be documented as in real world apps it’s quite common to have co-dependent stores, and having one global store simplifies the application logic for these co-dependencies (imo).
Zustand is fantastic, I want to harness the full power of it without introducing ridiculous ceremony like Redux.
Issue Analytics
- State:
- Created 3 years ago
- Comments:6 (3 by maintainers)
Top GitHub Comments
I will take a look through those other issues, cheers.
Immer doesn't like async
: Bit of a bad take on my part. While it does support async, I think it isn’t possible to set state incrementally inside the producer - new state is created when producer exits, for async this is when the promise resolves. I may be wrong here, I’ll need to double check…Assuming that’s right ^, if you had a async fetch action in the store, and wanted to set
isFetching
before and after the network call, and for store listeners to pickup on the incremental updates, this wouldn’t be possible in the same action.By no means an expert on state management patterns, but happy to help keep the conversation going and contribute where I can.
There’s some good solutions in those issues you mentioned above. I’ve settled on using your
scopedSet
solution from Issue 161, for anyone else looking to use this solution, I had to wrap and spread the ternary expression to get it to work nicely:I’m using it by passing the scoped setter as an extra arg into the stores:
#161 has alternative patterns for using scopedSet.
Not sure I can contribute much else than has already been discussed in the other issues. The integrated solution in #178 looks promising, I would use it if it was a part of Zustand, but the simple solution above solves my problem just fine.