Introducing zustand-entity-adapter

Michael De Abreu - Sep 21 - - Dev Community

Hello there. If you are following this series, you know I sent this proposal to the Zustand team. After some weeks, their feedback was to wait, stating that this was in the field of third-party libraries. Considering this, and since I didn't find anything similar, I created zustand-entity-adapter.

What is zustand-entity-adapter?

It's a tiny (~3kb/~1kb gzipped) library to create an Entity Adapter for Zustand. It also allows you to create a convenient store that includes any additional structure and the actions and selectors to be used to manage the entities.

Moreover, this library takes advantage of how simple Zustand is and integrates with it very well.

How to use zustand-entity-adapter?

Some of the API for the library has been developed in this series and can be used as documentation. Additionally, you can find more information on Entity Adapters from @ngrx and RTK.

As you may notice, this API draws inspiration from both the @ngrx/entity package and RTK's createEntityAdapter. But since Zustand is not using a reducer, there are some differences between those implementations and this one.

createEntityAdapter

When using createEntityAdapter, this will return an object with methods getState, getSelectors, and getActions. They all have similar counterparts in both implementations, but they have subtle differences because of Zustand's nature. Also, the options for createEntityAdapter are a little different from those of @ngrx and RTK.

  • idSelector: In @ngrx/entity and RTK, this is like the selectId option. This only accepts a function that takes an entity and returns the value that should uniquely identify that entity.

  • sort: In @ngrx/entity and RTK, this is like the sortComparer option. This only accepts a function that takes two entities and returns a number that will be used to sort them.

getActions

In both the @ngrx/entity and RTK implementations, the EntityAdapter has the actions as direct methods of the adapter. This is not possible here because, to do so, we would need the store information, but to create the store, we need the adapter information. Because of this, the actions are generated by a getActions method.

With this approach, we have an additional advantage. In Zustand, we usually have the state and actions together, although it also allows using no-store actions. By externalizing the action generation, you can use either approach to configure your store.

  • Using state and actions together:
    const adapter = createEntityAdapter<User, string>();
    type StoreState = ReturnType<typeof adapter.getState> & ReturnType<typeof adapter.getActions>
    const store = create<StoreState>((set) => ({
      ...adapter.getState(),
      ...adapter.getActions(set),
    }));
Enter fullscreen mode Exit fullscreen mode
  • Using external actions:
    const adapter = createEntityAdapter<User, string>();
    const store = create(adapter.getState);
    const actions = adapter.getActions(store.setState);
Enter fullscreen mode Exit fullscreen mode

Although the actions generated are more or less the same as in both @ngrx/entity and @reduxjs/toolkit, you need to consider that here, actions only need the payload to be used.

  actions.addOne({ id: uuid(), name: 'John Doe' });
Enter fullscreen mode Exit fullscreen mode

getSelectors

In this initial version of the library, the adapter doesn't have a concept of what a slice is, and because of this, neither do the selectors. The getSelectors method creates the same selectors @reduxjs/toolkit generates with adapter.getSelectors(), but it doesn't allow any argument to compute the property; therefore, the selection will always be from the root of the state.

getState

Because Zustand allows adding properties more easily than Redux, instead of having a concept of "initial state", getState only has the state of the entity adapter. If you want to add more properties to the store, just do so.

  const adapter = createEntityAdapter<User, string>();
  interface StoreState extends ReturnType<typeof adapter.getState> {
    selectedUser: User | undefined;
  }
  const store = create<StoreState>(() => ({
    ...adapter.getState(),
    selectedUser: undefined
  }));
Enter fullscreen mode Exit fullscreen mode

That's all folks!

I hope you like this library as much as I enjoyed developing it. In another post, I'll talk more about how to use the useEntityStore function.

Thanks a lot!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .