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 (3)

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.