Zustand: property derived from other properties in the same state
Asked Answered
A

5

5

Consider the following zustand store:

const useStore = ((set) => ({
   property1: 2,
   property2: 3,
   property3: 6,
   //etc.
}))

Where property 3 is a multiple of both property1 and property2. Is it possible to set it up such that property3 automatically updates when either property1 or property2 updates in zustand?

Allopath answered 29/3, 2023 at 1:6 Comment(0)
P
5

Option 1: You can use the hook inside the React component, which will have it's own state and update when property1 or property2 updates.

function Component() {
    const property1 = useStore(state => state.property1);
    const property2 = useStore(state => state.property2);

    // Just for example, this multiples property 1 by property 2
    const property3 = property1 * property2;
}

Option 2: You can use zustand-store-addons, which adds computed properties

import create from 'zustand-store-addons';

const useStore = create(
    (set) => ({
        property1: 2,
        property2: 3
    }),
    // Second object is for the addons
    {
        computed: {
            // Recomputes every time property1 or property2 changes.
            property3() {
                return this.property1 * property2;
            }
        }
    }
};

// The useStore state will look like
{
    property1: 2,
    property2: 3,
    property3: 6
}

Option 1 will be better if you're using the property only in one component, but if you'd like it to be a global property, option 2 will work better.

Plugugly answered 29/3, 2023 at 1:29 Comment(1)
There's also these middleware libraries: github.com/chrisvander/zustand-computed github.com/cmlarsen/zustand-middleware-computed-stateCigar
T
3

Take a look at zustand contributor answer

const useAuth = create((set, get) => ({
    user: {username: undefined, authLevel: 0},
    signedIn: () => !!get().user.username
})}

// in components
function Foo() {
  const signedIn = useAuth(state => state.signedIn())

// vanilla non-reactive
const signedIn = useAuth.getState().singedIn()
// vanilla reactive
useAuth.subscribe(signedIn => console.log(signedIn), state => state.signedIn())
Toponymy answered 3/1 at 15:5 Comment(2)
This appears to be the correct approach in 2024, the computed: {} suggestion in the accepted answer didn't work for me.Birdbath
Unfortunately, this is not cached and cause performance problems. I would suggest creating (or updating) setter functions like i suggested in github.com/pmndrs/zustand/issues/132#issuecomment-2096177778Malign
A
2

You can have derivative state straight from your selectors:

function Component() {
    const computedVal = useStore(s => s.property1 * s.property2);

    //...
}

Or, if your logic is too complex, you can extract it into an ordinary custom hook:

function useCustomValue() {
    const property1 = useStore(s => s.property1);
    const property2 = useStore(s => s.property2);
    return property1 * property2;
}

function Component() {
    const computedVal = useCustomValue();

    //...
}
Affray answered 9/11, 2023 at 18:10 Comment(0)
G
0

Please note, that subscription for component update matters! Your store computed getter:

{
  getSelectedTemplate: () => {
    const { templates, selectedTemplateId } = get();
    return templates.find((t) => t.id === selectedTemplateId);
  },
}

Wrong subscription in component:

const selectedTemplate = useStore(state => state.getSelectedTemplate)();

Correct subscription in component:

const selectedTemplate = useStore(state => state.getSelectedTemplate());
Godric answered 14/2 at 6:34 Comment(0)
M
0

If performance can be a problem: I would suggest creating store variable and calculating this variable (on demand) where you set your dependencies.

Here is the a gist for implementation :

lets say X depends on A and B.

With creating store setter function, you can calculate derived state on dependency changes

cons:

  • you need to track setting A or B without setter function. (since that setter function calculates derived state) maybe you are not using X yet, maybe it's not mounted yet.
  • You are still pre-calculating it (Maybe a good thing or a bad thing, depends on your business decision)

also:

  • you can check if setA or setB gets same value, if it is, return without recalculating. (I didn't add that for sake of simplicity)
// lets say X depends on A and B.
//before
set(draft => {
    draft.a = newA
})

//after
// first, create a new setFunction for a, since setting a won't be directly from now on
setA(newA){
    set(draft => {
        draft.a = newA
        draft.x = calculateNewX(draft.a, get().b)
    })
}

//second, check where you use set() to set a without a setter
set(draft => {
    draft.a = newA
})
// to
setA(newA)
Malign answered 6/5 at 15:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.