-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
Comments
I usually would just do something like |
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 |
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. |
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) |
@drcmda I don't think getters work because of |
oh thats right, bummer, would have been an interesting pattern |
Summary: 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. |
Thanks @dai-shi for your response.
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? |
Correct. I think that will be overkill for the Zustand core.
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. |
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) |
It's almost the same as #132 (comment) |
So, the new project is jotai. |
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 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) |
@woehrl01 it won't be cached, isn't it? |
@platon-rov no, it won't be cached. For caching you likely need to use a subscription to explicitly track and update dependencies. |
The correct syntax for a getter according to typescript is How does it work? @woehrl01 |
@harry-sm you need a return then as well get isSignedIn() { return !!get().user.usernamecomponents? } |
While the |
Can make a custom hook to update on changes
but its just the same work to compute the value on the fly |
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>)
} |
HI there @dai-shi, 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 would recommend this as a general practice. const signedIn = useAuth(state => !!state.user.username); |
If you are using {
name: 'products',
partialize: (state) => ({
products: state.products,
// don't persist computed values
}),
} |
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. |
How about using memoized selectors created with |
Using memoized selectors should be good. I also develop proxy-memoize. |
I'm using Zustand in TypeScript. Any idea what type |
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. |
it would cause all the code refactored to be one level down.... |
Yes, getter hacking can be a problem in where calculations are expensive.
With creating store setter function, you can calculate derived state on dependency changes cons:
also:
// 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) |
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;
},
})
)
); |
immerjs/immer#941 immerjs/immer#1015 import { create } from "zustand";
const useAppStore = create<{
count: number;
}>(() => ({
count: 1,
}));
const useDoubleCount = () => {
return useAppStore((state) => state.count * 2);
} |
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:
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?
The text was updated successfully, but these errors were encountered: