react
zustand
context
typescript
state-management

This article assumes basic familiarity with React and Zustand.

Using Zustand with React Context

The global store provided by the Zustand library offers significant benefits. However, the globally created store can pose challenges, especially when navigating between different pages within your application. Consider a scenario where you have several components located under /creator, and you navigate the user to /docs/preview?id=. What happens then? The state altered within /creator persists, and the same state is accessible under the new path. Refer to the following GIF to visualize this behavior:

Global store behavior Global store behavior

While this behavior might be acceptable in some cases, there are situations where you require a clean state upon landing on specific parts of the application. While using Zustand alone provides this option, it involves manual invocation of reset actions, which can be cumbersome to maintain.

const MyCounter = () => {
  const { count, increment } = useStore();

  useEffect(() => {
    return () => {
      // Reset state when the component unmounts.
      useStore.reset();
    };
  }, []);

  return <div>Count: {count}</div>;
};

By adopting this approach, you achieve the desired outcome: a clean state when returning to a page.

Reset action call Reset action call

However, it introduces maintenance challenges, and the store remains global, susceptible to interference from other pages. Managing this becomes a constant headache, especially with the addition of new routes or the occasional oversight.

In real-world applications, complexity abounds, and having global accessibility perpetually leads to maintenance headaches. Today, we'll explore integrating Zustand and Context to create a modular store.

Why not just use Context? If you're already leveraging Zustand, manually adding logic in Context alone, would not align with other state management practices. The reason is that Zustand excels in state management, offering a streamlined approach with minimal boilerplate code.

Integrating Zustand with Context

To address this issue, we can integrate Zustand with React Context, enabling the creation of multiple stores, each associated with its dedicated components tree. This eliminates the need for manual reset actions.

Implementation

import React from 'react';
import { create } from 'zustand';

interface CreatorProviderValue {
  count: number;
  increment(): void;
}

const Context = React.createContext<CreatorProviderValue | null>(null);

const CreatorProvider = ({ children }: { children: React.ReactNode }) => {
  // Store is created once per Provider usage.
  const useStore = React.useMemo(
    () =>
      create<CreatorProviderValue>((set) => ({
        count: 0,
        increment: () => {
          set(({ count }) => ({ count: count + 1 }));
        },
      })),
    [],
  );

  const store = useStore();

  return <Context.Provider value={store}>{children}</Context.Provider>;
};
// Hook to access Context that indirectly exposes the created store.
const useCreatorProvider = (): CreatorProviderValue => {
  const ctx = React.useContext(Context);

  if (!ctx) throw Error(`Lack of provider`);

  return ctx;
};

export { CreatorProvider, useCreatorProvider };

Usage

Wrap your components tree with the Provider and utilize useCreatorProvider to access the store:

const ConnectedCreatorView = () => (
  <CreatorProvider>
    <CreatorView />
  </CreatorProvider>
);

export default ConnectedCreatorView;

Inside CreatorView, utilize the hook:

const CreatorView: React.FC = () => {
  const creator = useCreatorProvider();

Zustand integrated with Context Zustand integrated with Context

Now, the solution is truly modular. By concealing the direct access to the created store and passing it down solely through Context, we effectively block the option to import the store and use it directly. This approach resembles a getter for a private function or property in a class, ensuring encapsulation and modularity.

Conclusion

Integrating Zustand with React Context offers a flexible solution for managing state in React applications. By encapsulating state within dedicated component trees, you can ensure a modular and maintainable codebase without the need for manual reset actions. Whether opting for a global or modular state management approach, the combination of Zustand and React Context provides the versatility to meet your application's requirements effectively.

If you're interested about Context API best practices, I can recommend following article: https://greenonsoftware.com/articles/react/common-mistakes-in-using-react-context-api/ as a bonus.