Separating Concerns with Zustand and TanStack Query


After using Redux + Saga for a while, using Zustand for my new project was both satisfying and confusing. On one hand, the simplicity of Zustand made it an obvious choice for a small PoC, on the other hand, the fact it doesn’t have any guidelines on working with APIs frustrated me a bit. Of course, Zustand is the client state management library, but let’s be honest - in modern apps the client state doesn’t exist on it’s own, in most of the cases the data comes from the backend.

Fetching data in Zustand actions

The first obvious way of syncing the client state with the server state in Zustand is to use actions and perform API calls there. Something like this: Example approach for state sync

My main concerns with such approach are:

  • mixed local state management and fetching data via APIs
  • increased complexity of the state management code by adding additional error handling to it

While data fetching was extracted to a separate module, still that code was called from Zustand actions and it felt simply wrong. I wanted to change it, and remembered the react-query server state management library, which is now called “TanStack Query”.

Managing client state and server state separately

After thinking a bit, I came up to a conclusion, it’s better to apply the “Separation of Concerns” principle and both client and server state should be managed completely separately. The resulting approach looks like this:

Using Zustand and Tanstack Query together

  • Server data is rendered in UI using the TanStack Query hooks. These hooks provide request caching and error handling.
  • User input is considered a client state and stored in the Zustand store. Once the user decides to save the information, data travels to the backend via TanStack Mutation Query.

In the real world, it looks like this on the UI side:

Autocomplete example

Auto-complete is a great example, since it’s a dynamic combination of user input (the search term) and the server data( search suggestions). The input of the TanStack query is the search term stored in Zustand.

The fetching process itself is pretty straightforward:

Tanstack Query example

Note it’s already includes caching by the search term and only performs requests to the server for search terms of two characters or more.

Final thoughts

Having two separate tools for client and server side state management may seem overkill, though for me it simplifies lots of things since such an approach provides responsibility boundaries.