pmndrs/zustand

What's the best approach when creating/composing nested stores?

strawberrysunset opened this issue ยท 4 comments

My main problem is that even if I nest one store in another, the basic store methods (getState, setState) are exposed as opposed to its state and methods that would be returned from a hook call. Since I am unable to initialize a hook in a store, how do I properly tackle composing stores within other stores?

As an example, I would like to create a store containing an array of users. Each user would also be an individual store with state and methods. Then, in a component I could access the users and call their methods independently. For example in pseudo-code:

const {users} = useUserDatabase();
const buttons = users.map(user => <button onClick={user.setStatus('Happy')}>Make User Happy</button>)

I've thought about passing the set method from the parent store but that seems to create too much coupling between stores. I've thought about creating one monolithic store but that would be quite messy. Perhaps I'm missing something obvious but I'm not sure how to tackle this problem.

I can think of a) creating a store in React lifecycle but I'd not recommend because it's easy to violate rules of hooks, b) creating multiple vanilla stores and composing them but that can be messy with subscribes, so c) creating one monolithic store which seems like a zustand way.

Does something like this work for you?

const useUsers = create((set, get) => ({
  users: [],
  createUser: (userProps) => {
    const userId = createUniqueId()
    const user = {
      ...userProps,
      userId,
      setStatus: (status) => {
        set({
          users: get().users.map((u) => u.userId === userId ? { ...u, status } : u),
        })
      },
    }
    set({
      users: [...get().users, user],
    })
  },
})

@dai-shi Hi there, thank you very much for your reply. Yes, I think keeping it to one store is the most suitable approach for the time being. Your solution is also very helpful. Thanks ๐Ÿ‘

@dai-shi I came up with a solution which means that child objects can remain unaware of the structure of a parent store. The implementation passes a custom set method which passes the child's state instead of the parent store state. As a sidenote, I'm using immer for state updates and Maps instead of arrays to avoid having id props on children.

Here's a code sandbox demo: https://codesandbox.io/s/zustand-reference-passing-6r09r?file=/src/App.js:110-829

const createUser = (props, set, name) => ({
  name,
  ...props,
  setName: (name) => {
    set((state) => {
      state.name = name;
    });
  }
});

const useDatabase = create((set, get) => ({
  users: new Map(),
  addUser: (name) => {
    const id = createID();
    // Set method passed to child passes the user itself as state.
    const childSet = (fn) => {
      set((state) => {
        fn(state.users.get(id));
      });
    };
    const remove = () => {
      set((state) => {
        state.users.delete(id);
      });
    };
    const newUser = createUser({ remove }, childSet, name);
    set((state) => {
      state.users.set(id, newUser);
    });
  },
  getUsers: () => Array.from(get().users.values())
}));

Hi