Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using getter to calculate computed values #132

Closed
filipesmedeiros opened this issue Aug 5, 2020 · 32 comments
Closed

Using getter to calculate computed values #132

filipesmedeiros opened this issue Aug 5, 2020 · 32 comments

Comments

@filipesmedeiros
Copy link

filipesmedeiros commented Aug 5, 2020

Hey!

Is it ok to use the getter to return a computed value in a function? Since using it in a value doesn't work?

Example:

const [useAuth] = create((set, get) => ({
    user: {username: undefined, authLevel: 0},
    // isSignedIn: !!get().user.username // doesn't work because it's executed before the store is created, so get() returns undefined right?
    isSignedIn: () => !!get().user.username // should work, just call isSignedIn() instead of using as a value, in the components?
})}

Also, I feel like computed values are a cool features for state management libraries to have out of the box, are there planes to add it? Do you guys need help with it somehow?

@shahruz
Copy link

shahruz commented Aug 6, 2020

I usually would just do something like const isSignedIn = useAuth(state => !!state.user.username); in the consuming component for this.

@filipesmedeiros
Copy link
Author

Right! That seems prettier. But that only works for component use right? If I want to separate some logic while calculating various layers of computed values in a store (in more complex cases) that doesn't work anymore. For example:

const [useStore] = create((set, get) => {
    value: 30,
    value1: get().value + 60,
    value2: get().value1 / 4,
    value3: get().value2 * 1000
})

Logically, it might be better if value3 depends on value2 directly than if you had to repeat the computations you did for the other ones, right?

@dai-shi dai-shi added the enhancement New feature or request label Aug 13, 2020
@jordanmkoncz
Copy link

I was also curious about the recommended approach for selectors which derive/compute values from state (and possibly from other selectors themselves). Reading about Recoil and how it handles this using selectors (also see https://www.youtube.com/watch?v=_ISAA_Jt9kI), I couldn't see anything analogous in Zustand which has the same optimisations.

@drcmda
Copy link
Member

drcmda commented Aug 21, 2020

sure you can use getters like that.

as for recoil and atoms, it's a slightly different paradigm. we have something coming up that's concentrating on that approach.

in theory even this should work

const useAuth = create((set, get) => ({
    user: {username: undefined, authLevel: 0},
    get 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)

@dai-shi
Copy link
Member

dai-shi commented Aug 21, 2020

@drcmda I don't think getters work because of Object.assign(). Once you setState({ user }), signedIn becomes a boolean value. I didn't try it, so correct me if I'm wrong.

@drcmda
Copy link
Member

drcmda commented Aug 21, 2020

oh thats right, bummer, would have been an interesting pattern

@dai-shi
Copy link
Member

dai-shi commented Aug 21, 2020

Summary:
So there are two topics in this issue.
a) Support object getters in store
b) Chaining computed values (aka atoms)

For b), we are planning a new project coming soon, and it's outside the scope of zustand.

For a), it's a possible enhancement, but the implementation might be a bit tricky. I'm not sold much, because we don't support b) anyway.

We should be able to do this without object getters.

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

Now, reading the thread from the beginning again, I noticed I misunderstood the original issue. The OP never said object getters. So, the above example would be the answer.

That said, I think it's fairly OK to mark this issue as resolved.

@dai-shi dai-shi closed this as completed Aug 21, 2020
@dai-shi dai-shi removed the enhancement New feature or request label Aug 21, 2020
@jordanmkoncz
Copy link

Thanks @dai-shi for your response.

b) Chaining computed values (aka atoms). For b), we are planning a new project coming soon, and it's outside the scope of zustand.

Just to confirm, you're saying that chaining computed values in a similar fashion to Recoil and how it handles selectors (where computed values are automatically re-computed when one of their 'dependencies' changes, and only re-computed when this happens) is not something you plan to add to Zustand itself, but instead you are planning on creating a new project that will address this and can be used with Zustand?

@dai-shi
Copy link
Member

dai-shi commented Aug 24, 2020

Hi @jordanmkoncz

chaining computed values is not something you plan to add to Zustand itself

Correct. I think that will be overkill for the Zustand core.
It would be possible for middleware, though.

