-
Notifications
You must be signed in to change notification settings - Fork 1
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
atomWithActions and create? #7
Comments
I was hoping something like this might be possible, essentially creating a interface State {
allItems: Item[]
firstItemAtom: Atom<Item | undefined> | undefined
}
interface Actions {
selectItems: (items: Item[]) => void
}
const storeAtom = atomWithActions<State, Actions>(
{
allItems: [],
firstItemAtom: undefined, // computed (read-only) - and independently subscribable without selector
},
(set, get) => {
return {
firstItemAtom: atom<Item | undefined>((_set, _get) => {
console.log("jotai/computed")
const items = get().selectedItems
return items.length > 0 ? items[0] : undefined
}),
selectItems(allItems) {
set({ allItems })
},
}
}
) ... but I cannot get the const storeAtom = atomWithActions<State & Actions>(
(set, get) => {
return {
allItems: [],
firstItemAtom: atom<Item | undefined>((_set, _get) => {
console.log("jotai/computed")
const items = get(this).selectedItems // perhaps bind state atom to this, or make it available as _get()
return items.length > 0 ? items[0] : undefined
}),
selectItems(allItems) {
set({ allItems })
}
}
}
) In any case, when using it I guess it would be something like this: // render optimized - expose sub-state (sub-atoms) directly for direct subscription
function Example() {
// const { allItems, firstItemAtom, selectItems } = useAtomValue(storeAtom)
const firstItem = useAtomValue(storeAtom.firstItem)
return <p>First item {firstItem}</button>
} |
That's an interesting requirement.
Yeah, that's kind of an experiment. kind of trying to prove Jotai includes Zustand capability-wise. ref: https://x.com/dai_shi/status/1833089238482698515 But, if your issue is performance, I don't think it helps, because it's backed by one atom.
Sounds too tough if we want a real compatibility, but if it's just about the programming model, it might be feasible. I haven't thought about it. |
Yeah, my main concern isn't compatibility, but performance while preserving the centralized state programming model of Zustand. Using |
Can we start with a very small problem? How would it look like with the programming model in your mind? |
Yes, that's it. I guess there are many ways to solve this, I was just wondering what the best/recommended way to do it is, while leveraging just Zustand and/or Jotai. |
With only Jotai, it would be just two separate atoms. If I understand it correctly, that's not the programming model in your mind. Another idea with Jotai is to define a store atom and derived smaller atoms. const stateAtom = atom({ a: 0, b: 0 });
const actionsAtom = atom(null, (get, set, action: { type: 'INC_A' } | { type: 'INC_B' }) => {
if (action.type === 'INC_A') {
set(stateAtom, (prev) => ({ ...prev, a: prev.a + 1 }));
}
if (action.type === 'INC_B') {
set(stateAtom, (prev) => ({ ...prev, b: prev.b + 1 }));
}
});
export const useDispatch = () => {
const dispatch = useSetAtom(actionsAtom);
return dispatch;
}
const stateAAtom = atom((get) => get(stateAtom).a);
const stateBAtom = atom((get) => get(stateAtom).b);
export const useStateA = () => useAtomValue(stateAAtom);
export const useStateB = () => useAtomValue(stateBAtom); |
And if we try to solve this by being closer to the Zustand model with a central store and functions instead of flux/redux actions? I understand that you sometimes really want to define your state in a distributed and tree shakable way (like a Jotai solution would allow for), but very often keeping it all encapsulated into one store/object makes it easier to confidently reason about (more Zustand-ish), and that's important. I think it'd be something like this all holistically integrated into one easy-to-understand Zustand store with:
And support using the store not only through selectors, but also through direct subscriptions to the computed values, so there's no overhead if the computed values doesn't change. |
How about this? import { create } from 'jotai-zustand';
const [useSelector, useAction, useComputed] = create({
state: { a: 1, b: 2 },
actions: {
incA: () => (state) => ({ ...state, a: state.a + 1 }),
incB: () => (state) => ({ ...state, b: state.b + 1 }),
},
computeds: {
a: (state) => state.a,
sum: (state) => state.a + state.b,
},
});
// usage
const a2 = useSelector((state) => state.a * 2);
const incA = useAction('incA');
const sum = useComputed('sum'); |
I think functionally that would be there, as long as computeds (perhaps even state) could be directly/atomically subscribed to, and ideally computeds could depend on other computeds. But aesthetically I think it would be better with a smaller API surface in terms of number of hooks, and perhaps avoid using labeled strings when JS properties could be used, e.g., import { create } from 'jotai-zustand';
const [useStore, store] = create({
state: { a: 1, b: 2 },
actions: {
incA: () => (state) => ({ ...state, a: state.a + 1 }),
incB: () => (state) => ({ ...state, b: state.b + 1 }),
},
computeds: {
a: (state) => state.a,
sum: (state) => state.a + state.b,
sumDoubled: (state) => 2 * state.sum
},
});
// usage
const a2 = useStore(store.a2)
const incA = useStore(store.incA)
const sum2x = useStore(store.sumDoubled)
const a2selectorStyle = useStore(state => state.a2) Or something like that...? But I guess this may be even nicer: import { create } from 'jotai-zustand';
const [useStore, store] = create((set, get) => ({
a: 1,
b: 2
get sum() { return get().a + get().b }, // computed
get dblSum() { return get().sum * 2 }, // computed that depends on computed
sum3x: get().sum * 3, // or could this way of doing computeds be possible?
incA: () => (state) => ({ ...state, a: state.a + 1 }),
incB: () => (state) => ({ ...state, b: state.b + 1 }),
}));
// usage
const a2 = useStore(store.a2)
const incA = useStore(store.incA)
const sum2x = useStore(store.dblSum) // subscription only "fires" if dblSum value changes
const a2selectorStyle = useStore(state => state.a2) |
That would be pretty difficult, or extremely tricky. Why did you close the issue? |
Sorry, I didn't mean to close the issue. Pressed the wrong button! Using labels isn't a big deal, just (I think at least) a slight improvement in DX. How about the other aspects? |
We need to explicitly separate actions from values (or original state or computeds). Having a single hook for actions and values is technically possible, but it's not typescript friendly. It's kind of a bad aspect of Zustand, because its API is designed before TypeScript era. |
I think explicitly separating out state, actions, computeds still gets the job done. So that would be possible? In terms of combining them, would it help to make computeds be values too (but tracked and therefore updated), so only actions are functions, and then use typescript utility types to separate the actions from the values/comptueds? E.g., type StoreState = {
a: number
b: number
sum: number
sum3x: number
incA: () => void
incB: () => void
}
const [useStore, store] = create<StoreState>((set, get) => ({
a: 1,
b: 2,
sum: get().a + get().b,
sum3x: get().sum * 3,
incA: () => (state) => ({ ...state, a: state.a + 1 }),
incB: () => (state) => ({ ...state, b: state.b + 1 })
}))
// usage
const a2 = useStore(store, "a2")
const incA = useStore(store, "incA")
const sum3x = useStore(store, "sum3x") // subscription only "fires" if sum value changes
const a2selectorStyle = useStore((state) => state.a2)
// separating out types
type FunctionProperties<T> = {
[K in keyof T]: T[K] extends Function ? K : never
}[keyof T]
type NonFunctionProperties<T> = {
[K in keyof T]: T[K] extends Function ? never : K
}[keyof T]
type FunctionsOnly<T> = Pick<T, FunctionProperties<T>>
type ValuesOnly<T> = Pick<T, NonFunctionProperties<T>>
type F = FunctionsOnly<StoreState>
// ^ { incA: () => void; incB: () => void }
type V = ValuesOnly<StoreState>
// ^ { a: number; b: number; sum: number; sum3x: number } |
It's not about TypeScript issue. Combining actions would complicate the implementation. Combining computeds and values would be easier. Computeds must be "functions". const [useValue, useAction] = create({
a: 1,
b: 2,
sum: (state) => state.a + state.b, // But, this doesn't work with Type inference
}, {
incA: () => (state) => ({ ...state, a: state.a + 1 }),
incB: () => (state) => ({ ...state, b: state.b + 1 }),
}); Not very pretty? |
If we were to design a new API now, it should be type inferable, I believe. check out https://stately.ai/docs/xstate-store for reference. |
const useStore = create({ a: 1, b: 2 })
.computeds({ sum: (state) => state.a + state.b })
.computeds({ sum2: (state) => state.sum * 2 })
.actions({
addA: (n: number) => (state) => ({ ...state, a: state.a + n })
});
// usage
const a = useStore('a')
const addA = useStore('addA') // I need to think about feasibility
const a2 = useStore((state) => state.a * 2) // this isn't very good in performance, prefer computeds |
Looks very promising! Would it be possible for sum2 & sum to be in the same computeds definition/call? And would it enable fine-grained tracking of computeds dependencies to only re-generate if dependencies change? I agree complete type inference is a huge plus. |
I don't think type inference works with the same call. Using string keys, it should be fine-grained. On second thought, returning a hook directly with the "chain" can be a bit troublesome. import { createStore, useStoreValue, useStoreAction } from 'jotai-zustand';
const { values, actions } = createStore({ a: 1, b: 2 })
.computeds({
sum: (state) => state.a + state.b,
})
.computeds({
sum2: (state) => state.sum * 2,
})
.actions({
addA: (n: number) => (state) => ({ ...state, a: state.a + n }),
});
// usage
const a = useStoreValue(values.a)
const sum2 = useStoreValue(values.sum2)
const addA = useStoreAction(actions.addA) |
I think it's solves the problems (type inferred, supports computeds, direct subscriptions, central store). But I do think it would at least seem less complex if the usage and definition APIs were a bit flatter, like in Zustand: // definition
const store = createStore({ a: 1, b: 2 })
.computeds({ // could these be part of the state definition? (function in state = computed)
sum: (state) => state.a + state.b,
})
.computeds({
sum2: (state) => state.sum * 2,
})
.actions({ // could .sum2 and other actions (.addB()) be available in the state here?
addA: (n: number) => (state) => ({ ...state, a: state.sum2 + n }),
})
// usage
const a = useStore(store.a)
const sum2 = useStore(store.sum2)
const addA = useStore(store.addA) I guess you could also use this fluid syntax to create sub-stores that could depend on the upstream/parent stores. |
I thought it's impossible to infer types, but as we don't use createState function like Zustand, I'm not sure. We need to try it.
It's available for read, but write should be prohibited because otherwise, it breaks computeds. // if type inference works:
const store = createStore({
a: 1,
b: 2,
sum: (state) => state.a + state.b,
addA: (n: number) => (state) => ({ a: state.sum + n })
});
// now the difficulty is sum and addA are both functions. So, even if we can solve with JS, it's not TS friendly. I think original approach might be better: const store = createStore(
// values
{
a: 1,
b: 2,
sum: (state) => state.a + state.b,
},
// actions
{
addA: (n: number) => (state) => ({ a: state.sum + n })
},
); |
This does seem to work to some extent (see gist) - but it relies on proxies etc - maybe not very Zustand-ish (more Valtio-ish) :-/ And doesn't support selector access, and probably doesn't handle deeper structures. // full impl https://gist.github.com/beorn/2726d3d3ffe661bb1c3666e2da51c787
// Create the store
const store = createStore({
a: 1,
b: 2,
get sum() {
return this.a + this.b
},
get sum2x() {
return this.sum * 2
},
addA(n: number) {
this.a = this.a + this.sum + n
},
})
// In a React component
export default function MyComponent() {
const a = useStoreValue(store.a)
const b = useStoreValue(store.b)
const sum = useStoreValue(store.sum)
const sum2x = useStoreValue(store.sum2x)
const addA = useStoreValue(store.addA)
return (
<div>
<h1>Brainstorming 9</h1>
<div>a: {a}</div>
<div>b: {b}</div>
<div>sum: {sum}</div>
<div>sum2x: {sum2x}</div>
<button onClick={() => addA(5)}>Add to A</button>
</div>
)
}``` |
Yeah, we should avoid Proxies in this project. |
btw, your last example resembles with https://github.com/zustandjs/zustand-valtio |
If we can use getter and import { createStore, useStore } from 'jotai-zustand';
const store = createStore({
a: 1,
b: 2,
get sum() {
return this.a + this.b
},
get sum2x() {
return this.sum * 2
},
addA: (n: number) => (state) => ({ a: state.sum + n }), // not sure if `state` type can be inferred
})
// In a React component
export default function MyComponent() {
const a = useStore(store.a)
const sum2x = useStore(store.sum2x)
const addA = useStore(store.addA)
return (
<div>
<div>a: {a}</div>
<div>sum2x: {sum2x}</div>
<button onClick={() => addA(5)}>Add to A</button>
</div>
)
}``` |
Or, addA(n: number) {
return { a: this.sum + n };
} But, I have never seen this style before. |
Ah, yes, looks a lot like zustand-valtio :) I agree it'd be best to avoid magic like proxies. I don't think state can be inferred if you do function MyComponent() {
const a = useStoreValue(store.a)
const b = useStoreValue(store.b)
const sum = useStoreValue(store.sum)
const sum2x = useStoreValue(store.sum2x)
const addA = useStoreValue(store.addA)
return (
<div>
<h1>Brainstorming 11</h1>
<div>a: {a}</div>
<div>b: {b}</div>
<div>sum: {sum}</div>
<div>sum2x: {sum2x}</div>
<button onClick={() => addA(5)}>Add to A</button>
</div>
)
}
const store = createStore({
a: 1,
b: 2,
addA(n: number) {
return { a: this.a + n }
},
get sum(): number {
return this.a + this.b
},
get sum2x(): number {
return this.sum * 2
},
}) See full gist. It doesn't use proxies, but it does track dependencies through property access. |
I still wonder if relying on |
Apologies, yes, should be useStoreValue on the action too. I updated the gist and the code above. And, yes, using This at least works: type StoreShape = {
a: number
b: number
addA(n: number): Partial<StoreShape>
sum: { get: () => number }
sum2x: { get: () => number }
}
export const store = createStore<StoreShape>({
a: 1,
b: 2,
addA: (n: number) => (state) => ({ a: state.a + n }),
sum: { get: () => (state) => state.a + state.b },
sum2x: { get: () => (state) => state.sum * 2 },
})
// Component should now have correct types
export default function MyComponent() {
const a = useStoreValue(store.a) // number
const b = useStoreValue(store.b) // number
const sum = useStoreValue(store.sum) // number
const sum2x = useStoreValue(store.sum2x) // number
const addA = useStoreValue(store.addA) // (n: number) => void
return (
<div>
<h1>Brainstorming 12</h1>
<div>a: {a}</div>
<div>b: {b}</div>
<div>sum: {sum}</div>
<div>sum2x: {sum2x}</div>
<button onClick={() => addA(5)}>Add to A</button>
</div>
)
} |
My second goal is to provide a solution for people who think Zustand is more scalable (or suitable for large scale apps) than Jotai. So, the central store syntax would be important. import { createStore, useStoreValue, useStoreAction } from 'jotai-zustand';
const store = createStore({
a: 1,
b: 2,
get sum() { return this.a + this.b },
get sum2() { return this.sum * 2 },
addA: (n: number) => (state) => ({ a: state.a + n }),
});
const sum2 = useStoreValue(store.sum2);
const addA = useStoreAction(store.addA); All first level properties become atoms, so it's direct subscription to them. No nesting are supported.
If that's important. We should make |
I think that would be nice, and fits with the bringing Zustand flavor to Jotai while still remaining atom based. Is there a difference between this and a factory method that creates a set of atoms? Is it in essence just a wrapper that give you a Zustand-like syntax to create the below: import { Atom, atom, useAtom, WritableAtom, PrimitiveAtom } from "jotai"
// /** Store */
type StoreShape = {
a: PrimitiveAtom<number>
b: PrimitiveAtom<number>
sum: Atom<number>
sum2x: Atom<number>
addA: WritableAtom<null, [number], void>
}
export const store: StoreShape = {
a: atom(1),
b: atom(2),
sum: atom((get) => get(store.a) + get(store.b)),
sum2x: atom((get) => get(store.sum) * 2),
addA: atom(null, (get, set, update) => set(store.a, get(store.a) + update)),
}
// Component should now have correct types
export default function MyComponent() {
const [a] = useAtom(store.a) // number
const [b] = useAtom(store.b) // number
const [sum] = useAtom(store.sum) // number
const [sum2x] = useAtom(store.sum2x) // number
const [, addA] = useAtom(store.addA) // (n: number) => void
return (
<div>
<h1>Brainstorming 13</h1>
<div>a: {a}</div>
<div>b: {b}</div>
<div>sum: {sum}</div>
<div>sum2x: {sum2x}</div>
<button onClick={() => addA(5)}>Add to A</button>
</div>
)
} |
Yeah, that's exactly what I was thinking. For - addA: (n: number) => (state) => ({ a: state.a + n }),
+ addA: (n: number) => ({ a: (prev) => prev + n }), Or, maybe it doesn't matter much. I wonder what would be a Zustand flavored api. |
Ok, I think it's cool. :) Note that to differentiate between getters at the type level, you need a bit of hackery, but it's okay; you can rely on the fact that getters create readonly properties, and those are different than read-write properties — see IsEqual. I'm also thinking that it may make sense to just treat all non-functions (non-actions = computeds or state) the same in terms of instrumenting for dependency tracking, so that you could have state that has both setters and getters. The only difference between computeds and state is that computeds only has getters, and therefore is readonly. |
I'm not entirely sure if I get it.
Would you like to implement it and send a PR? Or, do you want me or someone to work on it? |
I can take a stab at a PR, but I'd like to do a bit more analysis / design clarification first... :) GoalsBenefit from central Zustand-ish store definition syntax, while keeping Jotai atom benefits such as direct subscriptions (no selectors) and derived values. Store atomJust trying to get the nomenclature clear. I don't think end-users have to necessarily see all of these terms. Previously we've been saying store, and while I think we're clear it is meant in a Zustand store type of way, I realize in Jotai store already means a storage container for atoms, so it's going to potentially be confusing to just say store. Instead we could call it StoreAtom, which would be different from the atom store. (Alternatively Jotai's store could be renamed storage, which is also a common way to name such a thing, but it'd be a breaking change.) Definition and propertiesThe StoreAtom defines a State using an object (StoreAtomDefinition) which uses the syntax we have been talking about above; using getters and potentially
For example: import { createStoreAtom } from 'jotai-zustand';
const storeAtom = createStoreAtom({
// state
a: 1,
b: 2,
// computeds
get sum() { return this.a + this.b; },
get sum2() { return this.sum * 2; },
// actions
addA(n: number) { return { a: this.a + n }}
// addA(n: number) { this.a += n; }
// -- Alternatives that are not type inferable:
// addA: (n: number) => (state) => ({ a: state.a + n }),
// addA: (n: number) => ({ a: (prev) => prev + n }),
}); Realization into atomsThe // approximate realization
storeAtom = {
a: atom(1),
b: atom(2),
sum: atom((get) => get(storeAtom.a) + get(storeAtom.b)),
sum2x: atom((get) => get(storeAtom.sum) * 2),
addA: atom(null, (get, set, update) => set(store.a, get(storeAtom.a) + update)),
}; UsageThese store atoms are just Jotai atoms that can be used as normal: import { useAtom, useAtomValue, useSetAtom } from 'jotai';
const a = useAtomValue(store.a); // number
const b = useAtomValue(store.b); // number
const sum = useAtomValue(store.sum); // number
const sum2x = useAtomValue(store.sum2x); // number
const addA = useSetAtom(store.addA); // (n: number) => void RecomputationComputeds are recomputed when state it depends on is updated. Cyclic dependencies will throw an Error. Options
|
Sure.
Sounds good!
As the new function returns a set of atoms, it should be called "storeAtoms".
As we are using
For now, it will be infinite loops, which errors.
I think that's necessary to cover some edge cases. We need to ideate the api.
I'd like to avoid it unless there's a huge demand. Hm, but on the second thought, it's actually very cute. On the third thought, it requires Proxies, so let's not do that.
I'd like to avoid it. It sounds mismatching with "store" model.
This would be too complicated. It depends on a use case. Can be a separate util?
Async would be really nice. But, maybe not for the initial release.
Not sure if I understand how it looks like. Isn't it like "Allow using atoms within atom store definition"? |
Here's an updated spec/README — I'm trying out some nomenclature (atomic store, computeds => derived state) to see if it makes it easier/simpler. It struck me that it is presumably possible to make this atomic store completely Zustand compatible, and it would probably be possible to wrap a Zustand store to make it an atomic store — it wouldn't have derived state, but you could add that if you wanted to. Atomic StoreAn atomic store is a type inferred central store defined using a
The store exposes each of the properties as an appropriate Jotai atom which you can then consume/use to interact with the state in the store. This way you can benefit from both the conciseness and simplicity of a central Zustand-ish store definition syntax, and the Jotai atom benefits such as cached, auto-updating derived values, and direct subscriptions that doesn't require selectors. Definitionimport { create } from 'jotai-zustand'
const atomicStore = create({
a: 1,
b: 2,
// derived state defined using getters
get sum() { return this.a + this.b },
get sum2() { return this.sum * 2 },
// actions return Partial<State> or mutate state directly
adda(n: number) { return { a: this.a + n } },
addb(n: number) { this.b += n },
});
// => {
// a: PrimitiveAtom<number>
// b: PrimitiveAtom<number>
// sum: Atom<number>
// sum2: Atom<number>
// adda: WritableAtom<null, [number], void>
// addb: WritableAtom<null, [number], void>
// }; All method properties on the state object are considered to be actions, and they must either mutate the state in the Derived state (aka computeds or computed values) are defined using getters, and are automatically updated when the state they depend on changes. Be careful not to create circular dependencies. UsageThe store can be consumed as a set of atoms: import { useAtom, useAtomValue, useSetAtom } from 'jotai'
export default function MyComponent() {
const a = useAtomValue(atomicStore.a) // number
const sum2x = useAtomValue(atomicStore.sum2) // number
const adda = useSetAtom(atomicStore.adda) // (n: number) => void
return (
<div>
<div>a: {a}</div>
<div>sum2x: {sum2x}</div>
<button onClick={() => adda(5)}>Add 5 to a</button>
</div>
)
} Or through import { useStore } from 'jotai-zustand'
const sum = useStore(atomicStore, (state) => state.sum)
const state = useStore(atomicStore) Using selectors is not quite as performant as using atoms. Each Component instances that use atoms has no performance penalty unless the atom they depend on changes value. Craz idea: GeneralizationThe state definition object above could actually connect to and bridge to other state systems, e.g., import { fromZustand, fromSignal, type State } from 'jotai-zustand'
const store = create({
zustand: fromZustand(zustandStore), // composable
signal: fromSignal(signal$), // maybe auto-detect type
a: 1,
b: 2,
get sum() { return this.zustand.var + this.signal }
})
// => State<{
// zustand: State<...zustandStore>,
// signal: number,
// a: number,
// b: number,
// sum: readonly number
// }>
fromAtomic(store, { // extensible
get sum2() { return this.sum * 2 }
})
// => State<{
// zustand: State<...zustandStore>,
// signal: number,
// a: number,
// b: number,
// sum: number,
// sum2: number
// }>
toSignals(store)
// => {
// zustand: { var: Signal<number> },
// a: Signal<number>,
// b: Signal<number>,
// signal: Signal<number>,
// sum: Signal<number>
// }
toAtoms(store)
// => {
// zustand: { var: atom<...> },
// signal: atom<number>,
// a: atom<number>,
// b: atom<number>,
// sum: atom<number>
// } To doMust explore:
Also likely explore:
Perhaps out of scope:
Out of scope:
|
You can check out https://github.com/beorn/jotai-zustand/tree/pr/atomic-store |
Nice naming.
I don't prefer the direct mutation. I don't think we should allow
It's an interesting idea, but it's out of the scope of the current goal. And, it should be a separate library than |
Actually, On the other hand, supporting const EMPTY = Symbol();
const valueAtom = atom('str')
const actionAtom = atom(EMPTY, (get, set, arg) => ...);
const useValueOrActionAtom = (atom) => {
const [value, action] = useAtom(atom);
return value === EMPTY ? action : value;
}; |
I’ll reply more tomorrow - but just a quick note before signing off today: while not using Proxy, the code does wrap each property in a getter/setter. Not sure how you feel about that?
|
I'm still against to it. If Zustand's mental model & design policy is "immutable state", and no mutation style. That's another reason. |
- import { useStore } from 'jotai-zustand'
+ import { useAtomicStore } from 'jotai-zustand' might be considerable. |
Okay, this seems clear:
Questions:
|
https://github.com/beorn/jotai-zustand/tree/pr/atomic-store
|
Actually, we may want to separate it into two hooks. I mean, even for the basic state, it should be read-only.
I'm a bit confused with the context. Is it about
Is it about actions? Partly my concern was the coding, but partly it's api design. I don't think mutation style fits the mental model. Definitionimport { createAtomicStore } from 'jotai-zustand'
const atomicStore = createAtomicStore({
a: 1,
b: 2,
// derived state defined using getters
get sum() { return this.a + this.b },
get sum2() { return this.sum * 2 },
// actions return Partial<State>, can use `this` for reading current state
setA(n: number) { return { a: n } },
addB(n: number) { return { b: this.b + n } },
});
// => {
// a: PrimitiveAtom<number>
// b: PrimitiveAtom<number>
// sum: Atom<number>
// sum2: Atom<number>
// setA: WritableAtom<unique symbol, [number], void>
// addB: WritableAtom<unique symbol, [number], void>
// }; |
Mutations: I was thinking in terms of allowing Definition: Looks good to me! Would it be feasible, and would it make for a better DX, to have another subtype of Action? E.g., Consumption: Okay, so we have these options:
Prototype: When you have some time you can look at https://github.com/beorn/jotai-zustand/tree/pr/atomic-store to see if it's going in the right direction. I haven't implemented the custom hooks or Zustand selector or vanilla store API, but a lot of other stuff is in place. It could probably be refactored and simplified even further, though. (I can also submit a PR, but I think it's a bit exploratory/prototype-y for that.) |
Yeah, I agree.
What does it look like?
👍
👍
That's fine. I think types can tell.
Apart from implementation, for API design, it depends on how much we encourage this usage. It is required for using props, but otherwise if possible, we should encourage getters (for derived atoms) for better performance. If we rather discourage or limit the selector usage, I think a separate hook such as
We can't do that as atoms are just definitions. What we could do is to pass the Jotai store. import { createAtomicStore } from 'jotai-zustand';
import { createStore } from 'jotai';
const atomicStore = createAtomicStore(...);
const state = atomicStore.getState(store);
atomicStore.setState(store, { a: 3 });
atomicStore.setState(store, (state) => ({ a: state.a + 2 }));
I'm fine to have a draft PR from your branch saying it's a prototype for now. |
Actually, let's make it options: import { createAtomicStore } from 'jotai-zustand';
import { createStore } from 'jotai';
const atomicStore = createAtomicStore(...);
const state = atomicStore.getState({ store });
atomicStore.setState({ a: 3 }, { store });
atomicStore.setState((state) => ({ a: state.a + 2 }), { store }); And, // These use getDefaultStore
const state = atomicStore.getState();
atomicStore.setState({ a: 3 });
atomicStore.setState((state) => ({ a: state.a + 2 })); |
It's split into several commits and hard to comment. Can you open a draft PR? |
Ok, just created a PR :) Perhaps also track actions/plans as tasks in an issue? We could do it in this issue or a new one. |
Btw, I've tried using an atomic store for my project and I have a somewhat complex use case that I'm wondering how we could support: I have a bunch of 'items' that I keep in a map, indexed by id. I also have a list of Some thoughts on how to do this with respect to atomic store:
{
#selectedIds: [] as string[],
get selectedIds() {
return this.#selectedIds
},
set selectedIds(newIds: string[]) {
// has access to this.#selectedIds and newIds
return { selectedIds }
},
} |
Thanks. I'll look into it.
As you like. Do this in this issue or a new one, or a check list in the PR description. |
It sounds like I'd use atom-in-atom if it's pure Jotai.
I think it's still the same as it's a single-store approach. so, I don't think setters can't return values, but '#' prefix for private atoms might be an idea. set selectedIds(newIds: string[]) {
console.log('prevIds =', this.#selectedIds);
this({ #selectedIds: newIds }); // this should be technically possible, but I'm not sure if it's acceptable DX-wise.
}, |
Yup.
I am just not sure how to enable direct subscriptions to each item.
Duh! You're right. Also, |
Okay, pushed a version of the PR that responds to your comments. |
If it can be a Jotai solution, |
Hi,
I'm commenting a bit on the Zustand discussion boards (pmndrs/zustand#2830), trying to find a way to get great performance with many components listening to one Zustand store. I'm trying to not only reduce re-renders, but also reducing the number of selector calls that will have to be done for each component, i.e., to have components subscribe on a more granular basis to parts of the state. A solution is of course to use atoms/signals instead, but I like the more centralized model of a Zustand store, so I've been looking for ways to do it without rewriting everything in Jotai or resorting to convoluted workarounds.
Jotai-zustand could of course be a good solution, and I can use
atomWithStore
and create derived atoms and thenuseAtom
to create a more direct subscription to that atom's value (saving a lot of work done by selector calls).But then looking at your code base I came across a few interesting functions that are exported, and that could hint at some other solution, namely
create
andatomWithActions
— these seem to create a Zustand-like useStore interface backed by an atom, is that right?I thought perhaps I could create a hybrid/mutant Zustand store that directly supports deriving atoms from it (or within it) — and while the atom/store they create seems to have a
useStore
type interface, it doesn't have thegetStore
/setStore
of the vanilla stores, and it doesn't expose the underlying atoms?What is the purpose of these, and do you have any thoughts on how to achieve the performance of Jotai Atoms (avoid selector calls) while keeping the programming model of Zustand?
The text was updated successfully, but these errors were encountered: