DEV Community

Todsapon Boontap
Todsapon Boontap

Posted on

Solving zustand persisted store re-hydration merging state issue

Introduction

After spending about four months working in the Vue.js environment, I didn't choose it; the existing team was already using it. Now, I've transitioned back to React with Next.js.

The reason I chose Next.js over just React or React with Vite is because I'm intrigued by the concept of server components—a relatively new paradigm in server-side rendering that caught my interest.

Having been away from React for a while, I've noticed a significant expansion in the ecosystem of state management libraries. The last library I used for state management was Jotai. Now, there are new ones like xstate and zustand. After quickly exploring their documentation, I opted for zustand. It seems to offer an improved, less verbose version of Redux, which I believe early React developers are familiar with. Plus, I still appreciate the idea of time-traveling state.

Alright, that's enough for the introduction. Let's get started!

Usecase and issues

I've begun setting up a Zustand store following consultation with the official documentation. My goal is to partially persist certain parts of the state. Here's how I've set up the store:

Image description

I employ the slices pattern to construct and divide the main store into smaller parts, adhering to this good practice. As evident, I utilize devtools to encapsulate all state slices, but intentionally choose to persist the UserSlice.

Image description

This is how I've set up the app-user-slice. As you can see, I utilize the persist middleware from the



zustand/middleware


Enter fullscreen mode Exit fullscreen mode

package to handle this process. The expected behavior should be that upon refreshing or reloading the website, all persisted state should properly rehydrate to the state shape without any complexity.

However, a remarkable aspect that simplifies Zustand is its allowance to directly attach any side effect action to the state shape, exemplified by the login function.

This feature enables us to access the store's associated function and use it within a component, like so:

Image description

So, what are the issues here?

Upon pre-validating this code, we've realized that this component relies on the login function derived from the store to function correctly. However, as previously mentioned, this slice is a persisted slice. The problem arises when I attempt to reload the page for the first time. After the complete reload, the error surfaced, stating that the login function is undefined, although the other state properties exist.

Image description
as you can see, login function is disappear!

This error drive me an hours try figure this out, Here is what I understand and how to solve.

Initially, I questioned whether I set up the store and the slice correctly or if there was any relation to SSR hydration. Upon investigation, I discovered that the issue wasn't related to the hydration flow. Rather, it stemmed from the process of persisting the state shape to localStorage (the default setting), which involves serializing the current state behind the scenes.

The critical point here is the process of serialization.

As we're aware, JSON.stringify supports only primitive types and not object or reference types. However, because Zustand allows attaching side-effect functionalities directly to the store's state shape, this limitation becomes apparent. This is precisely why the login function disappears.

Solving & Solution

I consulted the official documentation and examined the persist middleware API specification. I came across the "merge" option, which stated:

Image description

The default implementation explain it all and this is what I need.
So I add it to my slice like this:

Image description

Why it work

Because every time the page reloads, the hydration is triggered, Zustand recreates the store with the initial state, which includes side-effect functions. Then, it loads the persisted state and attempts to merge them. So, we override the default behavior of the merge function and reassign the sideEffectFunction to currentState.sideEffectFunction (initial state).

This is the normal and simplest approach because I only have one function in this slice. However, when dealing with more functions or nested state, a more advanced approach, such as using deepMerge or Immer, can be employed to assist in merging the state shape.

Conclusion

For me, this kind of issue seems basic, something I didn't expect with such a popular library. But when it arises, it's a good reminder to revisit fundamental concepts, such as serialization and its limitations.

In terms of Zustand's intention to allow us to include functionality in the state, it's somewhat predictable that this type of issue might surface. I wonder why Zustand doesn't handle this case for us. Just imagine having to override the merge option behavior for every persisting slice—this could impact the Developer Experience (DX), right?

As users, we encounter limitations and then resolve them.

Hopefully, this insight is helpful.

References:
persist middleware merge option

Top comments (0)