instead you are planning on creating a new project that will address this and can be used with Zustand?

Yes for the first part and no for the second. The new project is something different from Zustand, but with the simplicity philosophy from Zustand.

@timkindberg
Copy link

Another option is pure functions:

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

const select = {
    signedIn: state => state.user.username
}

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

@dai-shi
Copy link
Member

dai-shi commented Sep 10, 2020

It's almost the same as #132 (comment)
Having selectors outside render is better.
Yeah, this is probably the best approach with Zustand (+React).

@dai-shi
Copy link
Member

dai-shi commented Sep 10, 2020

The new project is something different from Zustand, but with the simplicity philosophy from Zustand.

So, the new project is jotai.

@woehrl01
Copy link

woehrl01 commented May 8, 2022

I'd like to share that it's definitely possible to have computed fields, it just needs to be constructed differently to don't fall into the Object.assign issue. But this requirement makes the code even more readable and explicit:

const [useAuth] = create((set, get) => ({
    user: {username: undefined, authLevel: 0},
    computed: { //yes, just use a nested object, which can be easily used in `Object.assign`
       get isSignedIn: () => !!get().user.usernamecomponents?
    }
})}

//usage
const isSignedIn = useState(s => s.computed.isSignedIn)

@platon-rov
Copy link

@woehrl01 it won't be cached, isn't it?

@woehrl01
Copy link

@platon-rov no, it won't be cached. For caching you likely need to use a subscription to explicitly track and update dependencies.

@harry-sm
Copy link

harry-sm commented Sep 2, 2022

The correct syntax for a getter according to typescript is
get isSignedIn () { !!get().user.usernamecomponents? }
not
get isSignedIn: () => !!get().user.usernamecomponents?

How does it work? @woehrl01

@evanpurkhiser
Copy link

@harry-sm you need a return then as well

get isSignedIn() { return !!get().user.usernamecomponents? }

@SrBrahma
Copy link

While the computed solution is great and worked for one of my necessities, I suggest having here also a subscription example for cached values as it's a common thing to need

@laclance
Copy link

laclance commented Nov 1, 2022

Can make a custom hook to update on changes

export default function useForceUpdate(updateOn: (state: BoundStore) => any) {
  const [update, toggleUpdate] = useState(false);

  useEffect(() => {
    const unsub = useBoundStore.subscribe(
      state => updateOn(state),
      () => toggleUpdate(!update)
    );

    return () => {
      unsub();
    };
  }, [update, updateOn]);
}

but its just the same work to compute the value on the fly
const isSignedIn = useBoundStore(state => !!state.user.usernamecomponents)

@nxl3477
Copy link

nxl3477 commented Jun 15, 2023

Like this

// store.ts
const useAuth = create((set, get) => ({
    user: {username: undefined, authLevel: 0}
})}


export const computed = {
  isSignedIn: () => useAuth(state => state.user.username)
}


// index.tsx

import { computed } from './store'

export default function page () {
  const isSignedIn = computed.isSignedIn()
  return (<div>{isSignedIn}</div>)
}

@itayperry
Copy link
Contributor

HI there @dai-shi,
I looked at the code you wrote here and it really helped me! Thank you :)

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

// in components
function Foo() {
  const signedIn = useAuth(state => state.signedIn()); // ✨✨✨ This one :)

// vanilla non-reactive
const signedIn = useAuth.getState().singedIn()
// vanilla reactive
useAuth.subscribe(signedIn => console.log(signedIn), state => state.signedIn())

I really think this should be in the docs under a section called "computed", "derived-state", "getters" or something.. it's really helpful.
I know about the derived-zustand package (although it's outside the store and not in it), which is more flexible and complex, maybe it's worth mentioning too in the docs. It's just an idea of course and I don't wanna waste your time :)

@dai-shi
Copy link
Member

dai-shi commented Aug 22, 2023

const signedIn = useAuth(state => state.signedIn()); // ✨✨✨ This one :)

I would recommend this as a general practice.

  const signedIn = useAuth(state => !!state.user.username);

@ossomaster
Copy link

ossomaster commented Aug 28, 2023

If you are using persist middleware to store the data in localStorage it is necessary to remove the computed values in the configuration with partialize, example:

{
	name: 'products',
	partialize: (state) => ({
		products: state.products,
                 // don't persist computed values
	}),
}

@rodrigocfd
Copy link

I'd like to share that it's definitely possible to have computed fields, it just needs to be constructed differently to don't fall into the Object.assign issue. But this requirement makes the code even more readable and explicit:

const [useAuth] = create((set, get) => ({
    user: {username: undefined, authLevel: 0},
    computed: { //yes, just use a nested object, which can be easily used in `Object.assign`
       get isSignedIn: () => !!get().user.usernamecomponents?
    }
})}

//usage
const isSignedIn = useState(s => s.computed.isSignedIn)

While this approach is very clean, it's important to understand that there is a performance penalty when compared to the custom hook approach.

Just like the custom hook, the getter function will run every time the value is requested – that is, every time the displaying component re-renders. That's fine, and that's what we expect.

However, in addition, this getter will also run every time the state changes. So, if your computation is too heavy, it's probably not a good approach. In such heavy-computation cases, you will want to memoize the computation result.

@levino
Copy link

levino commented Feb 2, 2024

How about using memoized selectors created with createSelector from reselect? The memoization cache can be shared between components somehow, I suppose. And in most cases a global memoization cache might suffice. One can also chain selectors and their memoization.

@dai-shi
Copy link
Member

dai-shi commented Feb 2, 2024

Using memoized selectors should be good. I also develop proxy-memoize.

@amoghkulkarnifr
Copy link

I'd like to share that it's definitely possible to have computed fields, it just needs to be constructed differently to don't fall into the Object.assign issue. But this requirement makes the code even more readable and explicit:

const [useAuth] = create((set, get) => ({
    user: {username: undefined, authLevel: 0},
    computed: { //yes, just use a nested object, which can be easily used in `Object.assign`
       get isSignedIn: () => !!get().user.usernamecomponents?
    }
})}

//usage
const isSignedIn = useState(s => s.computed.isSignedIn)

I'm using Zustand in TypeScript. Any idea what type computed state variable should be of? Can we avoid any?

@levino
Copy link

levino commented Mar 7, 2024

I really think this getter-hacking is an awful anti-pattern. This is exactly what selectors are for. For performance, use memoized selectors which can be made easily with reselect. TypeScript will also be happy.

@MarkShawn2020
Copy link

I'd like to share that it's definitely possible to have computed fields, it just needs to be constructed differently to don't fall into the Object.assign issue. But this requirement makes the code even more readable and explicit:

const [useAuth] = create((set, get) => ({
    user: {username: undefined, authLevel: 0},
    computed: { //yes, just use a nested object, which can be easily used in `Object.assign`
       get isSignedIn: () => !!get().user.usernamecomponents?
    }
})}

//usage
const isSignedIn = useState(s => s.computed.isSignedIn)

it would cause all the code refactored to be one level down....

@doruksahin
Copy link

I really think this getter-hacking is an awful anti-pattern. This is exactly what selectors are for. For performance, use memoized selectors which can be made easily with reselect. TypeScript will also be happy.

Yes, getter hacking can be a problem in where calculations are expensive.
Here is another approach:

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)

@yasintz
Copy link
Contributor

yasintz commented Jun 27, 2024

I created zustand-computed-state library to manage computed states in Zustand using getters.

const useStore = create()(
  computed((set, get) =>
   ({
      count: 1,
      inc: () => set(prev => ({ count: prev.count + 1 })),
      dec: () => set(state => ({ count: state.count - 1 })),
      get countSq() {
        return this.count ** 2;
      },
    })
  )
);

@childrentime
Copy link

immerjs/immer#941 immerjs/immer#1015
As mentioned in Immer's PR, using getters with immutable data is not idiomatic. I believe we should always strive to achieve getter-like effects in a 'React way'.

import { create } from "zustand";

const useAppStore = create<{
  count: number;
}>(() => ({
  count: 1,
}));

const useDoubleCount = () => {
  return useAppStore((state) => state.count * 2);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests