reactjszustand

How to subscribe / unsubscribe to a Zustand nested object store?


I have a Zustand store as follows:

import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';

const initialRating = {
  id: 'rating1',
  stars: 5,
  score: 3,
  size: '24px',
  title: 'Food'
};

export const useRatingStore = create()(
  immer((set) => ({
    ratings: {
      [initialRating.id]: initialRating
    },

    removeAllRatings: () =>
      set((state) => {
        state.ratings = {};
      }),
    newRating: (payload) =>
      set((state) => {
        if (state.ratings[payload.id]) {
          console.error('Rating already exists', payload);
        } else {
          state.ratings[payload.id] = payload;
          if (payload.score > payload.stars) {
            state.ratings[payload.id].score = payload.stars;
          }
        }
      }),

    updateScore: (payload) =>
      set((state) => {
        const toUpdate = payload;
        if (Array.isArray(toUpdate)) {
          toUpdate.forEach((rating) => {
            if (state.ratings[rating.id]) {
              const proxyState = state.ratings[rating.id];
              proxyState.score = Math.min(rating.score, proxyState.stars);
            } else {
              console.error('updateScore not found', rating.id);
            }
          });
        } else if (state.ratings[toUpdate?.id]) {
          const proxyState = state.ratings[toUpdate.id];
          proxyState.score = Math.min(toUpdate.score, proxyState.stars);
        } else {
          console.error('Invalid updateScore payload', payload);
        }
      }),
    updateStars: (payload) =>
      set((state) => {
        const toUpdate = payload;
        if (Array.isArray(toUpdate)) {
          toUpdate.forEach((rating) => {
            if (state.ratings[rating.id]) {
              const proxyState = state.ratings[rating.id];
              proxyState.stars = rating.stars;
            } else {
              console.error('updateStars not found', rating.id);
            }
          });
        } else if (state.ratings[toUpdate?.id]) {
          const proxyState = state.ratings[toUpdate.id];
          proxyState.stars = toUpdate.stars;
        } else {
          console.error('Invalid updateStars payload', payload);
        }
      }),
    updateSize: (payload) =>
      set((state) => {
        const toUpdate = payload;
        if (Array.isArray(toUpdate)) {
          toUpdate.forEach((rating) => {
            if (state.ratings[rating.id]) {
              state.ratings[rating.id].size = rating.size;
            } else {
              console.error('updateSize not found', rating.id);
            }
          });
        } else if (state.ratings[toUpdate?.id]) {
          state.ratings[toUpdate.id].size = toUpdate.size;
        } else {
          console.error('Invalid updateSize payload', payload);
        }
      })
  }))
);

It keeps track of a variable number of ratings. Works like a charm with reactJS. Now I need to react to changes in individual components in non-ui parts of the app. The subscribe methods seems to be a good candidate. I can do:

useRatingStore.subscribe((state, oldState) => console.log('state', state, 'oldState', oldState));

and get all ratings. However I'd like to subscribe to specific ratings only. How do I do this? Also: how to unsubscribe?


Solution

  • You can use subscribeWithSelector middleware and then in useRatingStore.subscribe, you can pass a selector (first argument) to listen for specific state change and second selector will be called with new and previous state value when state changes.

    Here's the code

    import { create } from 'zustand';
    import { subscribeWithSelector } from 'zustand/middleware';
    import { immer } from 'zustand/middleware/immer';
    
    const initialRating = {
        id: 'rating1',
        stars: 5,
        score: 3,
        size: '24px',
        title: 'Food'
    };
    
    export const useRatingStore = create()(
        immer(
            subscribeWithSelector((set) => ({
                ratings: {
                    [initialRating.id]: initialRating
                },
    
                removeAllRatings: () =>
                    set((state) => {
                        state.ratings = {};
                    }),
                newRating: (payload) =>
                    set((state) => {
                        if (state.ratings[payload.id]) {
                            console.error('Rating already exists', payload);
                        } else {
                            state.ratings[payload.id] = payload;
                            if (payload.score > payload.stars) {
                                state.ratings[payload.id].score = payload.stars;
                            }
                        }
                    })
    
                // ... Rest of the store
            }))
        )
    );
    
    useRatingStore.subscribe(
        (state) => state.ratings?.rating1,
        (state, oldState) => {
            console.log('state', state, 'oldState', oldState);
        }
    );
    

    You can read more subscribeWithSelector here