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?
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