DEV Community

Eray Kaya
Eray Kaya

Posted on

Optimizing Zustand: How to Prevent Unnecessary Re-renders in Your React App

The problem: When you create a store in Zustand and use it across components, any change in the store's state will cause the component to rerender, even if the component is not related to that particular state. For example, if you have a store like the one below and call useBearStore in your component, whenever the cats state is updated, your bear component will also rerender.

import create, { SetState } from "zustand";

interface BearStore {
  bears: number;
  cats: number;
  incrementCats: () => void;
  increase: (by: number) => void;
  increment: () => void;
}

export const bearStore = (set: SetState<BearStore>) => ({
  bears: 0,
  cats: 0,
  increase: (by: number) => {
    set((state) => ({ bears: state.bears + by }));
  },
  increment: () => {
    set((state) => ({ bears: state.bears += 1 }));
  },
  incrementCats: () => {
    set((state) => ({ cats: state.cats += 1 }));
  }
});

export const useBearStore = create<BearStore>(bearStore);
Enter fullscreen mode Exit fullscreen mode

For example if you have a store like this and call useBearStore in your component whenever cats state updated your bear component will rerender also.

The Solution: To prevent this issue, we can create a simple utility function using shallow in Zustand.

import { StoreApi, UseBoundStore } from "zustand";
import shallow from "zustand/shallow";

type GenericState = Record<string, any>;

export const createStoreWithSelectors = <T extends GenericState>(
  store: UseBoundStore<StoreApi<T>>,
): (<K extends keyof T>(keys: K[]) => Pick<T, K>) => {
  const useStore: <K extends keyof T>(keys: K[]) => Pick<T, K> = <K extends keyof T>(keys: K[]) => {

    return store((state) => {
      const x = keys.reduce((acc, cur) => {
        acc[cur] = state[cur];
        return acc;
      }, {} as T);

      return x as Pick<T, K>;
    }, shallow);
  };

  return useStore;
};
Enter fullscreen mode Exit fullscreen mode

Then we can update our initial store to use this utility function:

const bearStore = create<BearStore>(bearStore);

export const useBearStore= createStoreWithSelectors(bearStore);
Enter fullscreen mode Exit fullscreen mode

Now we can use it in components like this:

const { bears, increment } = useBearStore(["bears", "increment"]);

Enter fullscreen mode Exit fullscreen mode

With this change, the component won't rerender even if the cats state is updated.

Top comments (5)

Collapse
 
soroshzzz26 profile image
M.Soroush Zamzam

thanks
really helpful

Collapse
 
benemma profile image
Ben Emma

Amazing, however
however, will produce a type error as

_

keys.reduce is not a function
_

because keys is not an array but rather an object.

The best way around that potential error is to use use a for...in loop to iterate through the keys object like so...

``export const createStoreWithSelectors = (
store: UseBoundStore>
): ((keys: K[]) => Pick) => {
const useStore: (keys: K[]) => Pick = <
K extends keyof T

(
keys: K[]
) => {
return store((state) => {
const x: Partial = {};

  if (Array.isArray(keys)) {
    for (const key of keys) {
      x[key] = state[key];
    }
  }

  return x as Pick<T, K>;
}, shallow);
Enter fullscreen mode Exit fullscreen mode

};

return useStore;
};

Collapse
 
eraywebdev profile image
Eray Kaya

Keys should be provided as an array, as shown in the example. Adding error handling to display a message indicating that keys should be an array would be a nice addition.

Collapse
 
sven1106 profile image
Sven1106

Just out of curiosity, is there a specific reason why you created this instead of using react-tracked :)

Collapse
 
eraywebdev profile image
Eray Kaya

Hey, I wasn't aware of react-tracked; it looks interesting. However, I wouldn't consider using it in production for a couple of reasons:

  • The last published date is 9 months ago, which raises concerns about its maintenance and compatibility with newer versions of zustand.

  • Let's say I'm already using Zustand for managing global state in my project. If I decide to upgrade to a newer version of Zustand, I only need to consider that library. Adding another dependency like react-tracked introduces unnecessary complexity. This might not be a big issue for smaller hobby projects, but for larger projects with 50+ stores or slices, it becomes a concern.

While it's true that we shouldn't reinvent the wheel, there's a balance to strike. We should avoid adding a new library for every single detail. What I've implemented here can be achieved without creating an additional dependency, which keeps things more streamlined in my opinion.