From 76d734d4ab533fd4797ddf6bfe845e833b5dffc7 Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Wed, 23 Jul 2025 17:11:30 -0400 Subject: [PATCH 01/49] docs: document Signal class for v2 --- docs/docs/v2/api/classes/Signal.mdx | 476 ++++++++++++++++++++++++++ docs/sidebars.js | 9 + docs/src/all.tsx | 4 +- docs/src/components/Legend/Legend.tsx | 10 +- docs/src/theme/CodeBlock/Sandbox.tsx | 14 + 5 files changed, 509 insertions(+), 4 deletions(-) create mode 100644 docs/docs/v2/api/classes/Signal.mdx diff --git a/docs/docs/v2/api/classes/Signal.mdx b/docs/docs/v2/api/classes/Signal.mdx new file mode 100644 index 00000000..a06188fd --- /dev/null +++ b/docs/docs/v2/api/classes/Signal.mdx @@ -0,0 +1,476 @@ +--- +id: Signal +title: Signal +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +The object returned by [`injectSignal`](/not-done?path=../injectors/injectSignal). Signals are reactive state containers. Each signal holds a value and provides methods for accessing and updating that value. In Zedux, the term "signal" refers to an instance of this class. + +Atoms themselves are signals. That simply means the [AtomInstance](/not-done?path=./AtomInstance) class extend this class. An atom can also contain many inner signals. When a signal is returned from an [atom state factory](/not-done?path=../factories/atom#valueorfactory), the atom becomes a thin wrapper around the returned signal. + +[Mapped signals](/not-done?path=./MappedSignal) are also signals themselves. + +As of v2, signals have replaced stores as the primary state container. + +## Creation + +You never instantiate this class yourself. There are two primary ways to create normal Signals: + +### `injectSignal` + +The most common way to create a signal inside an atom. + +```ts +const counterAtom = atom('counter', () => { + const signal = injectSignal(0) + + return signal // make this signal control this atom's entire state +}) +``` + +See [`injectSignal`](/not-done?path=../injectors/injectSignal). + +### `ecosystem.signal` + +For dynamically creating signals anywhere. + +```ts +const counterSignal = myEcosystem.signal(0) +``` + +Unlike `injectSignal`, `ecosystem.signal` can be used in loops and if statements. It's useful for creating dynamic lists of signals e.g. in response to state changes. The tradeoff is that you have to manage the signal's lifecycle manually. + +See [`Ecosystem#signal`](/not-done?path=./Ecosystem#signal) + +## Destruction + +Signals are always destroyed when they have no more observers. When created via [`injectSignal`](/not-done?path=../injectors/injectSignal), this typically means the signal will be destroyed when the injecting atom is destroyed. When created via [`ecosystem.signal`](/not-done?path=./Ecosystem#signal), you must either manually add observers or destroy the signal when you're done with it. + +```ts +const exampleAtom = atom('example', () => { + const signal = injectSignal('example state') + + return signal +}) + +const exampleNode = ecosystem.getNode(exampleAtom) +exampleNode.destroy() // also destroys the injected signal +``` + +## Set/Get + +At the most basic level, signals use simple set/get: + +```ts +const signal = injectSignal(0) + +// in a callback or effect: +signal.get() // 0 +signal.set(1) +signal.get() // 1 +signal.set(state => state + 1) // function overload +``` + +When setting a signal's state via `.set`, the passed state will completely replace the current state. This means that nested objects must be spread manually: + +```ts +const signal = injectSignal({ foo: { bar: [1, 2, 3] } }) + +signal.set(state => ({ + ...state, + foo: { + ...state.foo, + bar: state.foo.bar.filter(num => num % 2), + }, +})) +``` + +This can be tedious. For these operations, signals have a better way: + +## Mutation + +Besides basic sets and gets, signals have one extra superpower: [`signal.mutate`](#mutate). This method introduces immer-style proxy-based mutations to Zedux with an opt-in API - just stick to [`signal.set`](#set) if you don't like immer-style state updates. + +```tsx live ecosystemId=signal-mutation-example resultVar=result version=2 +const ecosystem = createEcosystem() +const signal = ecosystem.signal({ foo: 1, bar: { baz: [1, 3, 5] } }) + +signal.mutate(draft => { + draft.foo = 2 + draft.bar.baz.splice(1, 1) // delete the `3` from the `bar.baz` array +}) + +const result = signal.get() +``` + +## Generics + +For TypeScript users, signals have the following generics inherited from [`ZeduxNode`](/not-done?path=./ZeduxNode#generics): + + + + See [`ZeduxNode<{ Events }>`](/not-done?path=./ZeduxNode#gevents). + + + See [`ZeduxNode<{ Params }>`](/not-done?path=./ZeduxNode#gparams). + + + See [`ZeduxNode<{ State }>`](/not-done?path=./ZeduxNode#gstate). + + + See [`ZeduxNode<{ Template }>`](/not-done?path=./ZeduxNode#gtemplate). + + + +## Events + +Signals double as event emitters. Attach event listeners with `signal.on`. Call the returned cleanup function to remove the listener. + +```tsx live ecosystemId=signal-events-example resultVar=Alerter version=2 +const textInputAtom = atom('textInput', () => { + const signal = injectSignal('show no alert') + + injectEffect(() => { + const cleanup = signal.on('change', event => { + if (event.newState === 'show alert') { + alert('you called?') + } + }) + + return () => cleanup() + }, []) + + return signal +}) + +function Alerter() { + const [state, setState] = useAtomState(textInputAtom) + + return ( +
+ Change to say "show alert" + setState(event.target.value)} value={state} /> +
+ ) +} +``` + +See below for the full list of [built-in events](#built-in-events). + +### Custom Events + +Built-in events like `change` are fired automatically. You can also specify custom events that your signal accepts via the `events` config option. Use the [`As` util](/not-done?path=../utils/As) to type event payloads: + +```ts +const greetingSignal = ecosystem.signal(null, { + events: { + hello: As, + goodbye: As, + }, +}) +``` + +You can send custom events by themselves via `.send`: + +```ts +// using the above `greetingSignal`: +greetingSignal.send('hello', 'friend!') +greetingSignal.send('goodbye', 2) // Type Error: Payload must be a string + +// object form accepts multiple events: +greetingSignal.send({ hello: 'friend!', goodbye: 'friend :(' }) +``` + +It's very common to send events alongside a state update. To do this, pass an events object as the second parameter to `signal.set` or `signal.mutate`. + +This can be useful when you want to tell consumers exactly what changed e.g. to optimize derivation operations. It can also be used to "document" your state updates, improving DX. + +```ts +// this `todo` event will be used to give consumers more information about the +// state change: +const todosSignal = ecosystem.signal([]) + +todosSignal.set(state => [...state, todo], { todo }) +todosSignal.set(state => state.filter(({ id }) => id !== todo.id), { todo }) +todosSignal.mutate( + state => { + state.push(todo) + }, + { todo } +) +``` + +### Built-In Events + +Unlike custom events, built-in events cannot be sent manually via `.send`, `.set`, or `.mutate`. You can only listen to them via `.on`. These events are intrinsic to Zedux itself. + +All signals have the following built-in events: + + + + Sent whenever [`signal.mutate`](#mutate) is called. + + The payload is an array of [transactions](/not-done?path=../types/Transaction) efficiently documenting the changes made to the signal's state by the `mutate` call. + + ```tsx live noProvide=true resultVar=result version=2 + const ecosystem = createEcosystem() + const signal = ecosystem.signal({ + todos: [] + }) + + let result: Transaction[] = [] + + signal.on('mutate', transactions => { + result = transactions + }) + + signal.mutate(draft => { + draft.todos.push({ id: 1, title: 'Master Mutations' }) + }) + ``` + + + + +All signals also inherit the following built-in events from [`ZeduxNode`](/not-done?path=./ZeduxNode#events): + + + + See the [node `change` event](/not-done?path=./ZeduxNode#change-event). + + + See the [node `cycle` event](/not-done?path=./ZeduxNode#cycle-event). + + + +## Properties + +Signals inherit the following **readonly** properties from [`ZeduxNode`](/not-done?path=./ZeduxNode#properties): + + + See [`ZeduxNode#id`](/not-done?path=./ZeduxNode#id). + + See [`ZeduxNode#params`](/not-done?path=./ZeduxNode#params). This will + always be undefined - normal signals don't take params. + + + See [`ZeduxNode#status`](/not-done?path=./ZeduxNode#status). This will never + be "Stale" - normal signals skip from Active to Destroyed. + + + See [`ZeduxNode#template`](/not-done?path=./ZeduxNode#template). This will + always be undefined - normal signals don't have templates. + + + +## Methods + +Signals have the following unique methods: + + + + Creates a proxy that can be mutated immer-style to change the signal's state. Zedux tracks the mutations made to this proxy and immutably updates the signal's state accordingly. + + This is recursive! Zedux will lazily create proxies as needed when you access nested properties. + + Every `mutate` call also causes the signal to fire both a [`change` event](#change-event) and a [`mutate` event](#mutate-event) with an array of [transactions](/not-done?path=../types/Transaction) efficiently documenting the changes made. + + :::important + `mutate` can only be called on signals whose state is a JavaScript object, array, or Set. + ::: + +```tsx live ecosystemId=signal-mutate-example resultVar=Counter version=2 +const counterAtom = atom('counter', () => ({ count: 0 })) + +function Counter() { + // atoms are signals, remember? + const signal = useAtomInstance(counterAtom) + const { count } = useAtomValue(signal) + + return ( +
+
count: {count}
+ + +
+ ) +} +``` + + #### Object Shorthand + + `mutate` also has an object overload. This is a convenient shorthand syntax. + + ```ts + signal.mutate({ foo: 'bar' }) + // is equivalent to: + signal.mutate(draft => { + draft.foo = 'bar' + }) + ``` + + You can also use this shorthand form when passing a function. Just don't perform any mutations on the `draft` object and return the shorthand object instead. Zedux will assume you're asking it to apply the returned list of mutations for you: + + ```ts + signal.mutate(draft => ({ count: draft.count + 1 })) + // + ``` + + While convenient, these overloads have some quirks: + + - They only make sense for normal objects. Arrays and sets are not supported. + - You can't remove properties or set them to `undefined`: + + ```ts + // does nothing - Zedux sees `undefined` and skips this value: + signal.mutate({ token: undefined }) + + signal.mutate(draft => { + draft.token = undefined // works! + delete draft.token // also works! + }) + ``` + + - You can't change object references directly: + + ```ts + // does nothing - Zedux recurses into the object and finds no changes: + signal.mutate({ postsById: {} }) + + signal.mutate(draft => { + draft.postsById = {} // works! Sets this property to an empty object. + }) + ``` + + #### Limitations + + `mutate` currently only knows how to proxy JavaScript objects, arrays, and Sets. We will probably add support for Maps in the future, but note that you typically don't want to use Maps for immutable state since cloning them is [extremely slow](https://jsbench.me/4ym0jt8pbh). + + Additionally, Zedux only proxies operations that directly operate on the object, array, or Set. Operations that involve searching or iterating over the object and then mutating the result will not be captured. + + We will add support for common use cases like `array.find()` in the future, but some things will likely never be supported since we want to keep Zedux's mutation operations lightning fast. + +```ts +const signal = ecosystem.signal([ + { text: 'save the galaxy' }, + { text: 'use the force' }, +]) + +signal.mutate(draft => { + draft[0].text = 'I am your father' // works + + // bug! Zedux doesn't proxy the `find` result, so it misses this mutation: + draft.find(({ text }) => text.includes('force')).text = "That's impossible!" + + // instead, do: + const index = draft.findIndex(({ text }) => text.includes('force')) + draft[index].text = "That's totally possible!" +}) +``` + + Signature: + + ```ts + mutate = (mutatable, events?) => void + ``` + + + + A function that accepts a "draft" object and can perform mutation operations on it. If any mutations are performed, Zedux ignores the return value. + + If an object is passed, Zedux will recursively iterate through it and set all keys to their values, essentially performing mutations for you. + + If a function is passed, no mutations are performed during the function execution, and an object is returned, Zedux will recursively iterate through the returned object and perform the mutations for you. + + + An optional object of [custom events](#custom-events) to emit alongside the mutation. + + + +
+ + Completely replaces the signal's state with the passed value. Does nothing if the passed state exactly matches the signal's current state. + + Also accepts a function overload that will receive the current state and should return the new state. + + ```ts + const signal = ecosystem.signal(0) + + signal.set(1) + signal.set(state => state + 1) + signal.get() // 2 + ``` + + Fires the built-in [`change` event](#change-event). + + Signature: + + ```ts + set = (settable, events?) => void + ``` + + + + Either the new state or a function that accepts the current state and returns the new state. + + + An optional object of [custom events](#custom-events) to emit alongside the mutation. + + + + + + Sends one or more [custom events](#custom-events) to the signal's event listeners. + + Signature: + + ```ts + send = (eventNameOrMap, payload?) => void + ``` + + + + A string or object. + + If a string is passed, it is the name of the event to send. With this overload, the event's payload should be passed as the second argument. + + If an object is passed, each key is the name of an event to send, and the value is the event's payload. With this overload, the second argument is ignored. + + + Optional (required if the event requires it). The payload of the event. + + + + :::important + `send` can only be called on signals whose state is an object. Of course, it doesn't make sense to send anything else :smile: + ::: + + +
+ +Signals also inherit the following methods from [`ZeduxNode`](/not-done?path=./ZeduxNode#methods): + + + + See [`ZeduxNode#destroy`](/not-done?path=./ZeduxNode#destroy). + + See [`ZeduxNode#get`](/not-done?path=./ZeduxNode#get). + + See [`ZeduxNode#getOnce`](/not-done?path=./ZeduxNode#getonce). + + + See [`ZeduxNode#on`](/not-done?path=./ZeduxNode#on) and the above + documentation for [signal events](#events). + + + +## See Also + +- [The `MappedSignal` class](/not-done?path=./MappedSignal) +- [The `ZeduxNode` class](/not-done?path=./ZeduxNode) +- [The `AtomInstance` class](/not-done?path=./AtomInstance) diff --git a/docs/sidebars.js b/docs/sidebars.js index f00a0ce6..de6cc7e6 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -159,5 +159,14 @@ module.exports = { }, ], 'Migration Guides': ['migrations/v2'], + '🚧 v2 beta docs': { + API: [ + { + type: 'category', + label: 'Classes', + items: ['v2/api/classes/Signal'], + }, + ], + }, }, } diff --git a/docs/src/all.tsx b/docs/src/all.tsx index 40b72452..311c5e77 100644 --- a/docs/src/all.tsx +++ b/docs/src/all.tsx @@ -15,9 +15,9 @@ export const Tabs = ({ children }: { children: React.ReactElement[] }) => ( {children} ) -export const tab1 = (children: ReactNode) => ( +export const tab1 = (children: ReactNode, useTsx = false) => ( - {children} + {useTsx ? {children} : {children}} ) diff --git a/docs/src/components/Legend/Legend.tsx b/docs/src/components/Legend/Legend.tsx index cf1db62b..439f9250 100644 --- a/docs/src/components/Legend/Legend.tsx +++ b/docs/src/components/Legend/Legend.tsx @@ -8,6 +8,7 @@ const ItemDesc = styled.div` const ItemName = styled.div` padding: 1rem; + scroll-margin-top: calc(var(--ifm-navbar-height) + 0.5rem); @container (max-width: 600px) { &:not(:first-of-type) { @@ -37,9 +38,13 @@ const prefixContext = createContext('') export const Item = ({ children, name, -}: PropsWithChildren<{ name: string }>) => { + suffix = '', +}: PropsWithChildren<{ name: string; suffix?: string }>) => { const prefix = useContext(prefixContext) - const id = prefix + name.toLowerCase().replace(/[^a-z]/g, '') + const id = + prefix + + name.toLowerCase().replace(/[^a-z]/g, '') + + (suffix ? `-${suffix}` : '') return ( <> @@ -48,6 +53,7 @@ export const Item = ({ id={id} > {name === 'Returns' ? name : {name}} + {suffix && {suffix}} diff --git a/docs/src/theme/CodeBlock/Sandbox.tsx b/docs/src/theme/CodeBlock/Sandbox.tsx index a192e1e0..3821b702 100644 --- a/docs/src/theme/CodeBlock/Sandbox.tsx +++ b/docs/src/theme/CodeBlock/Sandbox.tsx @@ -60,6 +60,20 @@ const getScope = (version: string) => { ...RxJSOperators, ...(version === '1' ? Zedux_v1 : Zedux_v2), ...React, + exports: {}, + require: (path: string) => { + if (path === '@zedux/react') { + return ZeduxReact_v2 + } + + if (path === '@zedux/immer') { + return ZeduxImmer_v2 + } + + if (path === '@zedux/machines') { + return ZeduxMachines_v2 + } + }, window: typeof window === 'undefined' ? { addEventListener() {}, removeEventListener() {} } From 3b226d113e117a1f1978d52a2c29cd3ec66c6eb1 Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Wed, 23 Jul 2025 17:35:27 -0400 Subject: [PATCH 02/49] docs: document `injectSignal` injector for v2 --- docs/docs/v2/api/classes/Signal.mdx | 6 +- docs/docs/v2/api/injectors/injectSignal.mdx | 85 +++++++++++++++++++++ docs/sidebars.js | 5 ++ 3 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 docs/docs/v2/api/injectors/injectSignal.mdx diff --git a/docs/docs/v2/api/classes/Signal.mdx b/docs/docs/v2/api/classes/Signal.mdx index a06188fd..fa053bed 100644 --- a/docs/docs/v2/api/classes/Signal.mdx +++ b/docs/docs/v2/api/classes/Signal.mdx @@ -5,7 +5,7 @@ title: Signal import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' -The object returned by [`injectSignal`](/not-done?path=../injectors/injectSignal). Signals are reactive state containers. Each signal holds a value and provides methods for accessing and updating that value. In Zedux, the term "signal" refers to an instance of this class. +The object returned by [`injectSignal`](../injectors/injectSignal). Signals are reactive state containers. Each signal holds a value and provides methods for accessing and updating that value. In Zedux, the term "signal" refers to an instance of this class. Atoms themselves are signals. That simply means the [AtomInstance](/not-done?path=./AtomInstance) class extend this class. An atom can also contain many inner signals. When a signal is returned from an [atom state factory](/not-done?path=../factories/atom#valueorfactory), the atom becomes a thin wrapper around the returned signal. @@ -29,7 +29,7 @@ const counterAtom = atom('counter', () => { }) ``` -See [`injectSignal`](/not-done?path=../injectors/injectSignal). +See [`injectSignal`](../injectors/injectSignal). ### `ecosystem.signal` @@ -45,7 +45,7 @@ See [`Ecosystem#signal`](/not-done?path=./Ecosystem#signal) ## Destruction -Signals are always destroyed when they have no more observers. When created via [`injectSignal`](/not-done?path=../injectors/injectSignal), this typically means the signal will be destroyed when the injecting atom is destroyed. When created via [`ecosystem.signal`](/not-done?path=./Ecosystem#signal), you must either manually add observers or destroy the signal when you're done with it. +Signals are always destroyed when they have no more observers. When created via [`injectSignal`](../injectors/injectSignal), this typically means the signal will be destroyed when the injecting atom is destroyed. When created via [`ecosystem.signal`](/not-done?path=./Ecosystem#signal), you must either manually add observers or destroy the signal when you're done with it. ```ts const exampleAtom = atom('example', () => { diff --git a/docs/docs/v2/api/injectors/injectSignal.mdx b/docs/docs/v2/api/injectors/injectSignal.mdx new file mode 100644 index 00000000..30ede6eb --- /dev/null +++ b/docs/docs/v2/api/injectors/injectSignal.mdx @@ -0,0 +1,85 @@ +--- +id: injectSignal +title: injectSignal +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +An [injector](/not-done?path=../glossary#injector) that creates and returns a stable [`Signal`](../classes/Signal) instance. The reference will never change for the lifetime of the injecting atom. + +Registers a [dynamic graph dependency](/not-done?path=../glossary#dynamic-graph-dependency) on the injected signal. By default, every state change in the injected signal will trigger a reevaluation of the current atom. + +Unless you register other dependencies, the injecting atom will typically be the only observer of the injected signal. That means the signal will be destroyed when the injecting atom is destroyed. + +## Examples + +```ts +const simpleSignal = injectSignal(0) +const functionOverload = injectSignal(() => createExpensiveInitialState()) +const functionIsState = injectSignal(() => () => 'but why (prefer exports)') +const nonReactive = injectSignal('my state', { reactive: false }) +const withCustomEvents = injectSignal([] as Message[], { + events: { + message: As, + }, +}) +const passingGenerics = injectSignal([]) +``` + +## Signature + + + {tab1(`injectSignal = (initialState, config?) => Signal`)} + {tab2(`declare const injectSignal: < + State, + EventMap extends Record = {} + >( + state: (() => State) | State, + config?: InjectSignalConfig + ) => Signal<{ + Events: EventMap + State: State + }>`)} + + + + + Required. The initial state of the signal. Can be absolutely anything. + + If a function is passed, Zedux will call it to retrieve the initial state + of the signal. + + This value is only used on initial evaluation. It will be ignored on all + subsequent evaluations. + + + + Optional. An object with the following optional properties: + + ```ts + { events, reactive } + ``` + + + + An object mapping event names to [`As`](/not-done?path=../utils/As) where [`As`](/not-done?path=../utils/As) is Zedux's [`As`](/not-done?path=../utils/As) util and `MyType` is the payload type of the event. + + This payload must always be passed when the event is sent. Specify [`As`](/not-done?path=../utils/As) to allow no payload. + + + A boolean. Default: `true`. + + Pass `false` to prevent the injecting atom from registering a [dynamic graph dependency](/not-done?path=../glossary#dynamic-graph-dependency) on the injected signal. The atom will still register a [static graph dependency](/not-done?path=../glossary#static-graph-dependency) on the injected signal. + + + + + + A stable [`Signal`](../classes/Signal) instance. + + + +## See Also + +- [The `Signal` class](../classes/Signal) +- [`injectMappedSignal`](/not-done?path=./injectMappedSignal) diff --git a/docs/sidebars.js b/docs/sidebars.js index de6cc7e6..50617125 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -166,6 +166,11 @@ module.exports = { label: 'Classes', items: ['v2/api/classes/Signal'], }, + { + type: 'category', + label: 'Injectors', + items: ['v2/api/injectors/injectSignal'], + }, ], }, }, From 25f544204a6ecdfda44905b0246460a1833d61a9 Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Wed, 23 Jul 2025 18:00:02 -0400 Subject: [PATCH 03/49] docs: document `ZeduxNode` class for v2 --- docs/docs/v2/api/classes/Signal.mdx | 50 ++-- docs/docs/v2/api/classes/ZeduxNode.mdx | 342 +++++++++++++++++++++++++ docs/sidebars.js | 2 +- 3 files changed, 366 insertions(+), 28 deletions(-) create mode 100644 docs/docs/v2/api/classes/ZeduxNode.mdx diff --git a/docs/docs/v2/api/classes/Signal.mdx b/docs/docs/v2/api/classes/Signal.mdx index fa053bed..277e2c95 100644 --- a/docs/docs/v2/api/classes/Signal.mdx +++ b/docs/docs/v2/api/classes/Signal.mdx @@ -106,20 +106,20 @@ const result = signal.get() ## Generics -For TypeScript users, signals have the following generics inherited from [`ZeduxNode`](/not-done?path=./ZeduxNode#generics): +For TypeScript users, signals have the following generics inherited from [`ZeduxNode`](./ZeduxNode#generics): - See [`ZeduxNode<{ Events }>`](/not-done?path=./ZeduxNode#gevents). + See [`ZeduxNode<{ Events }>`](./ZeduxNode#gevents). - See [`ZeduxNode<{ Params }>`](/not-done?path=./ZeduxNode#gparams). + See [`ZeduxNode<{ Params }>`](./ZeduxNode#gparams). - See [`ZeduxNode<{ State }>`](/not-done?path=./ZeduxNode#gstate). + See [`ZeduxNode<{ State }>`](./ZeduxNode#gstate). - See [`ZeduxNode<{ Template }>`](/not-done?path=./ZeduxNode#gtemplate). + See [`ZeduxNode<{ Template }>`](./ZeduxNode#gtemplate). @@ -233,34 +233,34 @@ All signals have the following built-in events: -All signals also inherit the following built-in events from [`ZeduxNode`](/not-done?path=./ZeduxNode#events): +All signals also inherit the following built-in events from [`ZeduxNode`](./ZeduxNode#events): - See the [node `change` event](/not-done?path=./ZeduxNode#change-event). + See the [node `change` event](./ZeduxNode#change-event). - See the [node `cycle` event](/not-done?path=./ZeduxNode#cycle-event). + See the [node `cycle` event](./ZeduxNode#cycle-event). ## Properties -Signals inherit the following **readonly** properties from [`ZeduxNode`](/not-done?path=./ZeduxNode#properties): +Signals inherit the following **readonly** properties from [`ZeduxNode`](./ZeduxNode#properties): - See [`ZeduxNode#id`](/not-done?path=./ZeduxNode#id). + See [`ZeduxNode#id`](./ZeduxNode#id). - See [`ZeduxNode#params`](/not-done?path=./ZeduxNode#params). This will - always be undefined - normal signals don't take params. + See [`ZeduxNode#params`](./ZeduxNode#params). This will always be undefined + - normal signals don't take params. - See [`ZeduxNode#status`](/not-done?path=./ZeduxNode#status). This will never - be "Stale" - normal signals skip from Active to Destroyed. + See [`ZeduxNode#status`](./ZeduxNode#status). This will never be "Stale" - + normal signals skip from Active to Destroyed. - See [`ZeduxNode#template`](/not-done?path=./ZeduxNode#template). This will - always be undefined - normal signals don't have templates. + See [`ZeduxNode#template`](./ZeduxNode#template). This will always be + undefined - normal signals don't have templates. @@ -453,24 +453,20 @@ signal.mutate(draft => { -Signals also inherit the following methods from [`ZeduxNode`](/not-done?path=./ZeduxNode#methods): +Signals also inherit the following methods from [`ZeduxNode`](./ZeduxNode#methods): - - See [`ZeduxNode#destroy`](/not-done?path=./ZeduxNode#destroy). - - See [`ZeduxNode#get`](/not-done?path=./ZeduxNode#get). - - See [`ZeduxNode#getOnce`](/not-done?path=./ZeduxNode#getonce). - + See [`ZeduxNode#destroy`](./ZeduxNode#destroy). + See [`ZeduxNode#get`](./ZeduxNode#get). + See [`ZeduxNode#getOnce`](./ZeduxNode#getonce). - See [`ZeduxNode#on`](/not-done?path=./ZeduxNode#on) and the above - documentation for [signal events](#events). + See [`ZeduxNode#on`](./ZeduxNode#on) and the above documentation for [signal + events](#events). ## See Also - [The `MappedSignal` class](/not-done?path=./MappedSignal) -- [The `ZeduxNode` class](/not-done?path=./ZeduxNode) +- [The `ZeduxNode` class](./ZeduxNode) - [The `AtomInstance` class](/not-done?path=./AtomInstance) diff --git a/docs/docs/v2/api/classes/ZeduxNode.mdx b/docs/docs/v2/api/classes/ZeduxNode.mdx new file mode 100644 index 00000000..8278ec6b --- /dev/null +++ b/docs/docs/v2/api/classes/ZeduxNode.mdx @@ -0,0 +1,342 @@ +--- +id: ZeduxNode +title: ZeduxNode +--- + +import useBaseUrl from '@docusaurus/useBaseUrl' +import { Legend, Item, Link, Tabs, tab1, tab2 } from '@site/src/all' + +This is the base class for every [graph node](/not-done?path=../glossary#graph-node). The [AtomInstance class](/not-done?path=./AtomInstance), [SelectorInstance class](/not-done?path=./SelectorInstance), [Signal class](./Signal), and several other graph node types extend this class. + +You never create this class directly and should never need to reference it directly except as a type. + +## Generics + +For TypeScript users, this class holds a single type generic called the `NodeGenerics`. This type generic holds all the type information for a given node. Different node types will have different type information on this generic parameter. + +You can pull this generic information off any Zedux node by using various `*Of` type helpers. For example: + +```ts +const exampleAtom = atom('example', () => { + const signal = injectSignal('example state') + + return api(signal).setExports({ exampleExport: () => signal.get() }) +}) + +type ExampleEvents = EventsOf +type ExampleExports = ExportsOf +type ExampleNode = NodeOf +type ExampleParams = ParamsOf +type ExamplePromise = PromiseOf +type ExampleState = StateOf +type ExampleTemplate = TemplateOf +``` + +Full list of keys on the `NodeGenerics` (`G`) type generic: + + + + A Record type mapping event names to event payloads. These are the events + that this node emits. See the [built-in events](#events) below. Some node + types can also take custom events. + + + A recursive reference to the current node's full type. This allows some of + Zedux's recursive algorithms to keep full type information regardless how + deeply accessed they are. + + + A tuple type. The parameters of this node. Only exists on [atom + instances](/not-done?path=./AtomInstance) and [selector + instances](/not-done?path=./SelectorInstance). + + The state type of this node. Can be anything. + + A recursive reference to the current node's full template type. `undefined` + if none. Only [atom instances](/not-done?path=./AtomInstance) and [selector + instances](/not-done?path=./SelectorInstance) have templates. + + + +## Events + +A node's events can be listened to using [`node.on()`](#on). Every node has the following built-in events: + + + + Sent whenever the graph node's value changes. Event shape: + + ```ts + { newState, oldState, reasons?, source?, type } + ``` + + - `newState` - The new state of the graph node. Can be anything. + - `oldState` - The previous state of the graph node. Can be anything. + - `reasons` - An indefinitely-nested array of [reasons](/not-done?path=../types/EvaluationReason) why the state changed. This tracks the series of state changes through the dependency graph that led to the current node changing. If the node was updated directly, this will be undefined. + - `source` - The node that changed. Though the types don't reflect it yet, this will always be defined. + - `type` - The string `"change"`. + +```tsx live noProvide=true resultVar=App version=2 +const counterAtom = atom('counter', () => 0) +const timesTwoAtom = ion('timesTwo', ({ get }) => get(counterAtom) * 2) + +function App() { + const [count, setCount] = useAtomState(counterAtom) + const timesTwo = useAtomValue(timesTwoAtom) + const ecosystem = useEcosystem() + + useEffect(() => { + return ecosystem.on('change', (event, eventMap) => { + console.log('change', event, eventMap) + }) + }, []) + + return ( + <> +
Count: {count}
+
Times Two: {timesTwo}
+ + + ) +} +``` + +
+ + Sent when the node's [lifecycle status](#status) changes. Possible transitions: + + - `Active` -> `Stale` + - `Active` -> `Destroyed` + - `Stale` -> `Active` + - `Stale` -> `Destroyed` + + Event shape: + + ```ts + { oldStatus, newStatus, reasons?, source?, type } + ``` + + - `oldStatus` - A string. The previous status of the graph node. Refer to the above transitions list for possible values. + - `newStatus` - A string. The new status of the graph node. Refer to the above transitions list for possible values. + - `source` - The node that changed. Though the types don't reflect it yet, this will always be defined. + - `type` - The string `"cycle"`. + +```tsx live ecosystemId=ZeduxNode-cycle resultVar=App version=2 +const counterAtom = atom('counter', () => { + const node = injectSelf() + + injectEffect(() => { + const cleanup = node.on('cycle', (event, eventMap) => { + console.log('cycle', event, eventMap) + }) + + return cleanup + }, []) + + return 0 +}) + +function MaybeMounted() { + const [count, setCount] = useAtomState(counterAtom) + + return
Count: {count}
+} + +function App() { + const [isMounted, setIsMounted] = useState(true) + + return ( + <> + + {isMounted && } + + ) +} +``` + +
+
+ +## Properties + +Every node has the following **readonly** properties: + + + + A string. The unique id of this node. Zedux always tries to make this somewhat human-readable for easier debugging. + + For nodes that take params, the id is the combination of the node template's key and a deterministic stringification of the params. A node template's "key" can the string passed to the [`atom()`](/not-done?path=../factories/atom) factory for atoms or the selector function's name for selectors See [`ecosystem.hash()`](/not-done?path=./Ecosystem#hash) or [`node.params`](#params) for more details on how params are stringified. + + ```ts + ecosystem.getNode(atom('a', null)).id // 'a' + ecosystem.getNode( + atom('b', (param: string) => param), + ['c'] + ).id // 'b-["c"]' + ``` + + + + An array. The parameters passed to this node when it was created. + + A reference to the raw, unserialized params that were used to create this atom instance. Only [atom instances](/not-done?path=./AtomInstance) and [selector instances](/not-done?path=./SelectorInstance) take params. This will be `undefined` for node types that don't take params (like signals). For nodes that can take params but weren't passed any (like singleton atoms), this will be an empty array. + + ```ts + const instanceA = useAtomInstance(myAtom, ['param 1', 'param 2']) + const instanceB = useAtomInstance(myAtom, ['a', 'b']) + instanceA.params // ['param 1', 'param 2'] + instanceB.params // ['a', 'b'] + ``` + + All params must be serializable (no functions or symbols)! This is because + Zedux converts the params to a stable string representation in order to + efficiently check for an existing atom instance with the "same" params. + + Sameness is determined by deep value comparison, not reference equality. Order matters! + + ```ts + // These params are the "same" in Zedux's eyes: + useAtomInstance(myAtom, ['a', { b: 'b', c: 'c' }]) + useAtomInstance(myAtom, ['a', { c: 'c', b: 'b' }]) + + // But these are different: + useAtomInstance(myAtom, ['a', 'b']) + useAtomInstance(myAtom, ['b', 'a']) + ``` + + The only exception to the serializable rule is other atom instances. That's right! You can pass an atom instance to another atom instance. You can then use [`instance.get()`](#get) or [`ecosystem.get(instance)`](/not-done?path=./Ecosystem#get), [`injectAtomValue()`](/not-done?path=../injectors/injectAtomValue), or any other dynamic injector to register a [dynamic graph dependency](/not-done?path=../glossary#dynamic-graph-dependency) on the passed atom instance. + +```tsx live ecosystemId=AtomInstance-params resultVar=Shout version=2 +const normalAtom = atom( + 'normal', + () => 'row, row, row your boat gently lest I scream' +) +const shoutingAtom = atom( + 'shouting', + (instance: AnyAtomInstance<{ State: string }>) => { + const val = injectAtomValue(instance) // subscribe to updates + + return val.toUpperCase() + } +) + +function Shout() { + const instance = useAtomInstance(normalAtom) + const shout = useAtomValue(shoutingAtom, [instance]) // just pass the instance + + return
{shout}
+} +``` + +
+ + A string representing the status of the node. All nodes go through the following lifecycle: + + Initializing -> Active <-> Stale -> Destroyed + + This is mostly for debugging, but it has some practical uses. For example, you can check that `node.status !== 'Destroyed'` when holding a reference to a node outside of React/atoms (and if it is, update your local reference using [`ecosystem.getNode()`](/not-done?path=./Ecosystem#getnode)). + + + + A reference to the template this node was created from. `undefined` if none. Only [atom instances](/not-done?path=./AtomInstance) and [selector instances](/not-done?path=./SelectorInstance) have templates. + + For atom instances, this will be the [atom template](/not-done?path=./AtomTemplate). For selector instances, this will be the [selector template](/not-done?path=../types/SelectorTemplate). + + +
+ +## Methods + +Every node has the following methods: + + + + Destroys the node. Destruction will bail out by default if the node still has non-passive observers like event listeners. Pass `true` to force-destroy the node anyway. + + Signature: + + ```ts + destroy(force?) => void + ``` + + + + Optional. Default: `false`. If `true`, the node will be force-destroyed. + + + + See the [destruction walkthrough](/not-done?path=../../../walkthrough/destruction) for more information. + + + + Gets the current value of the node. Registers a [dynamic graph dependency](/not-done?path=../glossary#dynamic-graph-dependency) on the node when called in [reactive contexts](/not-done?path=../glossary#reactive-context). + + + Gets the current value of the node. Unlike [`node.get()`], `getOnce` does not register any graph dependencies, even when called in [reactive contexts](/not-done?path=../glossary#reactive-context). + + + Attaches an event listener to the node. + + Signature: + + ```ts + on(eventName?, listener, config?) => Cleanup + ``` + + + + Optional. The event to listen for. If not passed, the listener will be a "catch-all" listener that will be called on every node event. + + + Required. The callback function that Zedux will call when the event occurs. + + Signature: + + ```ts + listener = (event, eventMap) => void + ``` + + + + The event object. If the listener is a "catch-all" listener, this argument will be omitted - the `eventMap` will be the only argument. + + + An object containing all events that fired at the same time as the listened-to event, keyed by event type. + + + + + Optional. An object with the following optional properties: + + ```ts + { active } + ``` + + + + Optional. Default: `false`. If `true`, the listener will be an "active" listener, meaning it will prevent the node from becoming `Stale` or `Destroyed`. + + Listeners are passive by default, meaning they don't influence the node's lifecycle status and are appended to a single, special "Listener" observer on the target node. Passive listeners are removed automatically when the node is destroyed (`cycle` listeners will be notified of the destruction just before being removed). + + Active listeners each create their own graph node that observes the target node. They prevent automatic node cleanup and will recreate the target node if it's force-destroyed. + + :::tip + As of Zedux v2, active listeners are the key to [manual graphing](/not-done?path=../../../walkthrough/destruction#manual-graphing). + ::: + + + + + + + + +## See Also + +- [`ecosystem.getNode()`](/not-done?path=./Ecosystem#getnode) +- [the `AtomInstance` class](/not-done?path=./AtomInstance) +- [the `SelectorInstance` class](/not-done?path=./SelectorInstance) +- [the `Signal` class](./Signal) diff --git a/docs/sidebars.js b/docs/sidebars.js index 50617125..60f0cc45 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -164,7 +164,7 @@ module.exports = { { type: 'category', label: 'Classes', - items: ['v2/api/classes/Signal'], + items: ['v2/api/classes/Signal', 'v2/api/classes/ZeduxNode'], }, { type: 'category', From b85ec47f46198c6c6a9a4f49f197befd0edf49db Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Wed, 23 Jul 2025 18:19:00 -0400 Subject: [PATCH 04/49] docs: document `AtomInstance` class for v2 --- docs/docs/v2/api/classes/AtomInstance.mdx | 349 ++++++++++++++++++++++ docs/docs/v2/api/classes/Signal.mdx | 4 +- docs/docs/v2/api/classes/ZeduxNode.mdx | 12 +- docs/sidebars.js | 6 +- 4 files changed, 362 insertions(+), 9 deletions(-) create mode 100644 docs/docs/v2/api/classes/AtomInstance.mdx diff --git a/docs/docs/v2/api/classes/AtomInstance.mdx b/docs/docs/v2/api/classes/AtomInstance.mdx new file mode 100644 index 00000000..9913ddad --- /dev/null +++ b/docs/docs/v2/api/classes/AtomInstance.mdx @@ -0,0 +1,349 @@ +--- +id: AtomInstance +title: AtomInstance +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +All standard atoms (aka "atom instances") are actually instances of this class. When Zedux instantiates [atom templates](/not-done?path=./AtomTemplate) (and [ion templates](/not-done?path=./IonTemplate)), it's just creating instances of this class. + +This class extends the [`Signal` class](./Signal) (yes, atoms _are_ signals) which in turn extends the [`ZeduxNode` class](./ZeduxNode) (all atoms/signals are graph nodes). + +## Creation + +You should never instantiate this class yourself. Zedux does it for you. + +An atom instance is created the first time a given atom template + params combo is used in many Zedux hooks, injectors, and ecosystem methods. + +In components: + +```ts +import { useAtomInstance } from '@zedux/react' +import { myAtom } from './atoms' + +function MyComponent() { + // hooks like `useAtomInstance` create the instance if it doesn't exist yet + const instance = useAtomInstance(myAtom) + ... +} +``` + +In atoms: + +```ts +import { atom, ion } from '@zedux/react' + +const textAtom = atom('text', () => 'example text') + +// getting an atom in another atom will instantiate it if it doesn't exist yet: +const uppercaseAtom = ion('uppercase', ({ get }) => get(textAtom).toUpperCase()) +``` + +Statically, anywhere: + +```ts +import { createEcosystem } from '@zedux/react' + +const ecosystem = createEcosystem() + +// create atom instances: +const instance = ecosystem.getNode(myAtom) +const withParams = ecosystem.getNode(myParameterizedAtom, [ + 'param 1', + 'param 2', +]) +``` + +## Destruction + +Atom instances live forever by default. You can configure this with [ttl](#ttl). You can also destroy them manually via [`node.destroy()`](./ZeduxNode#destroy), or indirectly via an [ecosystem reset](/not-done?path=./Ecosystem#reset). + +### TTL + +Configuring an atom's TTL (Time To Live) is the recommended way to manage its lifecycle. TTL determines how long the atom instance will remain cached after it's no longer in use. + +You can configure this in two ways: + +- Setting the atom template's [`ttl` config](/not-done?path=./AtomTemplate#ttl). This is suitable for most cases, but only accepts a number in milliseconds. +- Returning an [atom api](/not-done?path=./AtomApi) from the atom's state factory that configures a TTL via [`.setTtl()`](/not-done?path=./AtomApi#setttl). This is much more powerful, accepting a number, promise, observable, or a function that returns a number, promise, or observable. + + Setting TTL via returning a configured [atom api](/not-done?path=./AtomApi) overrides any TTL configured on the [atom template](/not-done?path=./AtomTemplate#ttl). It also allows you to configure a different TTL for each instance of the atom. + +The sky's the limit. With TTL, you can destroy atoms on page route, on log out, when the cache reaches a certain size, or anything else you can think of. + +:::tip +Setting `ttl: 0` on an atom template is the most common as it prevents setting any timeouts. [Ions](/not-done?path=../factories/ion) have `ttl: 0` by default. +::: + +## Signal Wrappers + +When a signal is returned from an atom's state factory, the atom becomes a thin wrapper around that signal. Regard this as an implementation detail of the atom. + +Since atoms are signals, consumers never need to know if the atom is a signal wrapper. They should always use the signal methods directly on the atom instance. Zedux will sort out the details for you. + +This is a change from v1. In v1, all atoms had a `.store` property. This made atoms a bit heavier and less abstract - for example, there were subtle differences between `atom.store.setState` and `atom.setState`. In v2, you never need to worry about this. Just always call [`atom.set()`](#set) or [`atom.mutate()`](#mutate) directly. + +## Providing + +An atom instance can be provided over React context via [``](/not-done?path=../components/AtomProvider). + +```tsx +import { AtomProvider, useAtomInstance } from '@zedux/react' +import { myAtom } from './atoms' + +function Parent() { + const instance = useAtomInstance(myAtom) + + return ( + + + + ) +} +``` + +Consume provided instances with [`useAtomContext()`](/not-done?path=../hooks/useAtomContext) + +```ts +import { useAtomContext } from '@zedux/react' +import { myAtom } from './atoms' + +function Child() { + const instance = useAtomContext(myAtom) +} +``` + +## Extending + +There are many aspects of an atom instance's behavior you can change when extending this class. This is an extremely advanced feature. We're not documenting it yet as the internals of this class may change. + +## Generics + +For TypeScript users, atom instances have the following unique generics on their [`NodeGenerics`](./ZeduxNode#generics) (`G`) type generic: + + + + A Record type. The exports of the atom. Will be an empty record if the atom has no exports. + + + Extends `Promise`. The promise type of this node. This will be inferred automatically when the atom's state factory returns an atom promise with a promise attached via [`api().setPromise()`](/not-done?path=./AtomApi#setpromise). + + + +Atom instances also have the following generics inherited from [`ZeduxNode`](./ZeduxNode#generics): + + + + See [`ZeduxNode<{ Events }>`](./ZeduxNode#gevents). + + + See [`ZeduxNode<{ Node }>`](./ZeduxNode#gnode). + + + See [`ZeduxNode<{ Params }>`](./ZeduxNode#gparams). + + + See [`ZeduxNode<{ State }>`](./ZeduxNode#gstate). + + + See [`ZeduxNode<{ Template }>`](./ZeduxNode#gtemplate). + + + +## Events + +Atom instances emit several built-in events. They can also be configured with custom event types by returning a configured [signal](./Signal) from the atom's state factory: + +```ts +const greetingAtom = atom('greeting', () => { + const signal = injectSignal('Hello', { + events: { + greetedPerson: As, + }, + }) + + return signal +}) + +const greetingNode = myEcosystem.getNode(greetingAtom) + +// the `greetingAtom` inherits all events from its returned signal: +greetingNode.send('greetedPerson', 'Jim') +greetingNode.send('greetedPerson', true) // TS Error! Expected a string. +``` + + + + Zedux sends this event whenever `atomInstance.invalidate()` is called. Some + Zedux APIs hook into this event like + [`injectPromise`](/not-done?path=../injectors/injectPromise)'s + `runOnInvalidate` option. + + + Zedux sends this event when an atom instance's [`.promise`](#promise) + reference changed on a reevaluation. This essentially makes an atom's + `.promise` another piece of its state - all Zedux's injectors, atom getters, + and React hooks will cause a reevaluation/rerender when this event fires. + + + +Atom instances also inherit the following built-in events from [`Signal`](./Signal#events): + + + + See the [signal `mutate` event](./Signal#mutate-event). + + + +Atom instances also inherit the following built-in events from [`ZeduxNode`](./ZeduxNode#events): + + + + See the [node `change` event](./ZeduxNode#change-event). + + + See the [node `cycle` event](./ZeduxNode#cycle-event). + + + +## Properties + +Atom instances have the following **readonly** properties: + + + + A reference to the [AtomApi](/not-done?path=./AtomApi) returned from the atom instance's state factory on its last evaluation. + + Unlike [`exports`](#exports), this reference is not stable. It will change on every evaluation. Since atoms have no mechanism to notify observers when this changes, it's not recommended to use this directly. This is exposed for plugin authors and maybe some debugging cases. + + + + An object. May be undefined, if nothing was exported. + + The exports of the atom instance, as defined by the instance's returned [AtomApi](/not-done?path=./AtomApi). You can export absolutely anything. + + This object is stable. It is set the first time an atom instance is created and will not change on subsequent evaluations. + + ```ts + import { api, atom } from '@zedux/react' + + const exportsAtom = atom('exports', () => api().setExports({ hello: 'world' })) + const importAtom = atom('import', () => { + const { hello } = injectAtomInstance(exportsAtom).exports + // `hello` will always be `'world'` here + }) + ``` + + :::important + Don't export state or functions that close over stateful values. + + ```ts + const exampleAtom = atom('example', () => { + const signal = injectSignal('example state') + const state = signal.get() + + const bad = () => state // closes over a value that will become stale ❌ + const good = () => signal.get() // only closes over the stable `signal` ref ✅ + }) + ``` + + ::: + + + + A promise. May be undefined if no promise was set on a returned [AtomApi](/not-done?path=./AtomApi). + + This promise will be used to cause React to suspend whenever this atom instance is used in a component until the promise completes. This promise reference will change if a subsequent evaluation returns a new promise. + + + + The rejection value caught from the instance's [`.promise`](#promise). `undefined` if the promise did not reject. + + + A string or undefined. The status of the instance's [`.promise`](#promise). Will be `undefined` if the atom did not [set a promise](/not-done?path=./AtomApi#setpromise). + + Possible values: + + - `'error'` - the promise rejected. + - `'loading'` - the promise is still pending. + - `'success'` - the promise resolved successfully. + + + + +Atom instances also inherit the following properties from [`ZeduxNode`](./ZeduxNode#properties): + + + See [`ZeduxNode#id`](./ZeduxNode#id). + + See [`ZeduxNode#params`](./ZeduxNode#params). Will always be an array. + + See [`ZeduxNode#status`](./ZeduxNode#status). + + See [`ZeduxNode#template`](./ZeduxNode#template). Will always be a reference + to the [atom template](/not-done?path=./AtomTemplate) this instance was + created from. + + + +## Methods + + + + Forces the atom instance to reevaluate. + + ```tsx live ecosystemId=AtomInstance/invalidate resultVar=Coin version=2 + const coinTossAtom = atom('coinToss', () => Math.random() < 0.5) + + function Coin() { + const isHeads = useAtomValue(coinTossAtom) + const { invalidate } = useAtomInstance(coinTossAtom) + + return + } + ``` + + To access this method inside the atom's state factory, use [`injectSelf()`](/not-done?path=../injectors/injectSelf). + + + + +Atom instances also inherit the following methods from [`Signal`](./Signal#methods): + + + + See [`Signal.mutate`](#mutate). + + When a signal is returned from an atom's state factory, this method will become a thin wrapper around that signal's `mutate` method. + + + + See [`Signal.send`](#send). + + When a signal is returned from an atom's state factory, this method will become a thin wrapper around that signal's `send` method. + + + + See [`Signal.set`](#set). + + When a signal is returned from an atom's state factory, this method will become a thin wrapper around that signal's `set` method. + + + + +Atom instances also inherit the following methods from [`ZeduxNode`](./ZeduxNode#methods): + + + See [`ZeduxNode.destroy`](./ZeduxNode#destroy). + See [`ZeduxNode.get`](./ZeduxNode#get). + See [`ZeduxNode.getOnce`](./ZeduxNode#getonce). + See [`ZeduxNode.on`](./ZeduxNode#on). + + +## See Also + +- [The Atom Instances walkthrough](/not-done?path=../../../walkthrough/atom-instances) +- [The `Signal` class](./Signal) +- [The `ZeduxNode` class](./ZeduxNode) +- [The `AtomTemplate` class](/not-done?path=./AtomTemplate) +- [The `AtomApi` class](/not-done?path=./AtomApi) diff --git a/docs/docs/v2/api/classes/Signal.mdx b/docs/docs/v2/api/classes/Signal.mdx index 277e2c95..c975d4f0 100644 --- a/docs/docs/v2/api/classes/Signal.mdx +++ b/docs/docs/v2/api/classes/Signal.mdx @@ -7,7 +7,7 @@ import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' The object returned by [`injectSignal`](../injectors/injectSignal). Signals are reactive state containers. Each signal holds a value and provides methods for accessing and updating that value. In Zedux, the term "signal" refers to an instance of this class. -Atoms themselves are signals. That simply means the [AtomInstance](/not-done?path=./AtomInstance) class extend this class. An atom can also contain many inner signals. When a signal is returned from an [atom state factory](/not-done?path=../factories/atom#valueorfactory), the atom becomes a thin wrapper around the returned signal. +Atoms themselves are signals. That simply means the [AtomInstance](./AtomInstance) class extend this class. An atom can also contain many inner signals. When a signal is returned from an [atom state factory](/not-done?path=../factories/atom#valueorfactory), the atom becomes a thin wrapper around the returned signal. [Mapped signals](/not-done?path=./MappedSignal) are also signals themselves. @@ -469,4 +469,4 @@ Signals also inherit the following methods from [`ZeduxNode`](./ZeduxNode#method - [The `MappedSignal` class](/not-done?path=./MappedSignal) - [The `ZeduxNode` class](./ZeduxNode) -- [The `AtomInstance` class](/not-done?path=./AtomInstance) +- [The `AtomInstance` class](./AtomInstance) diff --git a/docs/docs/v2/api/classes/ZeduxNode.mdx b/docs/docs/v2/api/classes/ZeduxNode.mdx index 8278ec6b..fea074c4 100644 --- a/docs/docs/v2/api/classes/ZeduxNode.mdx +++ b/docs/docs/v2/api/classes/ZeduxNode.mdx @@ -6,7 +6,7 @@ title: ZeduxNode import useBaseUrl from '@docusaurus/useBaseUrl' import { Legend, Item, Link, Tabs, tab1, tab2 } from '@site/src/all' -This is the base class for every [graph node](/not-done?path=../glossary#graph-node). The [AtomInstance class](/not-done?path=./AtomInstance), [SelectorInstance class](/not-done?path=./SelectorInstance), [Signal class](./Signal), and several other graph node types extend this class. +This is the base class for every [graph node](/not-done?path=../glossary#graph-node). The [AtomInstance class](./AtomInstance), [SelectorInstance class](/not-done?path=./SelectorInstance), [Signal class](./Signal), and several other graph node types extend this class. You never create this class directly and should never need to reference it directly except as a type. @@ -47,13 +47,13 @@ Full list of keys on the `NodeGenerics` (`G`) type generic: A tuple type. The parameters of this node. Only exists on [atom - instances](/not-done?path=./AtomInstance) and [selector + instances](./AtomInstance) and [selector instances](/not-done?path=./SelectorInstance). The state type of this node. Can be anything. A recursive reference to the current node's full template type. `undefined` - if none. Only [atom instances](/not-done?path=./AtomInstance) and [selector + if none. Only [atom instances](./AtomInstance) and [selector instances](/not-done?path=./SelectorInstance) have templates. @@ -179,7 +179,7 @@ Every node has the following **readonly** properties: An array. The parameters passed to this node when it was created. - A reference to the raw, unserialized params that were used to create this atom instance. Only [atom instances](/not-done?path=./AtomInstance) and [selector instances](/not-done?path=./SelectorInstance) take params. This will be `undefined` for node types that don't take params (like signals). For nodes that can take params but weren't passed any (like singleton atoms), this will be an empty array. + A reference to the raw, unserialized params that were used to create this atom instance. Only [atom instances](./AtomInstance) and [selector instances](/not-done?path=./SelectorInstance) take params. This will be `undefined` for node types that don't take params (like signals). For nodes that can take params but weren't passed any (like singleton atoms), this will be an empty array. ```ts const instanceA = useAtomInstance(myAtom, ['param 1', 'param 2']) @@ -241,7 +241,7 @@ function Shout() { - A reference to the template this node was created from. `undefined` if none. Only [atom instances](/not-done?path=./AtomInstance) and [selector instances](/not-done?path=./SelectorInstance) have templates. + A reference to the template this node was created from. `undefined` if none. Only [atom instances](./AtomInstance) and [selector instances](/not-done?path=./SelectorInstance) have templates. For atom instances, this will be the [atom template](/not-done?path=./AtomTemplate). For selector instances, this will be the [selector template](/not-done?path=../types/SelectorTemplate). @@ -337,6 +337,6 @@ Every node has the following methods: ## See Also - [`ecosystem.getNode()`](/not-done?path=./Ecosystem#getnode) -- [the `AtomInstance` class](/not-done?path=./AtomInstance) +- [the `AtomInstance` class](./AtomInstance) - [the `SelectorInstance` class](/not-done?path=./SelectorInstance) - [the `Signal` class](./Signal) diff --git a/docs/sidebars.js b/docs/sidebars.js index 60f0cc45..a42581a1 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -164,7 +164,11 @@ module.exports = { { type: 'category', label: 'Classes', - items: ['v2/api/classes/Signal', 'v2/api/classes/ZeduxNode'], + items: [ + 'v2/api/classes/AtomInstance', + 'v2/api/classes/Signal', + 'v2/api/classes/ZeduxNode', + ], }, { type: 'category', From dda386e9468a8030d9d607407ba9ad7c30c5ae90 Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Thu, 24 Jul 2025 09:04:26 -0400 Subject: [PATCH 05/49] docs: document `Ecosystem` class for v2 --- docs/docs/v2/api/classes/AtomInstance.mdx | 2 +- docs/docs/v2/api/classes/Ecosystem.mdx | 1114 +++++++++++++++++++++ docs/docs/v2/api/classes/Signal.mdx | 4 +- docs/docs/v2/api/classes/ZeduxNode.mdx | 8 +- docs/sidebars.js | 1 + 5 files changed, 1122 insertions(+), 7 deletions(-) create mode 100644 docs/docs/v2/api/classes/Ecosystem.mdx diff --git a/docs/docs/v2/api/classes/AtomInstance.mdx b/docs/docs/v2/api/classes/AtomInstance.mdx index 9913ddad..324f64f8 100644 --- a/docs/docs/v2/api/classes/AtomInstance.mdx +++ b/docs/docs/v2/api/classes/AtomInstance.mdx @@ -56,7 +56,7 @@ const withParams = ecosystem.getNode(myParameterizedAtom, [ ## Destruction -Atom instances live forever by default. You can configure this with [ttl](#ttl). You can also destroy them manually via [`node.destroy()`](./ZeduxNode#destroy), or indirectly via an [ecosystem reset](/not-done?path=./Ecosystem#reset). +Atom instances live forever by default. You can configure this with [ttl](#ttl). You can also destroy them manually via [`node.destroy()`](./ZeduxNode#destroy), or indirectly via an [ecosystem reset](./Ecosystem#reset). ### TTL diff --git a/docs/docs/v2/api/classes/Ecosystem.mdx b/docs/docs/v2/api/classes/Ecosystem.mdx new file mode 100644 index 00000000..ded6cdf0 --- /dev/null +++ b/docs/docs/v2/api/classes/Ecosystem.mdx @@ -0,0 +1,1114 @@ +--- +id: Ecosystem +title: Ecosystem +--- + +import { Legend, Item, Link, Tabs, tab1, tab2 } from '@site/src/all' + +The ecosystem is an isolated atom environment. Every ecosystem manages: + +- Schedulers for intelligently running atom-related tasks. +- A cache of all your active graph nodes, including atoms, selectors, signals, and more. +- Global events for plugging into your graph, atom state, and more. +- Dependency Injection overrides. +- Hydration, e.g. for SSR. +- Global config options like [`ssr`](#ssr) and [`onReady`](#onready). + +The Ecosystem class itself defines many methods for creating, destroying, and inspecting atoms, selectors, signals, and the graph they all form. + +Ecosystems can be used completely outside of React. This can be helpful for testing atoms and selectors. + +## Creation + +Create ecosystems with [the `createEcosystem()` factory](/not-done?path=../factories/createEcosystem). + +```ts +import { createEcosystem } from '@zedux/react' + +const rootEcosystem = createEcosystem({ id: 'root' }) +``` + +Ecosystems are also created automatically when using an [``](/not-done?path=../components/EcosystemProvider) without passing an `ecosystem` prop: + +```tsx +import { EcosystemProvider } from '@zedux/react' + +function App() { + return ( + + + + ) +} +``` + +### Default Ecosystem + +The [default ecosystem](../../../walkthrough/ecosystems#global) will be created automatically if atoms are used in React outside any `` or the first time you call [`getDefaultEcosystem()`](/not-done?path=../utils/getDefaultEcosystem). + +The default ecosystem is great for simple apps. It's a full ecosystem, which means you can use features like [overrides](#overrides) and [ecosystem events](#events). However, it comes preconfigured with no (good) way to set config options like [`ssr`](#ssr) and [`onReady`](#onready). + +It's generally recommended to create your own ecosystem(s) via [`createEcosystem()`](/not-done?path=../factories/createEcosystem) and provide them to your app via [``](/not-done?path=../components/EcosystemProvider) instead of using the default ecosystem. This is especially needed for SSR. + +## Providing + +Ecosystems can take control of all atom usages in a React component tree by wrapping the tree in [``](/not-done?path=../components/EcosystemProvider). + +```tsx +function App() { + return ( + + + + ) +} +``` + +## Overrides + +The ability to swap out atom implementations on the fly is one of Zedux's superpowers. Use [`.addOverrides`](#addoverrides), [`.removeOverrides`](#removeoverrides), or [`.setOverrides`](#setoverrides). + +```tsx live ecosystemId=Ecosystem/modifying-overrides resultVar=Swapper version=2 +const one = atom('common-key', () => 'Numero Uno') +const two = atom('common-key', () => 'I am the best') +const three = atom('common-key', () => 'Two is not the best') + +function Swapper() { + const ecosystem = useEcosystem() + const state = useAtomValue(one) + + return ( + <> +
Current State: {state}
+ + + + + ) +} +``` + +## Events + +Ecosystems have many intrinsic events you can hook into to implement plugins and many extra features like universal undo/redo, persistence, and more. + +Register event listeners with [`ecosystem.on()`](#on). Clean up the listener with the returned cleanup function. + +```ts +ecosystem.on('change', (event, eventMap) => { + // the passed `event` object has properties specific to the event type. The + // second `eventMap` parameter contains all events that occurred in the + // ecosystem at the same time as the listened-to event. +}) + +ecosystem.on(eventMap => { + // catch-all listener. Will be called for every possible event. The passed + // `eventMap`object maps event names to payloads. +}) +``` + +Full event list: + + + + Sent whenever any graph node's value changes. Event shape: + + ```ts + { newState, oldState, reasons?, source?, type } + ``` + + - `newState` - The new state of the graph node. Can be anything. + - `oldState` - The previous state of the graph node. Can be anything. + - `reasons` - An indefinitely-nested array of [reasons](/not-done?path=../types/EvaluationReason) why the state changed. This tracks the series of state changes through the dependency graph that led to the current node changing. If the node was updated directly, this will be undefined. + - `source` - The [`ZeduxNode`](./ZeduxNode) that changed. In normal Zedux v2+ apps, this will always be defined. The legacy `@zedux/stores` package's stores are the only exception. + - `type` - The string `"change"`. + +```tsx live noProvide=true resultVar=App version=2 +const counterAtom = atom('counter', () => 0) +const timesTwoAtom = ion('timesTwo', ({ get }) => get(counterAtom) * 2) + +function App() { + const [count, setCount] = useAtomState(counterAtom) + const timesTwo = useAtomValue(timesTwoAtom) + const ecosystem = useEcosystem() + + useEffect(() => { + return ecosystem.on('change', (event, eventMap) => { + console.log('change', event, eventMap) + }) + }, []) + + return ( + <> +
Count: {count}
+
Times Two: {timesTwo}
+ + + ) +} +``` + +
+ + Sent when any graph node's lifecycle status changes. Possible transitions: + + - `Initializing` -> `Active` + - `Active` -> `Stale` + - `Active` -> `Destroyed` + - `Stale` -> `Active` + - `Stale` -> `Destroyed` + + Event shape: + + ```ts + { oldStatus, newStatus, reasons?, source?, type } + ``` + + - `oldStatus` - A string. The previous status of the graph node. Refer to the above transitions list for possible values. + - `newStatus` - A string. The new status of the graph node. Refer to the above transitions list for possible values. + - `source` - The [`ZeduxNode`](./ZeduxNode) that changed. Though the types don't reflect it yet, this will always be defined. + - `type` - The string `"cycle"`. + +```tsx live noProvide=true resultVar=App version=2 +const counterAtom = atom('counter', () => 0, { ttl: 0 }) + +function MaybeMounted() { + const [count, setCount] = useAtomState(counterAtom) + + return
Count: {count}
+} + +function App() { + const [isMounted, setIsMounted] = useState(true) + const ecosystem = useEcosystem() + + useEffect(() => { + return ecosystem.on('cycle', (event, eventMap) => { + console.log('cycle', event, eventMap) + }) + }, []) + + return ( + <> + + {isMounted && } + + ) +} +``` + +
+ + Sent whenever `atomInstance.invalidate()` is called. Some Zedux APIs hook into this event like [`injectPromise`](/not-done?path=../injectors/injectPromise)'s `runOnInvalidate` option. + + Event shape: + + ```ts + { source, type } + ``` + + - `source` - The [`ZeduxNode`](./ZeduxNode) that was [invalidated](./AtomInstance#invalidate). + - `type` - The string `"invalidate"`. + + + + Sent when an atom instance's `.promise` reference changed on a reevaluation. This essentially makes an atom's `.promise` another piece of its state - all Zedux's injectors, atom getters, and React hooks will cause a reevaluation/rerender when this event fires. + + Event shape: + + ```ts + { reasons, source, type } + ``` + + - `reasons` - An indefinitely-nested array of [reasons](/not-done?path=../types/EvaluationReason) that resulted in the atom reevaluating and changing its promise. This tracks the series of state changes through the dependency graph that led to the current node reevaluating. If the node was updated directly, this will be undefined. + - `source` - The [`ZeduxNode`](./ZeduxNode) whose promise changed. Though the types don't reflect it yet, this will always be defined. + - `type` - The string `"promiseChange"`. + + + + Sent when a graph edge (aka a dependency) between two nodes is added, updated, or removed. + + Event shape: + + ```ts + { action, observer, source, type } + ``` + + - `action` - A string. Possible values: `"add"`, `"remove"`, or `"update"`. Describes what happened to the edge. + - `"add"` - The `observer` node is now observing the `source` node. + - `"remove"` - The `observer` node is no longer observing the `source` node. + - `"update"` - The relationship between the `observer` and `source` node has changed - e.g. the dependency went from static to dynamic due to calling `get` instead of `getNode` on a reevaluation. + - `observer` - The [`ZeduxNode`](./ZeduxNode) that is observing the source. + - `source` - The [`ZeduxNode`](./ZeduxNode) that is being observed. + - `type` - The string `"edge"`. + + + + Sent when any atom or selector errors during evaluation or when an atom's [promise](./AtomInstance#promise) rejects. Useful for logging. + + Event shape: + + ```ts + { error, source, type } + ``` + + - `error` - An [`Error`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) instance. The error that occurred. + - `source` - The [`ZeduxNode`](./ZeduxNode) that errored. Though the types don't reflect it yet, this will always be defined. + - `type` - The string `"error"`. + + + + Sent when the ecosystem is [`.reset`](#reset), after the reset completes. This is the perfect time to reinitialize the ecosystem, e.g. preloading atoms. + + Event shape: + + ```ts + { hydration, listeners, overrides, type } + ``` + + - `hydration` - Boolean. Whether [`ecosystem.reset`](#reset) was called with `hydration: true`, removing any previous hydrations set via [`ecosystem.hydrate`](#hydrate). + - `listeners` - Boolean. Whether [`ecosystem.reset`](#reset) was called with `listeners: true`, removing ecosystem event listeners registered via [`ecosystem.on`](#on). + - `overrides` - Boolean. Whether [`ecosystem.reset`](#reset) was called with `overrides: true`, removing any previous overrides set via [`ecosystem.setOverrides`](#setoverrides)/[`ecosystem.addOverrides`](#addoverrides). + - `type` - The string `"resetEnd"`. + + + + Sent when the ecosystem is [`.reset`](#reset), before the reset begins. This can be used to capture atom values, [overrides](#overrides), or any other details about the ecosystem. These can then be restored when the [`resetEnd`](#resetend) event fires. + + Event shape: + + ```ts + { hydration, listeners, overrides, type } + ``` + + - `hydration` - Boolean. Whether [`ecosystem.reset`](#reset) was called with `hydration: true`, removing any previous hydrations set via [`ecosystem.hydrate`](#hydrate). + - `listeners` - Boolean. Whether [`ecosystem.reset`](#reset) was called with `listeners: true`, removing ecosystem event listeners registered via [`ecosystem.on`](#on). + - `overrides` - Boolean. Whether [`ecosystem.reset`](#reset) was called with `overrides: true`, removing any previous overrides set via [`ecosystem.setOverrides`](#setoverrides)/[`ecosystem.addOverrides`](#addoverrides). + - `type` - The string `"resetStart"`. + + + + Sent when any atom or selector finishes evaluating. This can be used in conjunction with the [`runStart`](#runstart) event to track evaluation duration. + + Event shape: + + ```ts + { source, type } + ``` + + - `source` - The [`ZeduxNode`](./ZeduxNode) that finished evaluating. Though the types don't reflect it yet, this will always be defined. + - `type` - The string `"runEnd"`. + + + + Sent when any atom or selector begins evaluating. This can be used in conjunction with the [`runEnd`](#runend) event to track evaluation duration. + + It can also be used to detect circular dependencies - if the same id starts running twice with no `runEnd` event in between, it's circular. + + Event shape: + + ```ts + { source, type } + ``` + + - `source` - The [`ZeduxNode`](./ZeduxNode) that started evaluating. Though the types don't reflect it yet, this will always be defined. + - `type` - The string `"runStart"`. + + :::important + When this event fires on initial evaluation, the `source` node will not be fully defined yet. For atoms, this means its [`.exports`](./AtomInstance#exports), [`.promise`](./AtomInstance#promise), and some internal properties will be undefined. See [`injectSelf`](/not-done?path=../injectors/injectSelf) for more details on uninitialized atoms. + ::: + + +
+ +## Properties + +Every ecosystem has the following properties. All properties are readonly! + + + + A reference to the [async scheduler](/not-done?path=./AsyncScheduler) used by this ecosystem. You may want to access this scheduler's `queue` or `flush` methods. See the [AsyncScheduler doc](/not-done?path=./AsyncScheduler) for details. + + + A boolean. May be undefined. This is set to the `complexParams` value you passed via [EcosystemConfig](/not-done?path=../types/EcosystemConfig#complexparams) when creating this ecosystem. + + Whether to allow non-serializable values as atom and selector params. See [the complex params guide](../../../advanced/complex-params). + + + + An object. May be undefined. A reference to the `context` object passed to [the `createEcosystem()` factory](/not-done?path=../factories/createEcosystem) (if any) or the latest [`.reset()`](#reset) call. + + When `.reset()` is called, the previous context (if any) will be passed as the second parameter to the `onReady` function as part of the reset. + + + + A string or undefined. The id of this ecosystem as set via [EcosystemConfig](/not-done?path=../types/EcosystemConfig#id) when creating this ecosystem (if any). The string `'@@zedux/default'` is reserved for the [default ecosystem](#default-ecosystem). + + This is just for debugging and is not guaranteed to be unique. + + + + A reference to the [sync scheduler](/not-done?path=./SyncScheduler) used by this ecosystem. Unlike the [`asyncScheduler`](#asyncscheduler), normal users should never need to access this. This is mostly for plugin authors to efficiently schedule Zedux jobs. + + + An array of strings. This is set to the `tags` value you passed via [EcosystemConfig](/not-done?path=../types/EcosystemConfig#tags) when creating this ecosystem. + + These work in conjunction with [atom tags](/not-done?path=../types/AtomTemplate#tags) to raise warnings when unsafe atom templates are not overridden in certain environments. + + If an atom template is used that has a flag that is not present in this array, Zedux will log a warning. + + Flag checking is off by default - simply don't pass a tags array to `createEcosystem()` and Zedux will ignore all tags. To turn it on, but with no tags, pass an empty array. + + ```ts + createEcosystem() // flag checking disabled. Zedux will ignore all atom tags. + createEcosystem({ tags: [] }) // flag checking enabled! All tags will log warnings + createEcosystem({ tags: ['a'] }) // all atom tags except 'a' will log warnings + ``` + + Which atoms, which tags, and which environments, is all up to you. You may want to flag atoms that run side effects you don't want to run in tests. Or you may want to flag atoms that use APIs that only work in the browser or electron or any other environment. + + :::tip + This is an opt-in feature. You don't have to use atom tags this way. Just don't pass a tags array to `createEcosystem()` to opt out. + ::: + + + + An object. May be undefined. The shallowly merged result of all calls to [`ecosystem.hydrate()`](#hydrate). + + These values stay in the ecosystem indefinitely, re-hydrating matching atoms every time they're recreated. To prevent this, you may access this property directly and modify it. Examples: + + ```ts + ecosystem.hydration = {} // reassign to a new object ✅ + ecosystem.hydration = undefined // set to undefined ✅ + delete ecosystem.hydration[myAtomId] // mutation ✅ + ``` + + + + A function. May be undefined. This is set to the `onReady` value you passed via [EcosystemConfig](/not-done?path=../types/EcosystemConfig#onready) when creating this ecosystem. + + Will be called as soon as the ecosystem has initialized. Is also called + every time the ecosystem is [`reset`](#reset). + + This is the ideal place to bootstrap data and preload atoms. Since this + function is called on reset, it can be used to ensure the ecosystem's + "necessary data" is always loaded. + + Signature: + + ```ts + (ecosystem, prevContext?) => maybeCleanup + ``` + + + A reference to this ecosystem + + A reference to the previous context value of the ecosystem. `ecosystem.reset()` can be optionally given a new context object. If that happens, the ecosystem's context will be updated before this function is called. So a reference to the old context is passed here. + + This parameter will be undefined the first time `onReady` runs. Thus you can use this to check if this is the initial run. + +```ts +const ecosystem = createEcosystem({ + context: { redux: reduxStore }, + onReady: (ecosystem, prevContext) => { + if (!prevContext) { + // this is the initial run + } else { + // onReady is running after an ecosystem reset + const nextContext = ecosystem.context + + if (prevContext.redux !== nextContext.redux) { + // ecosystem.reset() changed the redux store reference + } + } + }, +}) + +ecosystem.reset() // doesn't change context (prevContext === ecosystem.context) +ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context +``` + + Note that replacing context is an all-or-nothing deal. Spread `ecosystem.context` into a new object to update only part of the context: + + ```ts + ecosystem.reset({ + context: { ...ecosystem.context, specialField: 'new val' } + }) + ``` + + + + Either `undefined` or a cleanup function that will be called when the ecosystem is reset. + + + + + + An object mapping atom keys to atom templates. These are the currently-overridden atoms in this ecosystem. Modify this list by calling [`ecosystem.setOverrides()`](#setoverrides), [`ecosystem.addOverrides()`](#addoverrides), and [`ecosystem.removeOverrides()`](#removeoverrides). + + If an initial `overrides` array is passed, they will be immediately mapped into this object. + + + + The [Selectors](/not-done?path=../classes/Selectors) class instance that tracks all cached atom selectors in this ecosystem. + + + A boolean. Default: `false`. Whether the ecosystem is being used on the server to generate SSR content. + + This is set to the `ssr` value you passed via [EcosystemConfig](/not-done?path=../types/EcosystemConfig) when creating this ecosystem. + + Currently the only thing this affects is [`injectEffect()`](/not-done?path=../injectors/injectEffect) - SSR mode prevents effects from running at all in this ecosystem. + + + + +## Methods + + + + Adds new overrides to the ecosystem's current list of overrides and/or updates existing overrides - swapping them out with different implementations. All existing instances that match atom templates in the passed list will be force-destroyed, allowing their dependents to recreate them. + + Signature: + + ```ts + addOverrides = (overrides) => void + ``` + + + Required. An array of [atom templates](/not-done?path=./AtomTemplate). + + + + + Accepts a callback and batches all updates that happen synchronously while the callback is running. Flushes all updates once the passed callback completes. + + Signature: + + ```ts + batch = (callback) => void + ``` + + + + Required. A function that will be called immediately and can do anything and return anything. + + + Whatever the callback returns. Can be anything. + + + + See [the batching guide](/not-done?path=../../advanced/batching). + + + + Returns a snapshot of the current state of atom instances in the ecosystem. + + :::tip + Atoms are typically the only node type you want dehydrated/rehydrated, so they're the only nodes this method dehydrates. To dehydrate other node types, use [`ecosystem.findAll()`](#findall) and map/filter/reduce the results yourself. + ::: + + Signature: + + ```ts + dehydrate = (filter?) => snapshot + ``` + + + + Optional. Filters to limit the atoms included in the snapshot. If not specified, all atom instances will be dehydrated. + + See [the DehydrationFilter type](/not-done?path=../types/DehydrationFilter) for allowed values and what they do. + + + An object mapping atom instance ids to their current state. + + + + + + Works similar to [`ecosystem.getNode()`](#getnode) except it does NOT create the atom instance if it doesn't exist. In that case, [`ecosystem.find()`](#find) returns undefined. Because of this, `find` is called a "weak getter". + + ```ts + // creates the atom instance if needed: + instance = ecosystem.getNode(myAtom, [...params]) + // never creates an atom instance: + instance = ecosystem.find(myAtom, [...params]) + + // `find` can also search for a node by id: + instance = ecosystem.find('myAtomKey') + ``` + + :::tip + `ecosystem.find()` is best used with singleton atoms/selectors (that don't take params) or when you know the exact node id you're looking for. When fuzzy searching, it's much better to use [`ecosystem.findAll()`](#findall) and filter the results yourself rather than assuming that `find` will return the one you want (it usually won't). + + Think of it like `document.querySelector()` - it's nice when you know there's only one match, but usually you want `document.querySelectorAll()`. + ::: + + Signature: + + ```ts + find = (templateOrSearch, params?) => instance + ``` + + + + Required. An atom template, selector function reference, or string. If an atom or selector template is passed, `find` will return the first instance of that atom or selector it encounters. + + If a string is passed, `find` will return the node that exactly-matches that id (if any). If no exact match is found, `find` performas a "fuzzy search", returning the first node it encounters whose id contains the passed string (case-insensitive). + + + Optional. The atom or selector's params (only relevant when passing an atom or selector template). If passed, `find` will return the node that matches the passed params. + + + The matching [graph node](./ZeduxNode), if it exists, otherwise undefined. + + + + + + Returns an array of all graph nodes in the ecosystem. Pass a filter to limit the results. Or filter the returned array yourself. + + Since `findAll` only returns existing nodes and can never create them, it's called a "weak getter". + + ```ts + // some common patterns: + myEcosystem.findAll('@atom') // get all atoms + myEcosystem.findAll(myAtom) // get all instances of the `myAtom` template + + myEcosystem.findAll().map(({ id }) => id) // get all cached node ids + myEcosystem.findAll('@atom').map(({ id }) => id) // get all cached atom ids + + // get all nodes that export `someExport`: + myEcosystem.findAll().filter(node => node.exports?.someExport) + + // get all nodes with a promise set: + myEcosystem.findAll().filter(node => node.promise) + ``` + + Signature: + + ```ts + findAll = (filter?) => nodeList + ``` + + + + Optional. Filters to limit the nodes returned. If not specified, `findAll` returns all nodes. + + See [the NodeFilter type](/not-done?path=../types/NodeFilter) for allowed values and what they do. + + + An array of all cached nodes in the ecosystem that match the filter. + + + + + + Gets the current value of an atom instance or selector. Creates and caches the atom or selector instance if it doesn't exist yet. + + ```ts + const { get } = myEcosystem // `get` can be destructured like this + + value = get(myAtom) // atom + value = get(mySelectorFunction) // selector + value = get(myAtom, [param1, param2]) // atom with params + + // passing an instance directly: + const myAtomInstance = myEcosystem.getNode(myAtom) // creates a static dep + value = get(myAtomInstance) // upgrades to a dynamic dep + ``` + + The node whose value `get` returns is unique for the given template + params combo. + + #### Reactivity + + `get` is reactive by default. This behavior changes depending on when it's called: + + - When called in a [reactive context](/not-done?path=../glossary#reactive-context) (e.g. while an atom or selector is evaluating), `get` registers a [dynamic graph dependency](/not-done?path=../glossary#dynamic-graph-dependency) between the currently-evaluating node and the retrieved node. + - When called outside evaluation (e.g. in an effect or callback), `get` doesn't register any graph dependencies. + + Note that [`node.get()`](./ZeduxNode#get) has this exact same behavior. In fact, `ecosystem.get(myAtom)` is almost a shorthand for `ecosystem.getNode(myAtom).get()`, with one key difference: + + #### Note on Caching + + When an `ecosystem.getNode` call creates a new node, it always caches it. `ecosystem.get`, however, only caches nodes when it's "safe": + + - During evaluation, `get` always caches the retrieved node. Zedux's intelligent dependency tracking prevent any memory leaks in this case. + - Outside evaluation, `get` only caches atoms, **not selectors**. This is because selectors are often created on-the-fly. Since selectors are cached by reference, this makes it easy to accidentally leak memory. + + Consider this example: + + ```ts + // if `get` cached selectors, this would create a new cache entry every time + // it's called, with no good way of cleaning up previous entries: + function getExpensiveVal(listAtom) { + return myEcosystem.get(({ get }) => get(listAtom).reduce(myExpensiveReducer)) + } + ``` + + Fortunately, `get` does not cause leaks here. Instead, it creates the new selector instance, gets its value, and immediately destroys it. This ensures that all transitive dependencies of the selector are also cleaned up as necessary. + + :::tip + Use [`ecosystem.find(myAtom)?.get()`](#find) to get a value only if the instance already exists. Note however that `find` doesn't register graph dependencies. To weakly register a dependency, use the following pattern: + + ```ts + const instance = ecosystem.find(myAtom, [param1, param2]) + const val = instance ? get(instance) : defaultVal + ``` + ::: + + Signature: + + ```ts + get = (templateOrInstance, params?) => value + ``` + + + + Required. An [atom template](/not-done?path=./AtomTemplate), [selector template](/not-done?path=../types/SelectorTemplate), or atom instance. + + + Optional (required if the template takes params). An array of the atom or selector's params. Only relevant when passing an atom or selector template. + + + The current value of the retrieved atom or selector. + + + + + + Gets a cached [graph node](/not-done?path=../glossary#graph-node), namely an [atom instance](./AtomInstance) or [selector instance](/not-done?path=./SelectorInstance). Creates and caches the node if it doesn't exist yet. + + ```ts + const { getNode } = myEcosystem // `getNode` can be destructured like this + + node = getNode(myAtom) // atom + node = getNode(mySelectorFunction) // selector + node = getNode(myAtom, [param1, param2]) // atom with params + + // the returned node is an instance of `AtomInstance` or `SelectorInstance` + getNode(myAtom).set(newState) + getNode(myAtom).exports.myExport() + getNode(mySelectorFunction).get() + ``` + + The returned node is unique for the given template + params combo. + + #### Reactivity + + `getNode` registers graph dependencies by default. This behavior changes depending on when it's called: + + - When called in a [reactive context](/not-done?path=../glossary#reactive-context) (e.g. while an atom or selector is evaluating), `getNode` registers a [static graph dependency](/not-done?path=../glossary#static-graph-dependency) (unlike [`ecosystem.get`](#get), which registers dynamic dependencies) between the currently-evaluating node and the retrieved node. + - When called outside evaluation (e.g. in an effect or callback), `getNode` doesn't register any graph dependencies. + + #### Note on Caching + + When an `ecosystem.getNode` call creates a new node, it always caches it. This is different from [`ecosystem.get`](#get), which only caches nodes when it's "safe". This means you may need to be aware of when a `getNode` call has created a new node so you can properly dispose of it: + + ```ts + function getExpensiveVal(listAtom) { + // this inline selector creates a new node every time this function runs: + const node = myEcosystem.getNode( + ({ get }) => get(listAtom).reduce(myExpensiveReducer) + ) + const val = node.get() + + // destroy is safe to call outside evaluation - it bails out if the node + // has dependencies: + node.destroy() + + return val + } + ``` + + Note that this example is exactly what [`ecosystem.get`](#get) does for you outside evaluation. + + :::tip + Use [`ecosystem.find()`](#find) to get an instance only if it already exists. Note however that `find` doesn't register graph dependencies. To weakly register a static dependency, use the following pattern: + + ```ts + const maybeInstance = ecosystem.find(myAtom, [param1, param2]) + const instance = maybeInstance ? getNode(maybeInstance) : undefined + ``` + ::: + + Signature: + + ```ts + getNode = (templateOrInstance, params?) => instance + ``` + + + + Required. An [atom template](/not-done?path=./AtomTemplate), [selector template](/not-done?path=../types/SelectorTemplate), or [atom instance](./AtomInstance). + + When an instance is passed, `getNode` registers a static graph dependency on that instance (when called during evaluation) and returns it as-is. + + + Optional (required if the template takes params). The atom or selector's params. Only relevant when passing an atom or selector template. + + + The cached atom or selector instance for the given template + params combo. + + + + + + Returns a [graph node](/not-done?path=../glossary#graph-node). Functions exactly like [`ecosystem.getNode`](#getnode) except it never registers graph dependencies even when called in [reactive contexts](/not-done?path=../glossary#reactive-context). + + Signature: + + ```ts + getNodeOnce = (templateOrInstance, params?) => instance + ``` + + See [`ecosystem.getNode`](#getnode) for details. + + + + Returns the current value of the resolved atom or selector node. Functions exactly like [`ecosystem.get`](#get) except it never registers graph dependencies even when called in [reactive contexts](/not-done?path=../glossary#reactive-context). + + Signature: + + ```ts + getOnce = (templateOrInstance, params?) => instance + ``` + + See [`ecosystem.get`](#get) for details. + + + + Turns an array of anything into a predictable string. + + This is how all Zedux APIs generate consistent, deterministic ids for atom and selector params. This algorithm is almost exactly like [React Query's hashing algorithm](https://tanstack.com/query/latest/docs/framework/react/guides/query-keys#query-keys-are-hashed-deterministically). + + Note that circular object references are not supported. + + ```ts + // these generate the same hash: + ecosystem.hash(['todos', { status, page }]) + ecosystem.hash(['todos', { page, status }]) + + // these generate different hashes (array item order matters): + ecosystem.hash(['todos', { status, page }]) + ecosystem.hash([{ page, status }, 'todos']) + ``` + + Signature: + + ```ts + hash = (params) => hashedString + ``` + + + + Required. An array of anything. + + If any item is a [graph node](/not-done?path=../glossary#graph-node), it will be serialized as the node's id. + + + Optional. A boolean. Defaults to the ecosystem's [`complexParams` config](#complexparams). + + If true, class instances and functions will be weak-mapped to a consistent id for the reference. These objects can be circular. + + + A consistent string. + + + + + + Hydrates the state of atom instances in this ecosystem, usually using a previous state snapshot from [`ecosystem.dehydrate()`](#dehydrate). + + If `.hydrate()` has been called before, the new hydration will be shallowly merged into the existing hydration. + + Signature: + + ```ts + hydrate = (snapshot, config?) => void + ``` + + + + Required. An object. The keys of this object are ids corresponding to atom instances that may or may not exist in the ecosystem yet. + + + Optional. An object containing the following optional property: + + ```ts + { retroactive } + ``` + + By default, Zedux will update the state of all existing atom instances that have an entry in the passed snapshot. To disable this, pass `{ retroactive: false }`. + + + + + + Generates a consistent id that is guaranteed to be unique in this ecosystem, but not at all guaranteed to be unique globally. + + You can override this by passing the [`makeId`](/not-done?path=../types/EcosystemConfig#makeid) option to [`createEcosystem`](/not-done?path=../factories/createEcosystem). The default implementation is suitable for most use cases, including: + + - apps that use only one ecosystem (the most common). + - snapshot testing the ecosystem graph and dehydrations - calling `ecosystem.reset()` after each test will reset the ecosystem's id counter. + + You may want to override this when using multiple ecosystems or to customize ids to your liking (for example, prefixing atoms with `@atom()` to match all other node types). + + :::important + The default node filtering used by [`ecosystem.findAll()`](#findall), [`ecosystem.find()`](#find), and [`ecosystem.dehydrate()`](#dehydrate) depends on the default id format. When overriding `makeId`, you must filter nodes yourself: + + ```ts + ecosystem.findAll().filter(myCustomFilter) + ``` + ::: + + Every node type **except atoms** has an `@` prefix. If a node's id is not `@`-prefixed, it's an atom instance. The full list of built-in prefixes is: + + - `@component()-` An external node created via a React hook call. Wraps the component's name inside the `()` (only works in dev builds of React). + - `@listener()-` A `ZeduxNode#on` call. Wraps the listened node's template key inside the `()`. + - `@memo()-` An atom selector created via an `injectMemo` call with no deps. Wraps the containing atom's template key inside the `()`. + - `@ref()-` A function or class instance reference tracked when the ecosystem is configured with `complexParams: true`. Wraps the function or class name inside the `()`. + - `@selector()-` An atom selector. Wraps the selector's name inside the `()`. + - `@signal()-` A signal created via [`ecosystem.signal`](#signal) or [`injectSignal`](../injectors/injectSignal) or a mapped signal created via [`injectMappedSignal`](/not-done?path=../injectors/injectMappedSignal). Wraps the containing atom's template key inside the `()` (empty if created via `ecosystem.signal`). + + + + + Adds an event listener to the ecosystem. These listeners can hook into many different ecosystem events. This is the key to building plugins like loggers, dev tools, etc. + + Signature: + + ```ts + on = (eventName?, listener) => void + ``` + + + + Optional. The event to listen for. If not passed, the listener will be a "catch-all" listener that will be called on every ecosystem event. + + See [the events section](#events) for the full list of ecosystem events. + + + Required. The callback function that Zedux will call when the event occurs. + + Signature: + + ```ts + listener = (event, eventMap) => void + ``` + + + + The event object. If the listener is a "catch-all" listener, this argument will be omitted - the `eventMap` will be the only argument. + + See [the events section](#events) for event object details. + + + An object containing all events that fired at the same time as the listened-to event, keyed by event type. + + + + + A cleanup function. Call it to remove the listener. + + + + + + + Removes overrides previously set via `.addOverrides()` or `.setOverrides()`. All existing instances of atom templates in the passed list will be force-destroyed, allowing their dependents to recreate them using the original, non-overridden atom template. + + You can pass either the original template or the override. Zedux only looks at their `key` properties. You can also pass strings matching atom template keys. + + Signature: + + ```ts + removeOverrides = (overrides) => void + ``` + + + + Required. An array of [atom templates](/not-done?path=./AtomTemplate) and/or template key strings. If any haven't been set as overrides previously, they'll be ignored. + + + + + + Force destroys all [graph nodes](/not-done?path=../glossary#graph-node) in the ecosystem. Calls the cleanup function returned from the [onReady](/not-done?path=../types/EcosystemConfig#onready) function (if any), and calls `onReady` again to reinitialize the ecosystem. + + Accepts several options to also clear cached hydrations, listeners, and overrides. + + ```ts + const myEcosystem = createEcosystem({ + context: { someField: 'some val' }, + id: 'example', + onReady: (ecosystem, prevContext) => + console.log('old context:', prevContext, 'new context:', ecosystem.context), + }) + // old context: undefined new context: { someField: 'some val' } + + myEcosystem.reset({ context: { someField: 'new val' } }) + // old context: { someField: 'some val' } new context: { someField: 'new val' } + ``` + + Signature: + + ```ts + reset = (options?) => void + ``` + + + + Optional. An object containing the following optional properties: + + ```ts + { context, hydration, listeners, overrides } + ``` + + - `context`: An object to be set as the new `.context` value of the ecosystem. + - `hydration`: A boolean. Default: `false`. Pass `true` to also remove all hydrations previously passed to [`ecosystem.hydrate()`](#hydrate). + - `listeners`: A boolean. Default: `false`. Pass `true` to also remove all ecosystem event listeners previously passed to [`ecosystem.on()`](#on). + - `overrides`: A boolean. Default: `false`. Pass `true` to also remove all overrides previously passed to [`ecosystem.setOverrides()`](#setoverrides) or [`ecosystem.addOverrides()`](#addoverrides). + + + + + + Replaces the ecosystem's list of overridden atoms with the passed overrides. All instances of atom templates in either the new or old lists will be force-destroyed, allowing their dependencies to recreate them. + + To selectively update only certain atoms, use [`ecosystem.addOverrides()`](#addoverrides) or [`ecosystem.removeOverrides()`](#removeoverrides). + + Signature: + + ```ts + setOverrides = (newOverrides) => void + ``` + + + + Required. An array of atom templates. This will be set as the new [`.overrides` property](#overrides). + + + + + + Creates and caches a new signal. It's recommended to use [`injectSignal`](../injectors/injectSignal) instead when possible. However, `ecosystem.signal` can be useful for dynamically creating signals, at the cost of having to manage their lifecycles. For example: + + ```ts + // `injectSignal` manages the signal's lifecycle... + const injectedSignal = injectSignal(0) + + for (let i = 0; i < 1000; i++) { + // ...but `ecosystem.signal` doesn't... + const newSignal = ecosystem.signal(i) + + // ...so manually tie these signals to this atom's lifecycle by calling + // `.get` on each one during evaluation: + newSignal.get() + } + ``` + + This is just one example. You can manage these signals however you like - e.g. calling [`.destroy`](./ZeduxNode#destroy) on them manually or just keeping them around forever. + + Signature: + + ```ts + signal = (initialState, config?) => signal + ``` + + + + Required. The initial state of the signal. + + + Optional. An object containing the following optional property: + + ```ts + { events } + ``` + + - `events`: An object mapping custom event names to `As`. See [the `As` util](/not-done?path=../utils/As) for more details. + + + A new signal instance. + + + + + + See what the ecosystem's atom graph currently looks like. There are 3 graph "views": + + - `'top-down'` + - `'bottom-up'` + - `'flat'` + + `'flat'` is the default and is the most useful. It returns a normalized object containing every node in the graph. Each node points to its sources (aka "dependencies") and observers (aka "dependents") in the top-level object. + + Top-down and bottom-up are mostly just for DX, to help you quickly gain some insight into what your dependency graph actually looks like. + + Signature: + + ```ts + viewGraph = (view) => graph + ``` + + + + Optional. One of `'flat'`, `'top-down'`, or `'bottom-up'`. Default: `'flat'`. + + + An object whose structure depends on the requested view. See [the graph walkthrough](../../../walkthrough/the-graph#getting-the-graph). + + + + + + Returns a list of [EvaluationReasons](/not-done?path=../types/EvaluationReason) detailing why the current atom instance or selector is evaluating. + + If called outside a selector or atom state factory, `why` always returns `undefined`. + + If this is the first evaluation of the current atom instance or selector, `why` returns an empty array. + + + + Runs a callback in a scoped context. This is the only way to create or retrieve [scoped atoms](/not-done?path=../glossary#scoped-atom) outside React components. + + A "scope" is a JS Map mapping "contexts" to values. "Context" simultaneously means two completely different things: + + - A function execution (this is what "scoped context" is referring to). + - A stable object reference dynamically associated with a value, e.g. a React context object or an [atom template](/not-done?path=./AtomTemplate). + + Scopes are recursive - nested `withScope` calls will recursively look for context values in inner -> outer scopes. + + Signature: + + ```ts + withScope = (scope, callback) => void + ``` + + + + Required. A JS Map mapping context objects (e.g. React contexts or [atom templates](/not-done?path=./AtomTemplate)) to the provided values. + + + Required. The callback function to run in the scoped context. Any [`ecosystem.get`](#get) or [`ecosystem.getNode`](#getnode) calls in the callback will be able to create and retrieve scoped atoms that depend on the provided scope. + + + The callback's result. Can be anything. + + + + + + +## See Also + +- [the Ecosystems walkthrough](../../../walkthrough/ecosystems) +- [the `createEcosystem()` factory](/not-done?path=../factories/createEcosystem) +- [the Overrides walkthrough](../../../walkthrough/overrides) +- [the Plugins guide](../../../advanced/plugins) +- [`getDefaultEcosystem()`](/not-done?path=../utils/getDefaultEcosystem) +- [`setDefaultEcosystem()`](/not-done?path=../utils/setDefaultEcosystem) +- [the `` component](/not-done?path=../components/EcosystemProvider) +- [the `useEcosystem` hook](/not-done?path=../hooks/useEcosystem) +- [the `injectEcosystem` injector](/not-done?path=../injectors/injectEcosystem) diff --git a/docs/docs/v2/api/classes/Signal.mdx b/docs/docs/v2/api/classes/Signal.mdx index c975d4f0..0316f401 100644 --- a/docs/docs/v2/api/classes/Signal.mdx +++ b/docs/docs/v2/api/classes/Signal.mdx @@ -41,11 +41,11 @@ const counterSignal = myEcosystem.signal(0) Unlike `injectSignal`, `ecosystem.signal` can be used in loops and if statements. It's useful for creating dynamic lists of signals e.g. in response to state changes. The tradeoff is that you have to manage the signal's lifecycle manually. -See [`Ecosystem#signal`](/not-done?path=./Ecosystem#signal) +See [`Ecosystem#signal`](./Ecosystem#signal) ## Destruction -Signals are always destroyed when they have no more observers. When created via [`injectSignal`](../injectors/injectSignal), this typically means the signal will be destroyed when the injecting atom is destroyed. When created via [`ecosystem.signal`](/not-done?path=./Ecosystem#signal), you must either manually add observers or destroy the signal when you're done with it. +Signals are always destroyed when they have no more observers. When created via [`injectSignal`](../injectors/injectSignal), this typically means the signal will be destroyed when the injecting atom is destroyed. When created via [`ecosystem.signal`](./Ecosystem#signal), you must either manually add observers or destroy the signal when you're done with it. ```ts const exampleAtom = atom('example', () => { diff --git a/docs/docs/v2/api/classes/ZeduxNode.mdx b/docs/docs/v2/api/classes/ZeduxNode.mdx index fea074c4..d39274a8 100644 --- a/docs/docs/v2/api/classes/ZeduxNode.mdx +++ b/docs/docs/v2/api/classes/ZeduxNode.mdx @@ -165,7 +165,7 @@ Every node has the following **readonly** properties: A string. The unique id of this node. Zedux always tries to make this somewhat human-readable for easier debugging. - For nodes that take params, the id is the combination of the node template's key and a deterministic stringification of the params. A node template's "key" can the string passed to the [`atom()`](/not-done?path=../factories/atom) factory for atoms or the selector function's name for selectors See [`ecosystem.hash()`](/not-done?path=./Ecosystem#hash) or [`node.params`](#params) for more details on how params are stringified. + For nodes that take params, the id is the combination of the node template's key and a deterministic stringification of the params. A node template's "key" can the string passed to the [`atom()`](/not-done?path=../factories/atom) factory for atoms or the selector function's name for selectors See [`ecosystem.hash()`](./Ecosystem#hash) or [`node.params`](#params) for more details on how params are stringified. ```ts ecosystem.getNode(atom('a', null)).id // 'a' @@ -204,7 +204,7 @@ Every node has the following **readonly** properties: useAtomInstance(myAtom, ['b', 'a']) ``` - The only exception to the serializable rule is other atom instances. That's right! You can pass an atom instance to another atom instance. You can then use [`instance.get()`](#get) or [`ecosystem.get(instance)`](/not-done?path=./Ecosystem#get), [`injectAtomValue()`](/not-done?path=../injectors/injectAtomValue), or any other dynamic injector to register a [dynamic graph dependency](/not-done?path=../glossary#dynamic-graph-dependency) on the passed atom instance. + The only exception to the serializable rule is other atom instances. That's right! You can pass an atom instance to another atom instance. You can then use [`instance.get()`](#get) or [`ecosystem.get(instance)`](./Ecosystem#get), [`injectAtomValue()`](/not-done?path=../injectors/injectAtomValue), or any other dynamic injector to register a [dynamic graph dependency](/not-done?path=../glossary#dynamic-graph-dependency) on the passed atom instance. ```tsx live ecosystemId=AtomInstance-params resultVar=Shout version=2 const normalAtom = atom( @@ -237,7 +237,7 @@ function Shout() { src={useBaseUrl('/img/diagrams/status-machine.png')} /> - This is mostly for debugging, but it has some practical uses. For example, you can check that `node.status !== 'Destroyed'` when holding a reference to a node outside of React/atoms (and if it is, update your local reference using [`ecosystem.getNode()`](/not-done?path=./Ecosystem#getnode)). + This is mostly for debugging, but it has some practical uses. For example, you can check that `node.status !== 'Destroyed'` when holding a reference to a node outside of React/atoms (and if it is, update your local reference using [`ecosystem.getNode()`](./Ecosystem#getnode)). @@ -336,7 +336,7 @@ Every node has the following methods: ## See Also -- [`ecosystem.getNode()`](/not-done?path=./Ecosystem#getnode) +- [`ecosystem.getNode()`](./Ecosystem#getnode) - [the `AtomInstance` class](./AtomInstance) - [the `SelectorInstance` class](/not-done?path=./SelectorInstance) - [the `Signal` class](./Signal) diff --git a/docs/sidebars.js b/docs/sidebars.js index a42581a1..a480991d 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -166,6 +166,7 @@ module.exports = { label: 'Classes', items: [ 'v2/api/classes/AtomInstance', + 'v2/api/classes/Ecosystem', 'v2/api/classes/Signal', 'v2/api/classes/ZeduxNode', ], From 7399aa84c84425330a85e9149ee61f350e58a3d4 Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Thu, 24 Jul 2025 09:40:20 -0400 Subject: [PATCH 06/49] docs: document `AtomTemplate` class for v2 --- docs/docs/v2/api/classes/AtomInstance.mdx | 11 +- docs/docs/v2/api/classes/AtomTemplate.mdx | 153 ++++++++++++++++++++++ docs/docs/v2/api/classes/Ecosystem.mdx | 12 +- docs/docs/v2/api/classes/ZeduxNode.mdx | 2 +- docs/sidebars.js | 1 + 5 files changed, 166 insertions(+), 13 deletions(-) create mode 100644 docs/docs/v2/api/classes/AtomTemplate.mdx diff --git a/docs/docs/v2/api/classes/AtomInstance.mdx b/docs/docs/v2/api/classes/AtomInstance.mdx index 324f64f8..eed88aee 100644 --- a/docs/docs/v2/api/classes/AtomInstance.mdx +++ b/docs/docs/v2/api/classes/AtomInstance.mdx @@ -5,7 +5,7 @@ title: AtomInstance import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' -All standard atoms (aka "atom instances") are actually instances of this class. When Zedux instantiates [atom templates](/not-done?path=./AtomTemplate) (and [ion templates](/not-done?path=./IonTemplate)), it's just creating instances of this class. +All standard atoms (aka "atom instances") are actually instances of this class. When Zedux instantiates [atom templates](./AtomTemplate) (and [ion templates](/not-done?path=./IonTemplate)), it's just creating instances of this class. This class extends the [`Signal` class](./Signal) (yes, atoms _are_ signals) which in turn extends the [`ZeduxNode` class](./ZeduxNode) (all atoms/signals are graph nodes). @@ -64,10 +64,10 @@ Configuring an atom's TTL (Time To Live) is the recommended way to manage its li You can configure this in two ways: -- Setting the atom template's [`ttl` config](/not-done?path=./AtomTemplate#ttl). This is suitable for most cases, but only accepts a number in milliseconds. +- Setting the atom template's [`ttl` config](./AtomTemplate#ttl). This is suitable for most cases, but only accepts a number in milliseconds. - Returning an [atom api](/not-done?path=./AtomApi) from the atom's state factory that configures a TTL via [`.setTtl()`](/not-done?path=./AtomApi#setttl). This is much more powerful, accepting a number, promise, observable, or a function that returns a number, promise, or observable. - Setting TTL via returning a configured [atom api](/not-done?path=./AtomApi) overrides any TTL configured on the [atom template](/not-done?path=./AtomTemplate#ttl). It also allows you to configure a different TTL for each instance of the atom. + Setting TTL via returning a configured [atom api](/not-done?path=./AtomApi) overrides any TTL configured on the [atom template](./AtomTemplate#ttl). It also allows you to configure a different TTL for each instance of the atom. The sky's the limit. With TTL, you can destroy atoms on page route, on log out, when the cache reaches a certain size, or anything else you can think of. @@ -281,8 +281,7 @@ Atom instances also inherit the following properties from [`ZeduxNode`](./ZeduxN See [`ZeduxNode#status`](./ZeduxNode#status). See [`ZeduxNode#template`](./ZeduxNode#template). Will always be a reference - to the [atom template](/not-done?path=./AtomTemplate) this instance was - created from. + to the [atom template](./AtomTemplate) this instance was created from. @@ -345,5 +344,5 @@ Atom instances also inherit the following methods from [`ZeduxNode`](./ZeduxNode - [The Atom Instances walkthrough](/not-done?path=../../../walkthrough/atom-instances) - [The `Signal` class](./Signal) - [The `ZeduxNode` class](./ZeduxNode) -- [The `AtomTemplate` class](/not-done?path=./AtomTemplate) +- [The `AtomTemplate` class](./AtomTemplate) - [The `AtomApi` class](/not-done?path=./AtomApi) diff --git a/docs/docs/v2/api/classes/AtomTemplate.mdx b/docs/docs/v2/api/classes/AtomTemplate.mdx new file mode 100644 index 00000000..7813ebf3 --- /dev/null +++ b/docs/docs/v2/api/classes/AtomTemplate.mdx @@ -0,0 +1,153 @@ +--- +id: AtomTemplate +title: AtomTemplate +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +export const AtomKeyDesc = () => ( + <> +

+ The key will be used to create a unique id for each atom instance. It is + also the key to Dependency Injection - it's how ecosystems know which atom + templates to override. +

+

+ Keys also improve DX - many errors will log the key of the atom they + originated in. Keys also help with a codebase's grepability. +

+ +) + +The object returned from [the `atom()` factory](/not-done?path=../factories/atom). Instances of this class are passed to most of Zedux's hooks and injectors. + +An atom template defines a skeleton that Zedux will use to create [atom instances](./AtomInstance) on demand. + +## Creation + +Use [the `atom()` factory](/not-done?path=../factories/atom) to create atom templates: + +```ts +import { AtomTemplate, atom } from '@zedux/react' + +const exampleAtom = atom('example', 'initial state') + +exampleAtom instanceof AtomTemplate // true +``` + +:::note +The `_____Atom` naming convention comes from Jotai. If it's confusing (templates aren't atoms after all...), you don't have to use it. Feel free to create your own. + +For example, here's a convention using `PascalCase$` for templates and `camelCase$` for instances: + +```ts +const Example$ = atom('example', 'initial state') +const example$ = ecosystem.getNode(Example$) + +example$.get() // 'initial state' +``` + +::: + +## Extending + +When creating your own, custom atom types, you'll usually want to extend this class. Creating your own atom types is an advanced feature and we're not currently documenting it as the internals of these classes may change. + +## Properties + + + + + A function. Can be undefined. + + A reference to the [`dehydrate` atom config option](/not-done?path=../types/AtomConfig#dehydrate) passed to [the `atom()` factory](/not-done?path=../factories/atom), if any. + + + + An array of strings. Can be undefined. + + A reference to the [`tags` atom config option](/not-done?path=../types/AtomConfig#tags) passed to [the `atom()` factory](/not-done?path=../factories/atom), if any. + + If the ecosystem has [tags configured](./Ecosystem#tags), these tags will be checked against the ecosystem's to warn about unsafe atom templates being used in certain environments. + + + + A function. Can be undefined. + + A reference to the [`hydrate` atom config option](/not-done?path=../types/AtomConfig#hydrate) passed to [`atom()`](/not-done?path=../factories/atom), if any. + + + + A string. + + This is the key string passed as the first argument to [the `atom()` factory](/not-done?path=../factories/atom). + + + + + + A number. Can be undefined. + + This is the [`ttl` atom config option](/not-done?path=../types/AtomConfig#ttl) passed to [the `atom()` factory](/not-done?path=../factories/atom), if any. + + If not set, instances of this atom will live forever unless configured with [`.setTtl()`](/not-done?path=./AtomApi#setttl) on an AtomApi returned by the state factory. + + - Set to -1 to make this atom's instances live forever. + - Set to 0 to destroy all instances of this atom as soon as they go stale. + - Set to any positive integer to make atoms live in a stale state for{' '} + <ttl> milliseconds before being cleaned up. + + If set, this option will still be overridden by any `ttl` [set on an AtomApi](/not-done?path=./AtomApi#setttl) returned by the state factory. + + + + +## Methods + + + + + Gets the id that an instance of this atom template would have given a set of params. + + Signature: + + ```ts + getNodeId = (ecosystem, params?) => id + ``` + + + + Required. Your [ecosystem](./Ecosystem). + + + Optional (required if the atom takes params). An array of atom params that will be turned into a predictable hash. + + See [`ecosystem.hash()`](./Ecosystem#hash) and [`instance.params`](./AtomInstance#params) for more. + + + + Accepts an ecosystem and an array of atom params (required if the atom takes params, `undefined` if not). Uses the ecosystem's id generator to generate a predictable hash of the atom template's key + params combo. + + Zedux uses this internally to generate atom instance ids. You won't typically call this except in some advanced use cases. To find a cached atom instance via its template + params, prefer [`ecosystem.find()`](./Ecosystem#find). + + + + Creates a clone of this atom template, but with a different value. + + Signature: + + ```ts + override = (newValue) => newAtom + ``` + + Accepts any of the same [value types](/not-done?path=../factories/atom#value) that [the `atom()` factory](/not-done?path=../factories/atom) accepts. The state, promise, and exports type of the new value should match the corresponding types in the overridden atom. TypeScript will enforce this. + + Returns the new atom template. See the [overrides walkthrough](../../../walkthrough/overrides) for more details. + + + + +## See Also + +- [`atom()`](/not-done?path=../factories/atom) +- [The Configuring Atoms walkthrough](../../../walkthrough/configuring-atoms) diff --git a/docs/docs/v2/api/classes/Ecosystem.mdx b/docs/docs/v2/api/classes/Ecosystem.mdx index ded6cdf0..b7d83ede 100644 --- a/docs/docs/v2/api/classes/Ecosystem.mdx +++ b/docs/docs/v2/api/classes/Ecosystem.mdx @@ -475,7 +475,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context ``` - Required. An array of [atom templates](/not-done?path=./AtomTemplate). + Required. An array of [atom templates](./AtomTemplate).
@@ -665,7 +665,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context - Required. An [atom template](/not-done?path=./AtomTemplate), [selector template](/not-done?path=../types/SelectorTemplate), or atom instance. + Required. An [atom template](./AtomTemplate), [selector template](/not-done?path=../types/SelectorTemplate), or atom instance. Optional (required if the template takes params). An array of the atom or selector's params. Only relevant when passing an atom or selector template. @@ -740,7 +740,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context - Required. An [atom template](/not-done?path=./AtomTemplate), [selector template](/not-done?path=../types/SelectorTemplate), or [atom instance](./AtomInstance). + Required. An [atom template](./AtomTemplate), [selector template](/not-done?path=../types/SelectorTemplate), or [atom instance](./AtomInstance). When an instance is passed, `getNode` registers a static graph dependency on that instance (when called during evaluation) and returns it as-is. @@ -928,7 +928,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context - Required. An array of [atom templates](/not-done?path=./AtomTemplate) and/or template key strings. If any haven't been set as overrides previously, they'll be ignored. + Required. An array of [atom templates](./AtomTemplate) and/or template key strings. If any haven't been set as overrides previously, they'll be ignored. @@ -1076,7 +1076,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context A "scope" is a JS Map mapping "contexts" to values. "Context" simultaneously means two completely different things: - A function execution (this is what "scoped context" is referring to). - - A stable object reference dynamically associated with a value, e.g. a React context object or an [atom template](/not-done?path=./AtomTemplate). + - A stable object reference dynamically associated with a value, e.g. a React context object or an [atom template](./AtomTemplate). Scopes are recursive - nested `withScope` calls will recursively look for context values in inner -> outer scopes. @@ -1088,7 +1088,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context - Required. A JS Map mapping context objects (e.g. React contexts or [atom templates](/not-done?path=./AtomTemplate)) to the provided values. + Required. A JS Map mapping context objects (e.g. React contexts or [atom templates](./AtomTemplate)) to the provided values. Required. The callback function to run in the scoped context. Any [`ecosystem.get`](#get) or [`ecosystem.getNode`](#getnode) calls in the callback will be able to create and retrieve scoped atoms that depend on the provided scope. diff --git a/docs/docs/v2/api/classes/ZeduxNode.mdx b/docs/docs/v2/api/classes/ZeduxNode.mdx index d39274a8..7fed061d 100644 --- a/docs/docs/v2/api/classes/ZeduxNode.mdx +++ b/docs/docs/v2/api/classes/ZeduxNode.mdx @@ -243,7 +243,7 @@ function Shout() { A reference to the template this node was created from. `undefined` if none. Only [atom instances](./AtomInstance) and [selector instances](/not-done?path=./SelectorInstance) have templates. - For atom instances, this will be the [atom template](/not-done?path=./AtomTemplate). For selector instances, this will be the [selector template](/not-done?path=../types/SelectorTemplate). + For atom instances, this will be the [atom template](./AtomTemplate). For selector instances, this will be the [selector template](/not-done?path=../types/SelectorTemplate). diff --git a/docs/sidebars.js b/docs/sidebars.js index a480991d..22177046 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -166,6 +166,7 @@ module.exports = { label: 'Classes', items: [ 'v2/api/classes/AtomInstance', + 'v2/api/classes/AtomTemplate', 'v2/api/classes/Ecosystem', 'v2/api/classes/Signal', 'v2/api/classes/ZeduxNode', From 8feebc54199ef0b4998d125062cd26cc035d6731 Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Thu, 24 Jul 2025 09:51:33 -0400 Subject: [PATCH 07/49] docs: document glossary for v2 --- docs/docs/v2/api/classes/Ecosystem.mdx | 16 +-- docs/docs/v2/api/classes/ZeduxNode.mdx | 8 +- docs/docs/v2/api/glossary.mdx | 107 ++++++++++++++++++++ docs/docs/v2/api/injectors/injectSignal.mdx | 6 +- docs/sidebars.js | 1 + 5 files changed, 123 insertions(+), 15 deletions(-) create mode 100644 docs/docs/v2/api/glossary.mdx diff --git a/docs/docs/v2/api/classes/Ecosystem.mdx b/docs/docs/v2/api/classes/Ecosystem.mdx index b7d83ede..33f85af3 100644 --- a/docs/docs/v2/api/classes/Ecosystem.mdx +++ b/docs/docs/v2/api/classes/Ecosystem.mdx @@ -624,7 +624,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context `get` is reactive by default. This behavior changes depending on when it's called: - - When called in a [reactive context](/not-done?path=../glossary#reactive-context) (e.g. while an atom or selector is evaluating), `get` registers a [dynamic graph dependency](/not-done?path=../glossary#dynamic-graph-dependency) between the currently-evaluating node and the retrieved node. + - When called in a [reactive context](../glossary#reactive-context) (e.g. while an atom or selector is evaluating), `get` registers a [dynamic graph dependency](../glossary#dynamic-graph-dependency) between the currently-evaluating node and the retrieved node. - When called outside evaluation (e.g. in an effect or callback), `get` doesn't register any graph dependencies. Note that [`node.get()`](./ZeduxNode#get) has this exact same behavior. In fact, `ecosystem.get(myAtom)` is almost a shorthand for `ecosystem.getNode(myAtom).get()`, with one key difference: @@ -677,7 +677,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context - Gets a cached [graph node](/not-done?path=../glossary#graph-node), namely an [atom instance](./AtomInstance) or [selector instance](/not-done?path=./SelectorInstance). Creates and caches the node if it doesn't exist yet. + Gets a cached [graph node](../glossary#graph-node), namely an [atom instance](./AtomInstance) or [selector instance](/not-done?path=./SelectorInstance). Creates and caches the node if it doesn't exist yet. ```ts const { getNode } = myEcosystem // `getNode` can be destructured like this @@ -698,7 +698,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context `getNode` registers graph dependencies by default. This behavior changes depending on when it's called: - - When called in a [reactive context](/not-done?path=../glossary#reactive-context) (e.g. while an atom or selector is evaluating), `getNode` registers a [static graph dependency](/not-done?path=../glossary#static-graph-dependency) (unlike [`ecosystem.get`](#get), which registers dynamic dependencies) between the currently-evaluating node and the retrieved node. + - When called in a [reactive context](../glossary#reactive-context) (e.g. while an atom or selector is evaluating), `getNode` registers a [static graph dependency](../glossary#static-graph-dependency) (unlike [`ecosystem.get`](#get), which registers dynamic dependencies) between the currently-evaluating node and the retrieved node. - When called outside evaluation (e.g. in an effect or callback), `getNode` doesn't register any graph dependencies. #### Note on Caching @@ -754,7 +754,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context - Returns a [graph node](/not-done?path=../glossary#graph-node). Functions exactly like [`ecosystem.getNode`](#getnode) except it never registers graph dependencies even when called in [reactive contexts](/not-done?path=../glossary#reactive-context). + Returns a [graph node](../glossary#graph-node). Functions exactly like [`ecosystem.getNode`](#getnode) except it never registers graph dependencies even when called in [reactive contexts](../glossary#reactive-context). Signature: @@ -766,7 +766,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context - Returns the current value of the resolved atom or selector node. Functions exactly like [`ecosystem.get`](#get) except it never registers graph dependencies even when called in [reactive contexts](/not-done?path=../glossary#reactive-context). + Returns the current value of the resolved atom or selector node. Functions exactly like [`ecosystem.get`](#get) except it never registers graph dependencies even when called in [reactive contexts](../glossary#reactive-context). Signature: @@ -804,7 +804,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context Required. An array of anything. - If any item is a [graph node](/not-done?path=../glossary#graph-node), it will be serialized as the node's id. + If any item is a [graph node](../glossary#graph-node), it will be serialized as the node's id. Optional. A boolean. Defaults to the ecosystem's [`complexParams` config](#complexparams). @@ -934,7 +934,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context - Force destroys all [graph nodes](/not-done?path=../glossary#graph-node) in the ecosystem. Calls the cleanup function returned from the [onReady](/not-done?path=../types/EcosystemConfig#onready) function (if any), and calls `onReady` again to reinitialize the ecosystem. + Force destroys all [graph nodes](../glossary#graph-node) in the ecosystem. Calls the cleanup function returned from the [onReady](/not-done?path=../types/EcosystemConfig#onready) function (if any), and calls `onReady` again to reinitialize the ecosystem. Accepts several options to also clear cached hydrations, listeners, and overrides. @@ -1071,7 +1071,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context - Runs a callback in a scoped context. This is the only way to create or retrieve [scoped atoms](/not-done?path=../glossary#scoped-atom) outside React components. + Runs a callback in a scoped context. This is the only way to create or retrieve [scoped atoms](../glossary#scoped-atom) outside React components. A "scope" is a JS Map mapping "contexts" to values. "Context" simultaneously means two completely different things: diff --git a/docs/docs/v2/api/classes/ZeduxNode.mdx b/docs/docs/v2/api/classes/ZeduxNode.mdx index 7fed061d..e9d8acd8 100644 --- a/docs/docs/v2/api/classes/ZeduxNode.mdx +++ b/docs/docs/v2/api/classes/ZeduxNode.mdx @@ -6,7 +6,7 @@ title: ZeduxNode import useBaseUrl from '@docusaurus/useBaseUrl' import { Legend, Item, Link, Tabs, tab1, tab2 } from '@site/src/all' -This is the base class for every [graph node](/not-done?path=../glossary#graph-node). The [AtomInstance class](./AtomInstance), [SelectorInstance class](/not-done?path=./SelectorInstance), [Signal class](./Signal), and several other graph node types extend this class. +This is the base class for every [graph node](../glossary#graph-node). The [AtomInstance class](./AtomInstance), [SelectorInstance class](/not-done?path=./SelectorInstance), [Signal class](./Signal), and several other graph node types extend this class. You never create this class directly and should never need to reference it directly except as a type. @@ -204,7 +204,7 @@ Every node has the following **readonly** properties: useAtomInstance(myAtom, ['b', 'a']) ``` - The only exception to the serializable rule is other atom instances. That's right! You can pass an atom instance to another atom instance. You can then use [`instance.get()`](#get) or [`ecosystem.get(instance)`](./Ecosystem#get), [`injectAtomValue()`](/not-done?path=../injectors/injectAtomValue), or any other dynamic injector to register a [dynamic graph dependency](/not-done?path=../glossary#dynamic-graph-dependency) on the passed atom instance. + The only exception to the serializable rule is other atom instances. That's right! You can pass an atom instance to another atom instance. You can then use [`instance.get()`](#get) or [`ecosystem.get(instance)`](./Ecosystem#get), [`injectAtomValue()`](/not-done?path=../injectors/injectAtomValue), or any other dynamic injector to register a [dynamic graph dependency](../glossary#dynamic-graph-dependency) on the passed atom instance. ```tsx live ecosystemId=AtomInstance-params resultVar=Shout version=2 const normalAtom = atom( @@ -272,10 +272,10 @@ Every node has the following methods: - Gets the current value of the node. Registers a [dynamic graph dependency](/not-done?path=../glossary#dynamic-graph-dependency) on the node when called in [reactive contexts](/not-done?path=../glossary#reactive-context). + Gets the current value of the node. Registers a [dynamic graph dependency](../glossary#dynamic-graph-dependency) on the node when called in [reactive contexts](../glossary#reactive-context). - Gets the current value of the node. Unlike [`node.get()`], `getOnce` does not register any graph dependencies, even when called in [reactive contexts](/not-done?path=../glossary#reactive-context). + Gets the current value of the node. Unlike [`node.get()`], `getOnce` does not register any graph dependencies, even when called in [reactive contexts](../glossary#reactive-context). Attaches an event listener to the node. diff --git a/docs/docs/v2/api/glossary.mdx b/docs/docs/v2/api/glossary.mdx new file mode 100644 index 00000000..5740a2b8 --- /dev/null +++ b/docs/docs/v2/api/glossary.mdx @@ -0,0 +1,107 @@ +--- +id: glossary +title: Glossary +--- + +### Dynamic Graph Dependency + +When one [graph node](#graph-node) depends on another, Zedux draws an edge between those two nodes in its internal graph. + +A "dynamic" dependency is a dependency that will trigger updates in the dependent node when the dependency node's state updates. Contrast this to [static dependencies](#static-graph-dependency), which do not trigger updates. + +If the dependent is a React component, it will rerender when the dependency atom instance's state changes. + +If the dependent is another atom instance, it will reevaluate when the dependency atom instance's state changes. + +### Graph Edge + +The edges between [graph nodes](#graph-node). These edges can have several properties, depending on how the edge was created and how it should behave. + +Edges can be static or dynamic, internal or external, and async or synchronous. They can be identified by an "operation" string that helps when debugging. + +These can be created manually with [manual graphing](../../walkthrough/destruction#manual-graphing). + +### Graph Node + +Zedux builds an internal graph to manage atom dependencies and propagate updates in an optimal way. There are many types of nodes in this graph: + +- [Atom instances](./classes/AtomInstance) +- [Selector instances](/not-done?path=./classes/SelectorInstance) +- [Signals](./classes/Signal) +- [`injectMemo`](/not-done?path=./injectors/injectMemo) calls with no deps array (enabling automatic dependency tracking) +- "External" nodes created for React components and event listeners. + +Every node extends [the `ZeduxNode` class](./classes/ZeduxNode). + +### Injector + +Injectors are the "hooks" of Atoms. Zedux exports several injectors. + +There are 3 basic types of injectors: + +- React-hook equivalents, like [`injectEffect`](/not-done?path=./injectors/injectEffect), [`injectMemo`](/not-done?path=./injectors/injectMemo), and [`injectRef`](/not-done?path=./injectors/injectRef). +- Dependency injectors, like [`injectAtomValue`](/not-done?path=./injectors/injectAtomValue) and [`injectAtomInstance`](/not-done?path=./injectors/injectAtomInstance). +- Utility or dev X injectors, such as [`injectEcosystem`](/not-done?path=./injectors/injectEcosystem) and [`injectWhy`](/not-done?path=./injectors/injectWhy). + +Injectors should only be used at the top level of [atom state factories](#state-factory). Don't use them in loops or conditional statements. + +Injectors can be used any number of times throughout an atom state factory. For certain one-off operations like setting an atom instance's exports or setting a suspense promise, use an [AtomApi](/not-done?path=./classes/AtomApi). + +Like hooks, you can create custom injectors that compose other injectors. The convention is to start all injectors with the word "inject", similar to the word "use" with React hooks. + +### Reactive Context + +A function execution context in which Zedux automatically tracks dependencies. Atom state factories, selectors, and auto-tracked [`injectMemo`](/not-done?path=./injectors/injectMemo#auto-tracking) calls create reactive contexts. + +In a reactive context, any [`node.get()`](./classes/ZeduxNode#get) or [`ecosystem.get()`](./classes/Ecosystem#get) calls will register [dynamic graph dependencies](#dynamic-graph-dependency) on the retrieved node, and any [`ecosystem.getNode()`](./classes/Ecosystem#getnode) calls will register [static graph dependencies](#static-graph-dependency). This automatic dependency tracking is a staple in reactive libraries like Zedux. + +### Scope + +A group of contextual values. In Zedux, this is always represented with a JS Map mapping "context" objects (e.g. React context objects or Zedux [atom instances](./classes/AtomInstance)) to their values. E.g.: + +```tsx live ecosystemId=scope-example resultVar=val version=2 +const ecosystem = createEcosystem() +const exampleReactContext = React.createContext(undefined) +const contextAtom = atom('example', () => 'atom state') + +const exampleScope = new Map([ + // react contexts get mapped to their provided values + [exampleReactContext, 'react state'], + + // atom templates get mapped to their atom instances + [contextAtom, ecosystem.getNode(contextAtom)], +]) + +const scopedAtom = atom( + 'scoped', + () => inject(contextAtom).get() + ' ' + inject(exampleReactContext) +) + +const val = ecosystem.withScope(exampleScope, () => ecosystem.get(scopedAtom)) +``` + +### Scoped Atom + +An atom that contains one or more [`inject`](/not-done?path=./injectors/inject) calls. Such atoms must be called with ["scope"](#scope) - e.g. by providing contextual values via Provider components in React or by calling [`ecosystem.withScope`](/not-done?path=./classes/Ecoysstem#withscope) + +### State Factory + +A function passed to [`atom()`](/not-done?path=./factories/atom) (or other atom factory functions like [`ion()`](/not-done?path=./factories/ion)). This function is called to produce the initial value of the atom instance. It also runs every time an atom instance reevaluates. + +These are similar to render functions in React. Except of course they return state instead of UI. + +### Static Graph Dependency + +When one [graph node](#graph-node) depends on another, Zedux draws an edge between those two nodes in its internal graph algorithm. + +A "static" dependency is a dependency that does not trigger updates in the dependent node when the dependency node's state updates. Contrast this to [dynamic dependencies](#dynamic-graph-dependency), which do trigger updates. + +While they don't trigger updates, static dependencies are still useful for informing Zedux that an atom instance is in use. Zedux won't try to clean up atom instances that still have dependents. + +### Unrestricted Injector + +An [injector](#injector) whose use isn't restricted like normal injectors. An unrestricted injector still must be used inside an atom state factory (called synchronously during evaluation). However, unlike normal injectors, unrestricted injectors can be used in control flow statements (`if`, `for`, `while`) or after early returns. + +You usually won't need to worry about this distinction. Just use them like normal injectors and you'll be fine. + +Examples of unrestricted injectors include [`injectEcosystem()`](/not-done?path=./injectors/injectEcosystem), [`injectSelf()`](/not-done?path=./injectors/injectSelf), and [`injectWhy()`](/not-done?path=./injectors/injectWhy). diff --git a/docs/docs/v2/api/injectors/injectSignal.mdx b/docs/docs/v2/api/injectors/injectSignal.mdx index 30ede6eb..17135dec 100644 --- a/docs/docs/v2/api/injectors/injectSignal.mdx +++ b/docs/docs/v2/api/injectors/injectSignal.mdx @@ -5,9 +5,9 @@ title: injectSignal import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' -An [injector](/not-done?path=../glossary#injector) that creates and returns a stable [`Signal`](../classes/Signal) instance. The reference will never change for the lifetime of the injecting atom. +An [injector](../glossary#injector) that creates and returns a stable [`Signal`](../classes/Signal) instance. The reference will never change for the lifetime of the injecting atom. -Registers a [dynamic graph dependency](/not-done?path=../glossary#dynamic-graph-dependency) on the injected signal. By default, every state change in the injected signal will trigger a reevaluation of the current atom. +Registers a [dynamic graph dependency](../glossary#dynamic-graph-dependency) on the injected signal. By default, every state change in the injected signal will trigger a reevaluation of the current atom. Unless you register other dependencies, the injecting atom will typically be the only observer of the injected signal. That means the signal will be destroyed when the injecting atom is destroyed. @@ -69,7 +69,7 @@ const passingGenerics = injectSignal([]) A boolean. Default: `true`. - Pass `false` to prevent the injecting atom from registering a [dynamic graph dependency](/not-done?path=../glossary#dynamic-graph-dependency) on the injected signal. The atom will still register a [static graph dependency](/not-done?path=../glossary#static-graph-dependency) on the injected signal. + Pass `false` to prevent the injecting atom from registering a [dynamic graph dependency](../glossary#dynamic-graph-dependency) on the injected signal. The atom will still register a [static graph dependency](../glossary#static-graph-dependency) on the injected signal. diff --git a/docs/sidebars.js b/docs/sidebars.js index 22177046..825cb520 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -177,6 +177,7 @@ module.exports = { label: 'Injectors', items: ['v2/api/injectors/injectSignal'], }, + 'v2/api/glossary', ], }, }, From 08ab6e6f5ef2a477119499232507112ce05bd9f0 Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Thu, 24 Jul 2025 10:02:45 -0400 Subject: [PATCH 08/49] docs: document `atom` factory for v2 --- docs/docs/v2/api/classes/AtomTemplate.mdx | 18 +- docs/docs/v2/api/classes/Signal.mdx | 2 +- docs/docs/v2/api/classes/ZeduxNode.mdx | 2 +- docs/docs/v2/api/factories/atom.mdx | 235 ++++++++++++++++++++++ docs/docs/v2/api/glossary.mdx | 2 +- docs/sidebars.js | 5 + 6 files changed, 252 insertions(+), 12 deletions(-) create mode 100644 docs/docs/v2/api/factories/atom.mdx diff --git a/docs/docs/v2/api/classes/AtomTemplate.mdx b/docs/docs/v2/api/classes/AtomTemplate.mdx index 7813ebf3..5a4231fd 100644 --- a/docs/docs/v2/api/classes/AtomTemplate.mdx +++ b/docs/docs/v2/api/classes/AtomTemplate.mdx @@ -19,13 +19,13 @@ export const AtomKeyDesc = () => ( ) -The object returned from [the `atom()` factory](/not-done?path=../factories/atom). Instances of this class are passed to most of Zedux's hooks and injectors. +The object returned from [the `atom()` factory](../factories/atom). Instances of this class are passed to most of Zedux's hooks and injectors. An atom template defines a skeleton that Zedux will use to create [atom instances](./AtomInstance) on demand. ## Creation -Use [the `atom()` factory](/not-done?path=../factories/atom) to create atom templates: +Use [the `atom()` factory](../factories/atom) to create atom templates: ```ts import { AtomTemplate, atom } from '@zedux/react' @@ -60,13 +60,13 @@ When creating your own, custom atom types, you'll usually want to extend this cl A function. Can be undefined. - A reference to the [`dehydrate` atom config option](/not-done?path=../types/AtomConfig#dehydrate) passed to [the `atom()` factory](/not-done?path=../factories/atom), if any. + A reference to the [`dehydrate` atom config option](/not-done?path=../types/AtomConfig#dehydrate) passed to [the `atom()` factory](../factories/atom), if any. An array of strings. Can be undefined. - A reference to the [`tags` atom config option](/not-done?path=../types/AtomConfig#tags) passed to [the `atom()` factory](/not-done?path=../factories/atom), if any. + A reference to the [`tags` atom config option](/not-done?path=../types/AtomConfig#tags) passed to [the `atom()` factory](../factories/atom), if any. If the ecosystem has [tags configured](./Ecosystem#tags), these tags will be checked against the ecosystem's to warn about unsafe atom templates being used in certain environments. @@ -74,13 +74,13 @@ When creating your own, custom atom types, you'll usually want to extend this cl A function. Can be undefined. - A reference to the [`hydrate` atom config option](/not-done?path=../types/AtomConfig#hydrate) passed to [`atom()`](/not-done?path=../factories/atom), if any. + A reference to the [`hydrate` atom config option](/not-done?path=../types/AtomConfig#hydrate) passed to [`atom()`](../factories/atom), if any. A string. - This is the key string passed as the first argument to [the `atom()` factory](/not-done?path=../factories/atom). + This is the key string passed as the first argument to [the `atom()` factory](../factories/atom). @@ -88,7 +88,7 @@ When creating your own, custom atom types, you'll usually want to extend this cl A number. Can be undefined. - This is the [`ttl` atom config option](/not-done?path=../types/AtomConfig#ttl) passed to [the `atom()` factory](/not-done?path=../factories/atom), if any. + This is the [`ttl` atom config option](/not-done?path=../types/AtomConfig#ttl) passed to [the `atom()` factory](../factories/atom), if any. If not set, instances of this atom will live forever unless configured with [`.setTtl()`](/not-done?path=./AtomApi#setttl) on an AtomApi returned by the state factory. @@ -140,7 +140,7 @@ When creating your own, custom atom types, you'll usually want to extend this cl override = (newValue) => newAtom ``` - Accepts any of the same [value types](/not-done?path=../factories/atom#value) that [the `atom()` factory](/not-done?path=../factories/atom) accepts. The state, promise, and exports type of the new value should match the corresponding types in the overridden atom. TypeScript will enforce this. + Accepts any of the same [value types](../factories/atom#value) that [the `atom()` factory](../factories/atom) accepts. The state, promise, and exports type of the new value should match the corresponding types in the overridden atom. TypeScript will enforce this. Returns the new atom template. See the [overrides walkthrough](../../../walkthrough/overrides) for more details. @@ -149,5 +149,5 @@ When creating your own, custom atom types, you'll usually want to extend this cl ## See Also -- [`atom()`](/not-done?path=../factories/atom) +- [`atom()`](../factories/atom) - [The Configuring Atoms walkthrough](../../../walkthrough/configuring-atoms) diff --git a/docs/docs/v2/api/classes/Signal.mdx b/docs/docs/v2/api/classes/Signal.mdx index 0316f401..3c477bbd 100644 --- a/docs/docs/v2/api/classes/Signal.mdx +++ b/docs/docs/v2/api/classes/Signal.mdx @@ -7,7 +7,7 @@ import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' The object returned by [`injectSignal`](../injectors/injectSignal). Signals are reactive state containers. Each signal holds a value and provides methods for accessing and updating that value. In Zedux, the term "signal" refers to an instance of this class. -Atoms themselves are signals. That simply means the [AtomInstance](./AtomInstance) class extend this class. An atom can also contain many inner signals. When a signal is returned from an [atom state factory](/not-done?path=../factories/atom#valueorfactory), the atom becomes a thin wrapper around the returned signal. +Atoms themselves are signals. That simply means the [AtomInstance](./AtomInstance) class extend this class. An atom can also contain many inner signals. When a signal is returned from an [atom state factory](../factories/atom#valueorfactory), the atom becomes a thin wrapper around the returned signal. [Mapped signals](/not-done?path=./MappedSignal) are also signals themselves. diff --git a/docs/docs/v2/api/classes/ZeduxNode.mdx b/docs/docs/v2/api/classes/ZeduxNode.mdx index e9d8acd8..9476a886 100644 --- a/docs/docs/v2/api/classes/ZeduxNode.mdx +++ b/docs/docs/v2/api/classes/ZeduxNode.mdx @@ -165,7 +165,7 @@ Every node has the following **readonly** properties: A string. The unique id of this node. Zedux always tries to make this somewhat human-readable for easier debugging. - For nodes that take params, the id is the combination of the node template's key and a deterministic stringification of the params. A node template's "key" can the string passed to the [`atom()`](/not-done?path=../factories/atom) factory for atoms or the selector function's name for selectors See [`ecosystem.hash()`](./Ecosystem#hash) or [`node.params`](#params) for more details on how params are stringified. + For nodes that take params, the id is the combination of the node template's key and a deterministic stringification of the params. A node template's "key" can the string passed to the [`atom()`](../factories/atom) factory for atoms or the selector function's name for selectors See [`ecosystem.hash()`](./Ecosystem#hash) or [`node.params`](#params) for more details on how params are stringified. ```ts ecosystem.getNode(atom('a', null)).id // 'a' diff --git a/docs/docs/v2/api/factories/atom.mdx b/docs/docs/v2/api/factories/atom.mdx new file mode 100644 index 00000000..160bb9b7 --- /dev/null +++ b/docs/docs/v2/api/factories/atom.mdx @@ -0,0 +1,235 @@ +--- +id: atom +title: atom +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' +import { AtomKeyDesc } from '../classes/AtomTemplate.mdx' + +```ts +import { atom } from '@zedux/react' +``` + +Where it all starts. `atom()` is a factory for creating atom templates. Zedux creates atoms from these templates as you use them in various hooks and injectors. + +An atom template is actually an instance of [the AtomTemplate class](../classes/AtomTemplate). + +## Example + +```tsx live ecosystemId=atom/example resultVar=App version=2 +const simpleAtom = atom('simple', 'Hello, world!') + +const complexAtom = atom( + 'complex', + () => { + const signal = injectSignal({ date: new Date() }) + + injectEffect(() => { + const intervalId = setInterval( + () => signal.set({ date: new Date() }), + 1000 + ) + + return () => clearInterval(intervalId) + }, []) + + return signal + }, + { + flags: ['side-effect'], + } +) + +function App() { + const simple = useAtomValue(simpleAtom) + const { date } = useAtomValue(complexAtom) + + return ( + <> +
simple state: {simple}
+
complex state: {date.toLocaleTimeString()}
+ + ) +} +``` + +## Signature + + + {tab1(`atom = (key, valueOrFactory, config?) => newAtom`)} + {tab2(`declare const atom: { + // Query Atoms + < + State = any, + Params extends any[] = [], + Exports extends Record = None + >( + key: string, + value: (...params: Params) => AtomApi<{ + Exports: Exports + Promise: any + Signal: undefined + State: Promise + }>, + config?: AtomConfig + ): AtomTemplateRecursive<{ + State: PromiseState + Params: Params + Events: None + Exports: Exports + Promise: Promise + }>\n + // Signals + < + StateType, + EventsType extends Record = None, + Params extends any[] = [], + Exports extends Record = None, + PromiseType extends AtomApiPromise = undefined, + ResolvedState = StateType + >( + key: string, + value: (...params: Params) => + | Signal<{ + Events: EventsType + Params: any + State: StateType + Template: any + }> + | AtomApi<{ + Exports: Exports + Promise: PromiseType + Signal: Signal<{ + Events: EventsType + Params: any + State: StateType + Template: any + }> + State: StateType + }> + | Signal<{ + Events: EventsType + Params: any + ResolvedState: ResolvedState + State: StateType + Template: any + }> + | AtomApi<{ + Exports: Exports + Promise: PromiseType + Signal: Signal<{ + Events: EventsType + Params: any + ResolvedState: ResolvedState + State: StateType + Template: any + }> + State: StateType + }>, + config?: AtomConfig + ): AtomTemplateRecursive<{ + State: StateType + Params: Params + Events: EventsType + Exports: Exports + Promise: PromiseType + ResolvedState: ResolvedState + }>\n + // Catch-all + < + State = any, + Params extends any[] = [], + Exports extends Record = None, + Events extends Record = None, + SignalType extends + | Signal<{ + Events: Events + Params: any + State: State + Template: any + }> + | undefined = undefined, + PromiseType extends AtomApiPromise = undefined + >( + key: string, + value: AtomValueOrFactory<{ + Exports: Exports + Params: Params + Promise: PromiseType + Signal: SignalType + State: State + }>, + config?: AtomConfig + ): AtomTemplateRecursive<{ + Events: Events + Exports: Exports + Params: Params + Promise: PromiseType + State: State + }> + } +`)} + + + + + + Required. A string. + + This key must be unique **except** when creating [atom overrides](../../../walkthrough/overrides). + + + + :::tip + Currently, Zedux leaves it up to you to ensure keys are unique across your codebase. As such, it's recommended to use a namespace-based naming convention - e.g. based on your project's file structure - to minimize the chance of conflicts. + + ```ts + const accountDetailsAtom = atom('dashboard/account/details', ...) + const registrationFormAtom = atom('signup/registrationForm', ...) + ``` + ::: + + + + Required. Can be any of the following: + + - A raw value. Can be anything except a function. When the atom is instantiated, this value as-is will be its initial state. + + - A state factory function that returns a raw value. That raw value can be anything (including a function). The returned value will be the atom instance's initial state. + + - A state factory function that returns a [signal](../classes/Signal). When the atom is instantiated, the new atom instance will become a thin wrapper around the returned signal, forwarding events and state changes between it and the atom's own observers. + + - A state factory function that returns an [AtomApi](/not-done?path=../classes/AtomApi) instance. + + The Atom API's value can be any of the following: + + - A raw value. Can be anything. This value will be the atom instance's initial state. + + - A signal. The atom instance will become a thin wrapper around the returned signal, forwarding events and state changes between it and the atom's own observers. + + - A promise. This will turn the atom into a [query atom](../../../walkthrough/query-atoms). + + The Atom API's exports will be set as the atom instance's `.exports`. + + The Atom API's promise will be set as the atom instance's `.promise`. + + Any [`ttl`](/not-done?path=../classes/AtomApi#ttl) configured in the returned Atom API will control the atom instance's destruction timing. + + + + Optional. An [AtomConfig](/not-done?path=../types/AtomConfig) object. + + + An [atom template](../classes/AtomTemplate). + + Zedux will manage creating and maintaining instances of the atom template as you use it in various hooks, injectors, and ecosystem methods. + + + + +## See Also + +- [The `AtomTemplate` class](../classes/AtomTemplate) +- [The `AtomApi` class](/not-done?path=../classes/AtomApi) +- [The Quick Start](../../../walkthrough/quick-start) +- [The Configuring Atoms walkthrough](../../../walkthrough/configuring-atoms) diff --git a/docs/docs/v2/api/glossary.mdx b/docs/docs/v2/api/glossary.mdx index 5740a2b8..93e5c5ea 100644 --- a/docs/docs/v2/api/glossary.mdx +++ b/docs/docs/v2/api/glossary.mdx @@ -86,7 +86,7 @@ An atom that contains one or more [`inject`](/not-done?path=./injectors/inject) ### State Factory -A function passed to [`atom()`](/not-done?path=./factories/atom) (or other atom factory functions like [`ion()`](/not-done?path=./factories/ion)). This function is called to produce the initial value of the atom instance. It also runs every time an atom instance reevaluates. +A function passed to [`atom()`](./factories/atom) (or other atom factory functions like [`ion()`](/not-done?path=./factories/ion)). This function is called to produce the initial value of the atom instance. It also runs every time an atom instance reevaluates. These are similar to render functions in React. Except of course they return state instead of UI. diff --git a/docs/sidebars.js b/docs/sidebars.js index 825cb520..823a9c46 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -177,6 +177,11 @@ module.exports = { label: 'Injectors', items: ['v2/api/injectors/injectSignal'], }, + { + type: 'category', + label: 'Factories', + items: ['v2/api/factories/atom'], + }, 'v2/api/glossary', ], }, From 15ab2753378590e99c6b099dac59cdcc2a8ec913 Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Thu, 24 Jul 2025 12:38:08 -0400 Subject: [PATCH 09/49] docs: document `AtomApi` class for v2 --- docs/docs/v2/api/classes/AtomApi.mdx | 385 ++++++++++++++++++++++ docs/docs/v2/api/classes/AtomInstance.mdx | 16 +- docs/docs/v2/api/classes/AtomTemplate.mdx | 4 +- docs/docs/v2/api/factories/atom.mdx | 6 +- docs/docs/v2/api/glossary.mdx | 2 +- docs/sidebars.js | 1 + 6 files changed, 400 insertions(+), 14 deletions(-) create mode 100644 docs/docs/v2/api/classes/AtomApi.mdx diff --git a/docs/docs/v2/api/classes/AtomApi.mdx b/docs/docs/v2/api/classes/AtomApi.mdx new file mode 100644 index 00000000..26fee31c --- /dev/null +++ b/docs/docs/v2/api/classes/AtomApi.mdx @@ -0,0 +1,385 @@ +--- +id: AtomApi +title: AtomApi +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +Atom APIs dynamically define certain integral properties of an atom. These properties do not fit well in the [injector paradigm](../glossary#injector), as they define key characteristics of the atom itself. + +These properties include an atom's state (or [wrapped signal](./AtomInstance.mdx#signal-wrappers)), exports, suspense promise, and custom TTL configuration. + +**New in v2:** Properties added to an AtomApi do not have to be stable references. See [`.exports`](#exports) for details. + +:::note +The properties don't _have_ to be stable; Zedux will just ignore the new references on subsequent evaluations. +::: + +## Creation + +Create AtomApis with [the `api()` factory](/not-done?path=../factories/api). + +```ts +import { api } from '@zedux/react' + +const myApi = api() +const withValue = api('some value') +const withStore = api(createStore()) +const withExports = api(val).setExports({ ...myExports }) +const withPromise = api(val).setPromise(myPromise) +const fromApi = api(myApi) +const addingExports = api(withExports).addExports({ ...moreExports }) +const overwritingExports = api(withExports).setExports({ ...newExports }) +``` + +While this can be called anywhere, an atom api can only define properties for an atom when it's returned from the atom's [state factory](../factories/atom#valueorfactory). It's most common to return the result of `api()` directly: + +```ts +const exampleAtom = atom('example', () => { + const signal = injectSignal('some signal') + + const someExport = () => doSomethingWith(signal) + + return api(signal).setExports({ someExport }) +}) +``` + +## Usage + +AtomApis can be used to pass stores, promises, and exports around. Ultimately, you'll return only one AtomApi from the state factory. + +```ts +import { api, atom, injectStore } from '@zedux/react' + +const withEvaluator = atom('withEvaluator', () => { + return api('initial state') +}) + +const withStore = atom('withStore', () => { + const store = injectStore('initial state') + + return api(store) +}) + +const withExports = atom('withExports', () => { + const store = injectStore('initial state') + + return api(store).setExports({ + someProp: () => 'some val', + }) +}) + +const composingApis = atom('composingApis', () => { + const injectedApi = injectSomethingThatReturnsAnApi() + + return api(injectedApi).addExports({ + additionalExport: () => 'some val', + }) +}) +``` + +**New in v2:** Since Zedux wraps exported functions by default, you can selectively use [`.addExports`](#addexports) to add exports without wrapping them. + +```ts +const exampleAtom = atom('example', () => { + const signal = injectSignal('initial state') + const state = signal.get() + + return api(store) + .setExports( + { + wrappedExport: () => state, // accessing unstable values is fine for wrapped exports + }, + { wrap: true } // the default + ) + .addExports( + { + unwrappedExport: () => state, // this value will become stale when `signal` changes + }, + { wrap: false } + ) +}) +``` + +A rare use case for this is when consumers need access to an exact, unwrapped function reference. But you shouldn't typically need this. + +## Generics + +For TypeScript users, `AtomApi`s have a single type generic called the `AtomApiGenerics`. This is similar to [`ZeduxNode`s `NodeGenerics`](./ZeduxNode.mdx#generics). + +You can pull this generic information off any AtomApi by using various `*Of` type helpers. Comprehensive example: + +```ts +const exampleAtom = atom('example', () => { + const signal = injectSignal('example state') + + const exampleApi = api(signal).setExports({ + exampleExport: () => signal.get(), + }) + + // `EventsOf` knows how to reach through the `G['Signal']` generic to get the + // signal's events: + type ExampleEvents = EventsOf + type ExampleExports = ExportsOf + type ExamplePromise = PromiseOf + type ExampleState = StateOf + + // NOTE: There is no `SignalOf` type helper, since `EventsOf` or `StateOf` is + // usually all you need. You can create your own if needed: + type SignalOf
= A extends AtomApi + ? G['Signal'] + : never + + // then use like so: + type ExampleSignal = SignalOf + + return exampleApi +}) +``` + +Full list of keys on the `AtomApiGenerics` (`G`) type generic: + + + + The combined object type of all exports set via [`setExports`](#setexports) and/or [`addExports`](#addexports). + + + + The type of the promise set via [`setPromise`](#setpromise). + + + + The full type of the signal passed to the [`api()`](/not-done?path=../factories/api) factory (if any). This has the signal's [`G['Events']`](./Signal#generics) and [`G['State']`](./Signal#generics) intact. + + + + The type that will become the state of the atom instance, if this AtomApi is + returned from a state factory. This type is inferred based on the value + passed to the [`api()`](/not-done?path=../factories/api) factory: + + - If a raw value is passed, this type will be inferred directly from it. + + - If a promise is passed, this type will be the promise's resolved value type. + + - If a [signal](./Signal) is passed, this type will be the signal's state type. + + + + +## Properties + +AtomApis expose the following **readonly** properties: + + + + + An object, or `undefined` if no exports were set. + + These are the exports added to this AtomApi via [`setExports`](#setexports) and/or [`addExports`](#addexports). + + When an AtomApi is returned from a state factory, these will become the atom instance's [`.exports`](./AtomInstance#exports). + + **New in v2:** These exports can change on subsequent evaluations - they no longer have to be stable references. Exports are not part of the atom's state, meaning they don't trigger updates in consumers when changed. However, Zedux works around this by wrapping exported functions. See [`addExports`](#addexports) and [`setExports`](#setexports) for more information. + + + + The promise set via [`.setPromise`](#setpromise). + + Unless the AtomApi's `.value` is a promise (creating a "query atom"), this promise will be set as the atom instance's suspense `.promise` (if this AtomApi is returned from the state factory). For query atoms, this property is ignored. + + :::note + The atom's `.promise` is considered "stateful", meaning it will trigger updates in consumers when changed on subsequent evaluations. Unlike [`.value`](#value) changes, promise changes will also trigger updates for [static dependents](../glossary#static-graph-dependency) of the atom. + ::: + + + + If this AtomApi's `.value` is a [signal](./Signal), the signal will also be assigned to this property. This is mostly for convenience when working with TypeScript, since the `.value` property won't retain all the type information of the exact signal used. + + + + The value set via [`api.setTtl`](#setttl). See [`api.setTtl`](#setttl) for possible values. + + + + A reference to the value passed to the [`api()` factory](/not-done?path=../factories/api) itself. Can be any raw value, a promise, or a [signal](./Signal) (including a [mapped signal](/not-done?path=./MappedSignal) or even another [atom instance](./AtomInstance.mdx)). + + If it's a signal and this AtomApi is returned from a state factory, the signal should be a stable reference that won't change on subsequent evaluations, e.g. by using [`injectSignal`](../injectors/injectSignal). + + + + +## Methods + + + + Merges an object of "exported" values into any already-set [exports](#exports) on this AtomApi. If no exports have been set yet on this AtomApi, `.addExports()` sets the exports. + + ```ts + api('val') + .addExports({ a: 1 }) + .addExports({ b: 2 }) + .addExports({ a: 3 }) + // .exports // { a: 3, b: 2 } + ``` + + Signature: + + ```ts + addExports = (exports, config?) => api + ``` + + + + An object mapping export names to their values. Can contain anything. These will be merged into the AtomApi's existing exports, if any. + + See [`AtomInstance#exports`](./AtomInstance#exports) for more information. + + + + Optional. An object with a single, optional property: + + ```ts + { wrap?: boolean } + ``` + + - `wrap`: Default: `true`. If true, Zedux will detect any exported plain functions and wrap them in a new function that automatically [batches](./Ecosystem#batch) and, if the atom is scoped, [scopes](./Ecosystem#withscope) them with the currently-evaluating atom instance's scope. + + Zedux also uses this wrapper function to swap out export implementations while keeping the function provided to consumers stable. See [this PR](https://github.com/Omnistac/zedux/pull/256) for more info. + + Pass `false` to prevent this behavior. You shouldn't normally need this, but there are some use cases where it's useful - e.g. avoiding batching or micro-optimizing atoms that are created millions of times (since this wrapping has a tiny bit of overhead). + + This wrapping only occurs if `addExports` is called during atom evaluation and only on the initial evaluation of that atom, so overhead is minimal. + + + + The AtomApi for chaining. + + + + + + + The main way to set an AtomApi's exports. Sets the passed object of "exported" values as the AtomApi's [exports](#exports). Overwrites any previously-set exports on this AtomApi. + + If this AtomApi is returned from a state factory, these exports will be set as the atom instance's [`.exports`](#exports). + + ```ts + const initialExports = api(val).setExports({ ...myExports }) + const overwriteExports = api(initialExports).setExports({ ...newExports }) + ``` + + Signature: + + ```ts + setExports = (exports, config?) => api + ``` + + + + An object mapping export names to their values. Can contain anything. These will overwrite the AtomApi's existing exports, if any. + + See [`AtomInstance#exports`](./AtomInstance#exports) for more information. + + + Optional. An object with a single, optional property: + + ```ts + { wrap?: boolean } + ``` + + - `wrap`: Default: `true`. If true, Zedux will detect any exported plain functions and wrap them in a new function that automatically [batches](./Ecosystem#batch) and, if the atom is scoped, [scopes](./Ecosystem#withscope) them with the currently-evaluating atom instance's scope. + + Zedux also uses this wrapper function to swap out export implementations while keeping the function provided to consumers stable. See [this PR](https://github.com/Omnistac/zedux/pull/256) for more info. + + Pass `false` to prevent this behavior. You shouldn't normally need this, but there are some use cases where it's useful - e.g. avoiding batching or micro-optimizing atoms that are created millions of times (since this wrapping has a tiny bit of overhead). + + This wrapping only occurs if `setExports` is called during atom evaluation and only on the initial evaluation of that atom, so overhead is minimal. + + + + The AtomApi for chaining. + + + + + + + Sets the [`.promise`](#promise) property of this AtomApi. + + If this AtomApi is returned from a state factory, the promise will be set as the atom instance's [`.promise`](#promise) and will be used to cause React to suspend. + + This promise does not have to be a stable reference, though you should be conscious of when its reference changes since any components using the atom instance will re-suspend when the promise changes (if this AtomApi is returned from the state factory). + + ```ts + const promiseApi = api(val).setPromise(myPromise) + promiseApi.setPromise(myNewPromise) // overwrite myPromise + ``` + + Signature: + + ```ts + setPromise = (promise) => api + ``` + + + + Required. A promise. + + + + The AtomApi for chaining. + + + + + + + Accepts a number (in milliseconds), promise, or observable, or a function that returns a number (in milliseconds), promise, or observable. This will be set as the AtomApi's `.ttl` property. + + If this AtomApi is returned from a state factory, this will be set as the [atom instance's TTL](./AtomInstance#ttl), overriding any [atom template-level TTL](./AtomTemplate#ttl). + + This is far more flexible than atom-template-level ttl, which can only be a number in milliseconds. + + - When a number is set, Zedux will set a timeout for `ttl` milliseconds. When that timeout times out, if the atom instance is still unused, Zedux will destroy it. A ttl of `0` will skip the timeout and clean up immediately. + - When a promise is set, Zedux will wait for that promise to resolve before cleaning up the atom instance. + - When an observable is set, Zedux will wait for that observable to emit before cleaning up the atom instance. + + In all cases, if the atom instance is used again while Zedux is awaiting the ok for cleanup, cleanup will be cancelled, and the instance's [`status`](./AtomInstance#status) will transition from Stale back to Active. + + ```ts + const withNumberTtl = api().setTtl(1000) + const withPromiseTtl = api().setTtl(myPromise) + const withObservableTtl = api().setTtl(myRxJsObservable$) + + // when the ttl is dynamic, determined after the atom instance is created, use + // a function: + const withFunctionAndNumberTtl = api().setTtl(() => 1000) + const withFunctionAndPromiseTtl = api().setTtl(() => myPromise) + const withFunctionAndObservableTtl = api().setTtl(() => myRxJsObservable$) + ``` + + Signature: + + ```ts + setTtl = (ttl) => api + ``` + + + + Required. A number, promise, or observable, or a function that returns a number, promise, or observable. + + + + The AtomApi for chaining. + + + + + + + +## See Also + +- [The Atom APIs walkthrough](../../../walkthrough/atom-apis) +- [The `api()` factory](/not-done?path=../factories/api) +- [The `AtomInstance` class](./AtomInstance) diff --git a/docs/docs/v2/api/classes/AtomInstance.mdx b/docs/docs/v2/api/classes/AtomInstance.mdx index eed88aee..c1ea7aff 100644 --- a/docs/docs/v2/api/classes/AtomInstance.mdx +++ b/docs/docs/v2/api/classes/AtomInstance.mdx @@ -65,9 +65,9 @@ Configuring an atom's TTL (Time To Live) is the recommended way to manage its li You can configure this in two ways: - Setting the atom template's [`ttl` config](./AtomTemplate#ttl). This is suitable for most cases, but only accepts a number in milliseconds. -- Returning an [atom api](/not-done?path=./AtomApi) from the atom's state factory that configures a TTL via [`.setTtl()`](/not-done?path=./AtomApi#setttl). This is much more powerful, accepting a number, promise, observable, or a function that returns a number, promise, or observable. +- Returning an [atom api](./AtomApi) from the atom's state factory that configures a TTL via [`.setTtl()`](./AtomApi#setttl). This is much more powerful, accepting a number, promise, observable, or a function that returns a number, promise, or observable. - Setting TTL via returning a configured [atom api](/not-done?path=./AtomApi) overrides any TTL configured on the [atom template](./AtomTemplate#ttl). It also allows you to configure a different TTL for each instance of the atom. + Setting TTL via returning a configured [atom api](./AtomApi) overrides any TTL configured on the [atom template](./AtomTemplate#ttl). It also allows you to configure a different TTL for each instance of the atom. The sky's the limit. With TTL, you can destroy atoms on page route, on log out, when the cache reaches a certain size, or anything else you can think of. @@ -126,7 +126,7 @@ For TypeScript users, atom instances have the following unique generics on their A Record type. The exports of the atom. Will be an empty record if the atom has no exports. - Extends `Promise`. The promise type of this node. This will be inferred automatically when the atom's state factory returns an atom promise with a promise attached via [`api().setPromise()`](/not-done?path=./AtomApi#setpromise). + Extends `Promise`. The promise type of this node. This will be inferred automatically when the atom's state factory returns an atom promise with a promise attached via [`api().setPromise()`](./AtomApi#setpromise). @@ -212,7 +212,7 @@ Atom instances have the following **readonly** properties: - A reference to the [AtomApi](/not-done?path=./AtomApi) returned from the atom instance's state factory on its last evaluation. + A reference to the [AtomApi](./AtomApi) returned from the atom instance's state factory on its last evaluation. Unlike [`exports`](#exports), this reference is not stable. It will change on every evaluation. Since atoms have no mechanism to notify observers when this changes, it's not recommended to use this directly. This is exposed for plugin authors and maybe some debugging cases. @@ -220,7 +220,7 @@ Atom instances have the following **readonly** properties: An object. May be undefined, if nothing was exported. - The exports of the atom instance, as defined by the instance's returned [AtomApi](/not-done?path=./AtomApi). You can export absolutely anything. + The exports of the atom instance, as defined by the instance's returned [AtomApi](./AtomApi). You can export absolutely anything. This object is stable. It is set the first time an atom instance is created and will not change on subsequent evaluations. @@ -251,7 +251,7 @@ Atom instances have the following **readonly** properties: - A promise. May be undefined if no promise was set on a returned [AtomApi](/not-done?path=./AtomApi). + A promise. May be undefined if no promise was set on a returned [AtomApi](./AtomApi). This promise will be used to cause React to suspend whenever this atom instance is used in a component until the promise completes. This promise reference will change if a subsequent evaluation returns a new promise. @@ -260,7 +260,7 @@ Atom instances have the following **readonly** properties: The rejection value caught from the instance's [`.promise`](#promise). `undefined` if the promise did not reject. - A string or undefined. The status of the instance's [`.promise`](#promise). Will be `undefined` if the atom did not [set a promise](/not-done?path=./AtomApi#setpromise). + A string or undefined. The status of the instance's [`.promise`](#promise). Will be `undefined` if the atom did not [set a promise](./AtomApi#setpromise). Possible values: @@ -345,4 +345,4 @@ Atom instances also inherit the following methods from [`ZeduxNode`](./ZeduxNode - [The `Signal` class](./Signal) - [The `ZeduxNode` class](./ZeduxNode) - [The `AtomTemplate` class](./AtomTemplate) -- [The `AtomApi` class](/not-done?path=./AtomApi) +- [The `AtomApi` class](./AtomApi) diff --git a/docs/docs/v2/api/classes/AtomTemplate.mdx b/docs/docs/v2/api/classes/AtomTemplate.mdx index 5a4231fd..23bb6f16 100644 --- a/docs/docs/v2/api/classes/AtomTemplate.mdx +++ b/docs/docs/v2/api/classes/AtomTemplate.mdx @@ -90,14 +90,14 @@ When creating your own, custom atom types, you'll usually want to extend this cl This is the [`ttl` atom config option](/not-done?path=../types/AtomConfig#ttl) passed to [the `atom()` factory](../factories/atom), if any. - If not set, instances of this atom will live forever unless configured with [`.setTtl()`](/not-done?path=./AtomApi#setttl) on an AtomApi returned by the state factory. + If not set, instances of this atom will live forever unless configured with [`.setTtl()`](./AtomApi#setttl) on an AtomApi returned by the state factory. - Set to -1 to make this atom's instances live forever. - Set to 0 to destroy all instances of this atom as soon as they go stale. - Set to any positive integer to make atoms live in a stale state for{' '} <ttl> milliseconds before being cleaned up. - If set, this option will still be overridden by any `ttl` [set on an AtomApi](/not-done?path=./AtomApi#setttl) returned by the state factory. + If set, this option will still be overridden by any `ttl` [set on an AtomApi](./AtomApi#setttl) returned by the state factory. diff --git a/docs/docs/v2/api/factories/atom.mdx b/docs/docs/v2/api/factories/atom.mdx index 160bb9b7..dce5095c 100644 --- a/docs/docs/v2/api/factories/atom.mdx +++ b/docs/docs/v2/api/factories/atom.mdx @@ -199,7 +199,7 @@ function App() { - A state factory function that returns a [signal](../classes/Signal). When the atom is instantiated, the new atom instance will become a thin wrapper around the returned signal, forwarding events and state changes between it and the atom's own observers. - - A state factory function that returns an [AtomApi](/not-done?path=../classes/AtomApi) instance. + - A state factory function that returns an [AtomApi](../classes/AtomApi) instance. The Atom API's value can be any of the following: @@ -213,7 +213,7 @@ function App() { The Atom API's promise will be set as the atom instance's `.promise`. - Any [`ttl`](/not-done?path=../classes/AtomApi#ttl) configured in the returned Atom API will control the atom instance's destruction timing. + Any [`ttl`](../classes/AtomApi#ttl) configured in the returned Atom API will control the atom instance's destruction timing. @@ -230,6 +230,6 @@ function App() { ## See Also - [The `AtomTemplate` class](../classes/AtomTemplate) -- [The `AtomApi` class](/not-done?path=../classes/AtomApi) +- [The `AtomApi` class](../classes/AtomApi) - [The Quick Start](../../../walkthrough/quick-start) - [The Configuring Atoms walkthrough](../../../walkthrough/configuring-atoms) diff --git a/docs/docs/v2/api/glossary.mdx b/docs/docs/v2/api/glossary.mdx index 93e5c5ea..f943da7c 100644 --- a/docs/docs/v2/api/glossary.mdx +++ b/docs/docs/v2/api/glossary.mdx @@ -45,7 +45,7 @@ There are 3 basic types of injectors: Injectors should only be used at the top level of [atom state factories](#state-factory). Don't use them in loops or conditional statements. -Injectors can be used any number of times throughout an atom state factory. For certain one-off operations like setting an atom instance's exports or setting a suspense promise, use an [AtomApi](/not-done?path=./classes/AtomApi). +Injectors can be used any number of times throughout an atom state factory. For certain one-off operations like setting an atom instance's exports or setting a suspense promise, use an [AtomApi](./classes/AtomApi). Like hooks, you can create custom injectors that compose other injectors. The convention is to start all injectors with the word "inject", similar to the word "use" with React hooks. diff --git a/docs/sidebars.js b/docs/sidebars.js index 823a9c46..e44a5aa4 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -165,6 +165,7 @@ module.exports = { type: 'category', label: 'Classes', items: [ + 'v2/api/classes/AtomApi', 'v2/api/classes/AtomInstance', 'v2/api/classes/AtomTemplate', 'v2/api/classes/Ecosystem', From cd3e5c1f7ec1d18a7b47d03af12144e5225883ed Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Thu, 24 Jul 2025 13:20:47 -0400 Subject: [PATCH 10/49] docs: document `api` factory for v2 --- docs/docs/v2/api/classes/AtomApi.mdx | 10 +- docs/docs/v2/api/factories/api.mdx | 148 +++++++++++++++++++++++++++ docs/sidebars.js | 2 +- 3 files changed, 154 insertions(+), 6 deletions(-) create mode 100644 docs/docs/v2/api/factories/api.mdx diff --git a/docs/docs/v2/api/classes/AtomApi.mdx b/docs/docs/v2/api/classes/AtomApi.mdx index 26fee31c..5546e472 100644 --- a/docs/docs/v2/api/classes/AtomApi.mdx +++ b/docs/docs/v2/api/classes/AtomApi.mdx @@ -17,7 +17,7 @@ The properties don't _have_ to be stable; Zedux will just ignore the new referen ## Creation -Create AtomApis with [the `api()` factory](/not-done?path=../factories/api). +Create AtomApis with [the `api()` factory](../factories/api.mdx). ```ts import { api } from '@zedux/react' @@ -149,13 +149,13 @@ Full list of keys on the `AtomApiGenerics` (`G`) type generic: - The full type of the signal passed to the [`api()`](/not-done?path=../factories/api) factory (if any). This has the signal's [`G['Events']`](./Signal#generics) and [`G['State']`](./Signal#generics) intact. + The full type of the signal passed to the [`api()`](../factories/api.mdx) factory (if any). This has the signal's [`G['Events']`](./Signal#generics) and [`G['State']`](./Signal#generics) intact. The type that will become the state of the atom instance, if this AtomApi is returned from a state factory. This type is inferred based on the value - passed to the [`api()`](/not-done?path=../factories/api) factory: + passed to the [`api()`](../factories/api.mdx) factory: - If a raw value is passed, this type will be inferred directly from it. @@ -201,7 +201,7 @@ AtomApis expose the following **readonly** properties: - A reference to the value passed to the [`api()` factory](/not-done?path=../factories/api) itself. Can be any raw value, a promise, or a [signal](./Signal) (including a [mapped signal](/not-done?path=./MappedSignal) or even another [atom instance](./AtomInstance.mdx)). + A reference to the value passed to the [`api()` factory](../factories/api.mdx) itself. Can be any raw value, a promise, or a [signal](./Signal) (including a [mapped signal](/not-done?path=./MappedSignal) or even another [atom instance](./AtomInstance.mdx)). If it's a signal and this AtomApi is returned from a state factory, the signal should be a stable reference that won't change on subsequent evaluations, e.g. by using [`injectSignal`](../injectors/injectSignal). @@ -381,5 +381,5 @@ AtomApis expose the following **readonly** properties: ## See Also - [The Atom APIs walkthrough](../../../walkthrough/atom-apis) -- [The `api()` factory](/not-done?path=../factories/api) +- [The `api()` factory](../factories/api.mdx) - [The `AtomInstance` class](./AtomInstance) diff --git a/docs/docs/v2/api/factories/api.mdx b/docs/docs/v2/api/factories/api.mdx new file mode 100644 index 00000000..14b0a170 --- /dev/null +++ b/docs/docs/v2/api/factories/api.mdx @@ -0,0 +1,148 @@ +--- +id: api +title: api +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +```ts +import { api } from '@zedux/react' +``` + +A factory for defining an atom's API. This factory returns an instance of the special [AtomApi class](../classes/AtomApi). + +You can return an AtomApi from an atom state factory to define many characteristics of the atom - namely its state, exports, promise, and lifecycle. + +## Example + +Defining an atom's `exports`: + +```tsx live ecosystemId=api/exports-example resultVar=App version=2 +const counterAtom = atom('counter', () => { + const signal = injectSignal(0) + + const decrement = () => signal.set(val => val - 1) + const increment = () => signal.set(val => val + 1) + + return api(signal).setExports({ decrement, increment }) +}) + +function App() { + const [count, { decrement, increment }] = useAtomState(counterAtom) + + return ( +
+
Count: {count}
+ + +
+ ) +} +``` + +Defining an atom's `promise`, e.g. for use with React Suspense: + +```tsx live ecosystemId=api/promis-example resultVar=App version=2 +const helloWorldAtom = atom('helloWorld', () => { + const signal = injectSignal('') + + return api(signal).setPromise( + new Promise(resolve => { + setTimeout(() => { + signal.set('Hello, World!') + + // resolving the promise with the atom's resolved state is a best practice + resolve(signal.get()) + }, 500) + }) + ) +}) + +function HelloWorld() { + const hello = useAtomValue(helloWorldAtom) + + return ( +
+ The resolved value: {hello} (Click "Reset" to see the fallback again) +
+ ) +} + +function App() { + return ( + Loading...}> + + + ) +} +``` + +Defining an atom's `ttl`: + +```tsx +// this is equivalent to passing `{ ttl: 0 }` as the third argument to the +// `atom` factory: +const staticNumericTtlAtom = atom('staticNumericTtl', () => + api('example state').setTtl(0) +) + +const dynamicNumericTtlAtom = atom('dynamicNumericTtl', () => { + const signal = injectSignal(0) + const ttlRef = injectRef(null) + + // Consumers can set the ttl via `instance.exports.ttlRef.current = newTtl`. + // That ttl will then be used when this atom becomes stale. + return api(signal) + .setExports({ ttlRef }) + .setTtl(() => ttlRef.current) +}) + +const dynamicPromiseTtlAtom = atom('dynamicPromiseTtl', () => { + const signal = injectSignal(0) + const promise = injectAtomValue(somePromiseAtom) + + return api(signal).setTtl(() => promiseRef.current) +}) +``` + +:::tip +You can mix and match these approaches. For example, export a `promiseRef` or `observableSignal` to give consumers lots of control over the atom's TTL. +::: + +## Signature + + + {tab1(`api = (value) => newAtomApi`)} + {tab2(`declare const api: < + Value, + Exports extends Record = {}, + Promise extends AtomApiPromise = undefined +>( + value: Value +) => AtomApi<{ + Exports: Exports + Promise: Promise + State: Value + Signal: Value extends Signal ? Value : undefined +}>`)} + + + + + Required. The state value, signal, or promise that this AtomApi should wrap. + - If a signal, the atom instance will become a wrapper around the signal - + If a promise, the atom becomes a [query + atom](../../../walkthrough/query-atoms) - If any other value, this becomes + the atom's state + + + An [AtomApi instance](../classes/AtomApi) that can be configured with + exports, ttl, and other options. + + + +## See Also + +- [The `AtomApi` class](../classes/AtomApi) +- [The `atom` factory](./atom) +- [The AtomApi walkthrough](../../../walkthrough/atom-apis) diff --git a/docs/sidebars.js b/docs/sidebars.js index e44a5aa4..2c77516b 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -181,7 +181,7 @@ module.exports = { { type: 'category', label: 'Factories', - items: ['v2/api/factories/atom'], + items: ['v2/api/factories/api', 'v2/api/factories/atom'], }, 'v2/api/glossary', ], From 98223eded4c7297160187b2142f56133c5dbad87 Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Thu, 24 Jul 2025 13:44:39 -0400 Subject: [PATCH 11/49] docs: document `SelectorInstance` class for v2 --- docs/docs/v2/api/classes/Ecosystem.mdx | 2 +- docs/docs/v2/api/classes/SelectorInstance.mdx | 161 ++++++++++++++++++ docs/docs/v2/api/classes/ZeduxNode.mdx | 13 +- docs/docs/v2/api/glossary.mdx | 2 +- docs/sidebars.js | 1 + 5 files changed, 170 insertions(+), 9 deletions(-) create mode 100644 docs/docs/v2/api/classes/SelectorInstance.mdx diff --git a/docs/docs/v2/api/classes/Ecosystem.mdx b/docs/docs/v2/api/classes/Ecosystem.mdx index 33f85af3..df475f44 100644 --- a/docs/docs/v2/api/classes/Ecosystem.mdx +++ b/docs/docs/v2/api/classes/Ecosystem.mdx @@ -677,7 +677,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context
- Gets a cached [graph node](../glossary#graph-node), namely an [atom instance](./AtomInstance) or [selector instance](/not-done?path=./SelectorInstance). Creates and caches the node if it doesn't exist yet. + Gets a cached [graph node](../glossary#graph-node), namely an [atom instance](./AtomInstance) or [selector instance](./SelectorInstance.mdx). Creates and caches the node if it doesn't exist yet. ```ts const { getNode } = myEcosystem // `getNode` can be destructured like this diff --git a/docs/docs/v2/api/classes/SelectorInstance.mdx b/docs/docs/v2/api/classes/SelectorInstance.mdx new file mode 100644 index 00000000..b1eff444 --- /dev/null +++ b/docs/docs/v2/api/classes/SelectorInstance.mdx @@ -0,0 +1,161 @@ +--- +id: SelectorInstance +title: SelectorInstance +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +A cached [selector](/not-done?path=../types/AtomSelector). When a selector is used for the first time in most Zedux hooks, injectors, or ecosystem methods, Zedux caches the selector's result by creating an instance of this class. + +Selectors are cached by reference. Each unique [AtomSelector](/not-done?path=../types/AtomSelector) function reference or [AtomSelectorConfig](/not-done?path=../types/AtomSelectorConfig) object creates a unique cached selector instance. + +Selectors that take params also create a unique cached selector instance for each unique set of params. This works exactly the same as [atom params](./AtomInstance#params). + +Selector instances are graph nodes, meaning this class extends [the `ZeduxNode` class](./ZeduxNode.mdx). + +## Creation + +You never instantiate this class yourself. Zedux instantiates it automatically as you use [selectors](/not-done?path=../types/AtomSelector) in your app. + +```ts +const getUsername = ({ get }: Ecosystem) => get(userDataAtom).username + +// this selector caches the `getUsername` selector, if it isn't cached yet: +const shoutUsername = ({ get }: Ecosystem) => get(getUsername).toUpperCase() + +function MyComponent() { + // this hook call caches the `shoutUsername` selector, which in turn caches + // the `getUsername` selector: + const loudUsername = useAtomValue(shoutUsername) + + // since `shoutUsername` already cached the `getUsername` selector, this hook + // call just retrieves the cached value: + const username = useAtomValue(getUsername) + + // ... +} +``` + +Parameterized selector example: + +```ts +const getUsersByRole = ({ get }: Ecosystem, role: string) => + get(userListAtom).filter(users => user.role === role) + +const groupedUsersAtom = ion('groupedUsers', ({ get }) => { + // create two cached selector instances of the `getUsersByRole` selector, one + // for each role: + const adminUsers = get(getUsersByRole, ['admin']) + const moderatorUsers = get(getUsersByRole, ['moderator']) + + return { adminUsers, moderatorUsers } +}) +``` + +## Destruction + +Selector instances are always destroyed when they're no longer in use. This is the same behavior as atoms configured with [`ttl: 0`](./AtomInstance#ttl). + +## Inline Selectors + +Since selectors are simply functions, they're easy to create on the fly: + +```ts +function ExampleComponent() { + const filteredTodos = useAtomValue(({ get }) => + get(todosAtom).filter(todo => todo.completed) + ) +} +``` + +While Zedux supports this, it has a few caveats: + +- This inline selector function has to run on every render since Zedux can't know whether it closes over any props, state, or other unstable values in the component body. This almost guarantees some unnecessary overhead, though its impact is typically small. +- The `SelectorInstance` class instance for the inline selector will **not** usually be destroyed/recreated on every render, however Zedux has to run some extra checks to pull this off. This means even more overhead. +- Zedux has historically had several bugs and edge cases with inline selectors used in React strict/concurrent mode. It just isn't something React is set up to handle well. + +As such, it's recommended to either define selector functions outside components or wrap inline selectors in a `useCallback` or `useMemo` hook. + +:::note +The [React compiler](https://react.dev/learn/react-compiler) may fix this by auto-memoizing the selector function reference passed to `useAtomValue` or other hooks. The jury's still out on this, but it could be a huge win. +::: + +## Generics + +For TypeScript users, selector instances inherit the following generics from the `ZeduxNode` class: + + + + See [`ZeduxNode<{ Params }>`](./ZeduxNode.mdx#gparams). + + For selectors, this is a reference to the array of params passed to this selector instance when it was first created. + + + + See [`ZeduxNode<{ State }>`](./ZeduxNode.mdx#gstate). + + + + See [`ZeduxNode<{ Template }>`](./ZeduxNode.mdx#gtemplate). + + For selectors, this is a reference to the [AtomSelector](/not-done?path=../types/AtomSelector) function or [AtomSelectorConfig](/not-done?path=../types/AtomSelectorConfig) object that was used to create this selector instance. + + + + +## Events + +Selector instances inherit the following built-in events from [`ZeduxNode`](./ZeduxNode.mdx#events): + + + + See the [node `change` event](./ZeduxNode.mdx#change-event). + + + + See the [node `cycle` event](./ZeduxNode.mdx#cycle-event). + + + + +## Properties + +Selector instances inherit the following **readonly** properties from [`ZeduxNode`](./ZeduxNode.mdx#properties): + + + See [`ZeduxNode#id`](./ZeduxNode.mdx#id). + See [`ZeduxNode#params`](./ZeduxNode.mdx#params). + See [`ZeduxNode#status`](./ZeduxNode.mdx#status). + + See [`ZeduxNode#template`](./ZeduxNode.mdx#template). + + + +## Methods + +Selector instances inherit the following methods from [`ZeduxNode`](./ZeduxNode.mdx#methods): + + + + See [`ZeduxNode#destroy`](./ZeduxNode.mdx#destroy). + + + + See [`ZeduxNode#get`](./ZeduxNode.mdx#get). + + + + See [`ZeduxNode#getOnce`](./ZeduxNode.mdx#getonce). + + + + See [`ZeduxNode#on`](./ZeduxNode.mdx#on) and the above documentation for [selector events](#events). + + + +## See Also + +- The [selectors walkthrough](../../../walkthrough/selectors.mdx) +- The "selector template" types: + - [The `AtomSelector` type](/not-done?path=../types/AtomSelector) + - [The `AtomSelectorConfig` type](/not-done?path=../types/AtomSelectorConfig) diff --git a/docs/docs/v2/api/classes/ZeduxNode.mdx b/docs/docs/v2/api/classes/ZeduxNode.mdx index 9476a886..f8745161 100644 --- a/docs/docs/v2/api/classes/ZeduxNode.mdx +++ b/docs/docs/v2/api/classes/ZeduxNode.mdx @@ -6,7 +6,7 @@ title: ZeduxNode import useBaseUrl from '@docusaurus/useBaseUrl' import { Legend, Item, Link, Tabs, tab1, tab2 } from '@site/src/all' -This is the base class for every [graph node](../glossary#graph-node). The [AtomInstance class](./AtomInstance), [SelectorInstance class](/not-done?path=./SelectorInstance), [Signal class](./Signal), and several other graph node types extend this class. +This is the base class for every [graph node](../glossary#graph-node). The [AtomInstance class](./AtomInstance), [SelectorInstance class](./SelectorInstance.mdx), [Signal class](./Signal), and several other graph node types extend this class. You never create this class directly and should never need to reference it directly except as a type. @@ -47,14 +47,13 @@ Full list of keys on the `NodeGenerics` (`G`) type generic: A tuple type. The parameters of this node. Only exists on [atom - instances](./AtomInstance) and [selector - instances](/not-done?path=./SelectorInstance). + instances](./AtomInstance) and [selector instances](./SelectorInstance.mdx). The state type of this node. Can be anything. A recursive reference to the current node's full template type. `undefined` if none. Only [atom instances](./AtomInstance) and [selector - instances](/not-done?path=./SelectorInstance) have templates. + instances](./SelectorInstance.mdx) have templates. @@ -179,7 +178,7 @@ Every node has the following **readonly** properties: An array. The parameters passed to this node when it was created. - A reference to the raw, unserialized params that were used to create this atom instance. Only [atom instances](./AtomInstance) and [selector instances](/not-done?path=./SelectorInstance) take params. This will be `undefined` for node types that don't take params (like signals). For nodes that can take params but weren't passed any (like singleton atoms), this will be an empty array. + A reference to the raw, unserialized params that were used to create this atom instance. Only [atom instances](./AtomInstance) and [selector instances](./SelectorInstance.mdx) take params. This will be `undefined` for node types that don't take params (like signals). For nodes that can take params but weren't passed any (like singleton atoms), this will be an empty array. ```ts const instanceA = useAtomInstance(myAtom, ['param 1', 'param 2']) @@ -241,7 +240,7 @@ function Shout() { - A reference to the template this node was created from. `undefined` if none. Only [atom instances](./AtomInstance) and [selector instances](/not-done?path=./SelectorInstance) have templates. + A reference to the template this node was created from. `undefined` if none. Only [atom instances](./AtomInstance) and [selector instances](./SelectorInstance.mdx) have templates. For atom instances, this will be the [atom template](./AtomTemplate). For selector instances, this will be the [selector template](/not-done?path=../types/SelectorTemplate). @@ -338,5 +337,5 @@ Every node has the following methods: - [`ecosystem.getNode()`](./Ecosystem#getnode) - [the `AtomInstance` class](./AtomInstance) -- [the `SelectorInstance` class](/not-done?path=./SelectorInstance) +- [the `SelectorInstance` class](./SelectorInstance.mdx) - [the `Signal` class](./Signal) diff --git a/docs/docs/v2/api/glossary.mdx b/docs/docs/v2/api/glossary.mdx index f943da7c..bbb159d2 100644 --- a/docs/docs/v2/api/glossary.mdx +++ b/docs/docs/v2/api/glossary.mdx @@ -26,7 +26,7 @@ These can be created manually with [manual graphing](../../walkthrough/destructi Zedux builds an internal graph to manage atom dependencies and propagate updates in an optimal way. There are many types of nodes in this graph: - [Atom instances](./classes/AtomInstance) -- [Selector instances](/not-done?path=./classes/SelectorInstance) +- [Selector instances](./classes/SelectorInstance.mdx) - [Signals](./classes/Signal) - [`injectMemo`](/not-done?path=./injectors/injectMemo) calls with no deps array (enabling automatic dependency tracking) - "External" nodes created for React components and event listeners. diff --git a/docs/sidebars.js b/docs/sidebars.js index 2c77516b..397676e6 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -169,6 +169,7 @@ module.exports = { 'v2/api/classes/AtomInstance', 'v2/api/classes/AtomTemplate', 'v2/api/classes/Ecosystem', + 'v2/api/classes/SelectorInstance', 'v2/api/classes/Signal', 'v2/api/classes/ZeduxNode', ], From 142c2f63be3cec47853c3bd8398fd7b84df440c7 Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Thu, 24 Jul 2025 14:10:41 -0400 Subject: [PATCH 12/49] docs: document `AtomProvider` component for v2 --- docs/docs/v2/api/classes/AtomInstance.mdx | 2 +- docs/docs/v2/api/components/AtomProvider.mdx | 163 +++++++++++++++++++ docs/sidebars.js | 5 + 3 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 docs/docs/v2/api/components/AtomProvider.mdx diff --git a/docs/docs/v2/api/classes/AtomInstance.mdx b/docs/docs/v2/api/classes/AtomInstance.mdx index c1ea7aff..29002497 100644 --- a/docs/docs/v2/api/classes/AtomInstance.mdx +++ b/docs/docs/v2/api/classes/AtomInstance.mdx @@ -85,7 +85,7 @@ This is a change from v1. In v1, all atoms had a `.store` property. This made at ## Providing -An atom instance can be provided over React context via [``](/not-done?path=../components/AtomProvider). +An atom instance can be provided over React context via [``](../components/AtomProvider). ```tsx import { AtomProvider, useAtomInstance } from '@zedux/react' diff --git a/docs/docs/v2/api/components/AtomProvider.mdx b/docs/docs/v2/api/components/AtomProvider.mdx new file mode 100644 index 00000000..10a6019a --- /dev/null +++ b/docs/docs/v2/api/components/AtomProvider.mdx @@ -0,0 +1,163 @@ +--- +id: AtomProvider +title: AtomProvider +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +```ts +import { AtomProvider } from '@zedux/react' +``` + +A component that provides one or more [atom instances](../classes/AtomInstance) over React context. + +Use [`useAtomContext()`](/not-done?path=../hooks/useAtomContext) to consume the provided instance(s). + +## Example + +```tsx live ecosystemId=AtomProvider/example resultVar=Parent version=2 +const secondsAtom = atom('seconds', (start: number) => { + const signal = injectSignal(start) + + injectEffect(() => { + const intervalId = setInterval(() => signal.set(val => val + 1), 1000) + + return () => clearInterval(intervalId) + }, []) + + return signal +}) + +function Child() { + const instance = useAtomContext(secondsAtom) // no need to pass params + const state = useAtomValue(instance) // subscribe to updates + + return
Child's Seconds: {state}
+} + +function Parent() { + const instance = useAtomInstance(secondsAtom, [100]) + + return ( + + + + ) +} +``` + +Providing multiple instances: + +```tsx + + + +``` + +## Dynamic Providers + +Sometimes you need to provide an atom instance without creating a dependency on the provided instance in the providing component. This can be necessary when the provided atom instance triggers suspense but the providing component is the one defining the suspense boundary. + +For this case, `AtomProvider`'s `instance` and `instances` props accept a function. The function will be called with the current ecosystem and should return the atom instance(s) to provide. + +```tsx +// since this atom triggers suspense, it needs to be used under a suspense +// boundary +const exampleAtom = atom('example', () => { + return api( + new Promise(resolve => { + setTimeout(() => { + resolve('Hello, world!') + }, 1000) + }) + ) +}) + +function Child() { + const value = useAtomValue(useAtomContext(exampleAtom, true)) + + return
{value}
+} + +function Parent() { + // we don't want to `useAtomInstance(exampleAtom)` here since the suspense + // boundary isn't defined yet. + + return ( + Loading...}> + ecosystem.getNode(exampleAtom)}> + + + + ) +} +``` + +This function overload is also useful for providing multiple instances at once: + +```tsx + ecosystem.findAll('my/providers/namespace')} +> + + +``` + +This is extremely flexible. + +## Signature + + + {tab1( + ` + {children} + + // or + + {children} + `, + true + )} + {tab2(`declare const AtomProvider: ( + props: + | { + children?: ReactNode + instance: AnyAtomInstance | ((ecosystem: Ecosystem) => AnyAtomInstance) + instances?: undefined + } + | { + children?: ReactNode + instance?: undefined + instances: + | AnyAtomInstance[] + | ((ecosystem: Ecosystem) => AnyAtomInstance[]) + } + ) => ReactElement`)} + + +## Props + +AtomProvider accepts **either** an `instance` prop to provide a single atom instance OR an `instances` prop to provide multiple instances. You must pass one or the other but not both. + + + + A single [atom instance](../classes/AtomInstance) or a function that receives the ecosystem and returns an atom instance. This instance will be provided over React context. + + See the above section on [dynamic providers](#dynamic-providers) for examples of when the function overload is useful. + + + + An array of atom instances or a function that receives the ecosystem and returns an array of atom instances. Each instance will be provided via a separate React context provider. + + Be careful reordering this list and adding/removing items since this will make React destroy/recreate the entire component subtree inside ``. + + See the above section on [dynamic providers](#dynamic-providers) for examples of when the function overload is useful. + + + + +## See Also + +- [The React Context walkthrough](../../../walkthrough/react-context.mdx) +- [The `useAtomContext` hook](/not-done?path=../hooks/useAtomContext) +- [The `useAtomInstance` hook](/not-done?path=../hooks/useAtomInstance) diff --git a/docs/sidebars.js b/docs/sidebars.js index 397676e6..19703d19 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -174,6 +174,11 @@ module.exports = { 'v2/api/classes/ZeduxNode', ], }, + { + type: 'category', + label: 'Components', + items: ['v2/api/components/AtomProvider'], + }, { type: 'category', label: 'Injectors', From 66a18e6f87671c8acc26cacf91fc7f128c6b6faa Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Thu, 24 Jul 2025 14:33:03 -0400 Subject: [PATCH 13/49] docs: document `EcosystemProvider` component for v2 --- docs/docs/v2/api/classes/Ecosystem.mdx | 8 +- .../v2/api/components/EcosystemProvider.mdx | 144 ++++++++++++++++++ docs/sidebars.js | 5 +- 3 files changed, 152 insertions(+), 5 deletions(-) create mode 100644 docs/docs/v2/api/components/EcosystemProvider.mdx diff --git a/docs/docs/v2/api/classes/Ecosystem.mdx b/docs/docs/v2/api/classes/Ecosystem.mdx index df475f44..e16fe310 100644 --- a/docs/docs/v2/api/classes/Ecosystem.mdx +++ b/docs/docs/v2/api/classes/Ecosystem.mdx @@ -28,7 +28,7 @@ import { createEcosystem } from '@zedux/react' const rootEcosystem = createEcosystem({ id: 'root' }) ``` -Ecosystems are also created automatically when using an [``](/not-done?path=../components/EcosystemProvider) without passing an `ecosystem` prop: +Ecosystems are also created automatically when using an [``](../components/EcosystemProvider) without passing an `ecosystem` prop: ```tsx import { EcosystemProvider } from '@zedux/react' @@ -48,11 +48,11 @@ The [default ecosystem](../../../walkthrough/ecosystems#global) will be created The default ecosystem is great for simple apps. It's a full ecosystem, which means you can use features like [overrides](#overrides) and [ecosystem events](#events). However, it comes preconfigured with no (good) way to set config options like [`ssr`](#ssr) and [`onReady`](#onready). -It's generally recommended to create your own ecosystem(s) via [`createEcosystem()`](/not-done?path=../factories/createEcosystem) and provide them to your app via [``](/not-done?path=../components/EcosystemProvider) instead of using the default ecosystem. This is especially needed for SSR. +It's generally recommended to create your own ecosystem(s) via [`createEcosystem()`](/not-done?path=../factories/createEcosystem) and provide them to your app via [``](../components/EcosystemProvider) instead of using the default ecosystem. This is especially needed for SSR. ## Providing -Ecosystems can take control of all atom usages in a React component tree by wrapping the tree in [``](/not-done?path=../components/EcosystemProvider). +Ecosystems can take control of all atom usages in a React component tree by wrapping the tree in [``](../components/EcosystemProvider). ```tsx function App() { @@ -1109,6 +1109,6 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context - [the Plugins guide](../../../advanced/plugins) - [`getDefaultEcosystem()`](/not-done?path=../utils/getDefaultEcosystem) - [`setDefaultEcosystem()`](/not-done?path=../utils/setDefaultEcosystem) -- [the `` component](/not-done?path=../components/EcosystemProvider) +- [the `` component](../components/EcosystemProvider) - [the `useEcosystem` hook](/not-done?path=../hooks/useEcosystem) - [the `injectEcosystem` injector](/not-done?path=../injectors/injectEcosystem) diff --git a/docs/docs/v2/api/components/EcosystemProvider.mdx b/docs/docs/v2/api/components/EcosystemProvider.mdx new file mode 100644 index 00000000..ae48f181 --- /dev/null +++ b/docs/docs/v2/api/components/EcosystemProvider.mdx @@ -0,0 +1,144 @@ +--- +id: EcosystemProvider +title: EcosystemProvider +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +```ts +import { EcosystemProvider } from '@zedux/react' +``` + +A React component that provides an [ecosystem](../classes/Ecosystem) to a component tree. The provided ecosystem will take control of all atom usages below it. + +You can pass either an existing [ecosystem](../classes/Ecosystem) via the `ecosystem` prop or any number of [EcosystemConfig properties](/not-done?path=../types/EcosystemConfig) via their respectively named props. + +When passing config properties, the EcosystemProvider will create an ecosystem for you. + +## Examples + +Passing an ecosystem: + +```tsx +import { EcosystemProvider, createEcosystem } from '@zedux/react' + +function App() { + // this is the recommended way for most apps to create an ecosystem - + // memoized in a top-level component. + const ecosystem = useMemo( + () => createEcosystem({ id: 'root', overrides: [someAtom] }), + [] + ) + + return ( + + + + ) +} +``` + +Passing configuration: + +```tsx +import { EcosystemProvider } from '@zedux/react' + +function App() { + return ( + + + + ) +} +``` + +All Zedux hooks in any child component will use the provided ecosystem. + +Live example: + +```tsx live ecosystemId=EcosystemProvider noProvide=true resultVar=App version=2 +import { useEcosystem, EcosystemProvider } from '@zedux/react' + +function Example() { + const ecosystem = useEcosystem() + + return
{ecosystem.id}
+} + +function App() { + const ecosystem = useMemo(() => createEcosystem({ id: 'root' }), []) + + return ( + <> + + + + + + + ) +} +``` + +## Signature + + + {tab1( + ` + {children} + + // or + + {children} + `, + true + )} + {tab2(`declare const EcosystemProvider: ({ + children, + ecosystem, + ...ecosystemConfig + }: + | (Partial<{ [k in keyof EcosystemConfig]: undefined }> & { + children?: ReactNode + ecosystem?: Ecosystem + }) + | (Partial & { + children?: ReactNode + ecosystem?: undefined + })) => React.JSX.Element`)} + + +## Props + +You must pass either an `ecosystem` prop or any combination of the ecosystem config props (no props is also fine) but not both. + + + + + Pass a single ReactNode child. To pass multiple components, wrap them in a React Fragment. + + + + An ecosystem created via [`createEcosystem()`](/not-done?path=../factories/createEcosystem). + + Passing this gives you the most control over the ecosystem, at the cost of being a little lower-level. + + Make sure this ecosystem reference is stable. Changing the reference is supported, but is almost never what you want as it will recreate the entire cache for all atom usages below this component. + + + + See [the EcosystemConfig type](/not-done?path=../types/EcosystemConfig) for all the other props and their types. The EcosystemConfig key names have a one-to-one mapping with props of this component. + + If the `id` prop is changed, Zedux will completely destroy the previous ecosystem and create a new one using the id and the current value of all the other EcosystemConfig props. Changing any other props besides `id` will have no effect unless `id` is also changed. + + This overload is for convenience when you don't need to configure the ecosystem much. If you need more power, pass an `ecosystem` and manage it yourself. + + + + +## See Also + +- [The Ecosystems walkthrough](../../../walkthrough/ecosystems.mdx) +- [The `EcosystemConfig` type](/not-done?path=../types/EcosystemConfig) +- [The `Ecosystem` class](../classes/Ecosystem) +- [The `useEcosystem` hook](/not-done?path=../hooks/useEcosystem) diff --git a/docs/sidebars.js b/docs/sidebars.js index 19703d19..73d86868 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -177,7 +177,10 @@ module.exports = { { type: 'category', label: 'Components', - items: ['v2/api/components/AtomProvider'], + items: [ + 'v2/api/components/AtomProvider', + 'v2/api/components/EcosystemProvider', + ], }, { type: 'category', From a653a5ed797b84afb5cdb7a173caf6cacf796946 Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Thu, 24 Jul 2025 15:49:25 -0400 Subject: [PATCH 14/49] docs: document `ion` factory for v2 --- docs/docs/v2/api/classes/AtomInstance.mdx | 59 ++++---- docs/docs/v2/api/classes/Ecosystem.mdx | 14 +- docs/docs/v2/api/classes/ZeduxNode.mdx | 10 +- docs/docs/v2/api/factories/atom.mdx | 4 +- docs/docs/v2/api/factories/ion.mdx | 159 ++++++++++++++++++++++ docs/docs/v2/api/glossary.mdx | 6 +- docs/sidebars.js | 6 +- 7 files changed, 209 insertions(+), 49 deletions(-) create mode 100644 docs/docs/v2/api/factories/ion.mdx diff --git a/docs/docs/v2/api/classes/AtomInstance.mdx b/docs/docs/v2/api/classes/AtomInstance.mdx index 29002497..091377d4 100644 --- a/docs/docs/v2/api/classes/AtomInstance.mdx +++ b/docs/docs/v2/api/classes/AtomInstance.mdx @@ -5,9 +5,9 @@ title: AtomInstance import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' -All standard atoms (aka "atom instances") are actually instances of this class. When Zedux instantiates [atom templates](./AtomTemplate) (and [ion templates](/not-done?path=./IonTemplate)), it's just creating instances of this class. +All standard atoms (aka "atom instances") are actually instances of this class. When Zedux "instantiates" [atom templates](./AtomTemplate) (including those made via [the `ion()` factory](../factories/ion)), it's just creating instances of this class. -This class extends the [`Signal` class](./Signal) (yes, atoms _are_ signals) which in turn extends the [`ZeduxNode` class](./ZeduxNode) (all atoms/signals are graph nodes). +This class extends the [`Signal` class](./Signal) (yes, atoms _are_ signals) which in turn extends the [`ZeduxNode` class](./ZeduxNode.mdx) (all atoms/signals are graph nodes). ## Creation @@ -56,7 +56,7 @@ const withParams = ecosystem.getNode(myParameterizedAtom, [ ## Destruction -Atom instances live forever by default. You can configure this with [ttl](#ttl). You can also destroy them manually via [`node.destroy()`](./ZeduxNode#destroy), or indirectly via an [ecosystem reset](./Ecosystem#reset). +Atom instances live forever by default. You can configure this with [ttl](#ttl). You can also destroy them manually via [`node.destroy()`](./ZeduxNode.mdx#destroy), or indirectly via an [ecosystem reset](./Ecosystem#reset). ### TTL @@ -72,7 +72,7 @@ You can configure this in two ways: The sky's the limit. With TTL, you can destroy atoms on page route, on log out, when the cache reaches a certain size, or anything else you can think of. :::tip -Setting `ttl: 0` on an atom template is the most common as it prevents setting any timeouts. [Ions](/not-done?path=../factories/ion) have `ttl: 0` by default. +Setting `ttl: 0` on an atom template is the most common as it prevents setting any timeouts. [Ions](../factories/ion) have `ttl: 0` by default. ::: ## Signal Wrappers @@ -119,7 +119,7 @@ There are many aspects of an atom instance's behavior you can change when extend ## Generics -For TypeScript users, atom instances have the following unique generics on their [`NodeGenerics`](./ZeduxNode#generics) (`G`) type generic: +For TypeScript users, atom instances have the following unique generics on their [`NodeGenerics`](./ZeduxNode.mdx#generics) (`G`) type generic: @@ -130,23 +130,23 @@ For TypeScript users, atom instances have the following unique generics on their -Atom instances also have the following generics inherited from [`ZeduxNode`](./ZeduxNode#generics): +Atom instances also have the following generics inherited from [`ZeduxNode`](./ZeduxNode.mdx#generics): - See [`ZeduxNode<{ Events }>`](./ZeduxNode#gevents). + See [`ZeduxNode<{ Events }>`](./ZeduxNode.mdx#gevents). - See [`ZeduxNode<{ Node }>`](./ZeduxNode#gnode). + See [`ZeduxNode<{ Node }>`](./ZeduxNode.mdx#gnode). - See [`ZeduxNode<{ Params }>`](./ZeduxNode#gparams). + See [`ZeduxNode<{ Params }>`](./ZeduxNode.mdx#gparams). - See [`ZeduxNode<{ State }>`](./ZeduxNode#gstate). + See [`ZeduxNode<{ State }>`](./ZeduxNode.mdx#gstate). - See [`ZeduxNode<{ Template }>`](./ZeduxNode#gtemplate). + See [`ZeduxNode<{ Template }>`](./ZeduxNode.mdx#gtemplate). @@ -195,14 +195,14 @@ Atom instances also inherit the following built-in events from [`Signal`](./Sign
-Atom instances also inherit the following built-in events from [`ZeduxNode`](./ZeduxNode#events): +Atom instances also inherit the following built-in events from [`ZeduxNode`](./ZeduxNode.mdx#events): - See the [node `change` event](./ZeduxNode#change-event). + See the [node `change` event](./ZeduxNode.mdx#change-event). - See the [node `cycle` event](./ZeduxNode#cycle-event). + See the [node `cycle` event](./ZeduxNode.mdx#cycle-event). @@ -271,17 +271,18 @@ Atom instances have the following **readonly** properties: -Atom instances also inherit the following properties from [`ZeduxNode`](./ZeduxNode#properties): +Atom instances also inherit the following properties from [`ZeduxNode`](./ZeduxNode.mdx#properties): - See [`ZeduxNode#id`](./ZeduxNode#id). + See [`ZeduxNode#id`](./ZeduxNode.mdx#id). - See [`ZeduxNode#params`](./ZeduxNode#params). Will always be an array. + See [`ZeduxNode#params`](./ZeduxNode.mdx#params). Will always be an array. - See [`ZeduxNode#status`](./ZeduxNode#status). + See [`ZeduxNode#status`](./ZeduxNode.mdx#status). - See [`ZeduxNode#template`](./ZeduxNode#template). Will always be a reference - to the [atom template](./AtomTemplate) this instance was created from. + See [`ZeduxNode#template`](./ZeduxNode.mdx#template). Will always be a + reference to the [atom template](./AtomTemplate) this instance was created + from. @@ -330,19 +331,23 @@ Atom instances also inherit the following methods from [`Signal`](./Signal#metho -Atom instances also inherit the following methods from [`ZeduxNode`](./ZeduxNode#methods): +Atom instances also inherit the following methods from [`ZeduxNode`](./ZeduxNode.mdx#methods): - See [`ZeduxNode.destroy`](./ZeduxNode#destroy). - See [`ZeduxNode.get`](./ZeduxNode#get). - See [`ZeduxNode.getOnce`](./ZeduxNode#getonce). - See [`ZeduxNode.on`](./ZeduxNode#on). + + See [`ZeduxNode.destroy`](./ZeduxNode.mdx#destroy). + + See [`ZeduxNode.get`](./ZeduxNode.mdx#get). + + See [`ZeduxNode.getOnce`](./ZeduxNode.mdx#getonce). + + See [`ZeduxNode.on`](./ZeduxNode.mdx#on). ## See Also -- [The Atom Instances walkthrough](/not-done?path=../../../walkthrough/atom-instances) +- [The Atom Instances walkthrough](../../../walkthrough/atom-instances) - [The `Signal` class](./Signal) -- [The `ZeduxNode` class](./ZeduxNode) +- [The `ZeduxNode` class](./ZeduxNode.mdx) - [The `AtomTemplate` class](./AtomTemplate) - [The `AtomApi` class](./AtomApi) diff --git a/docs/docs/v2/api/classes/Ecosystem.mdx b/docs/docs/v2/api/classes/Ecosystem.mdx index e16fe310..5ab313bc 100644 --- a/docs/docs/v2/api/classes/Ecosystem.mdx +++ b/docs/docs/v2/api/classes/Ecosystem.mdx @@ -780,19 +780,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context Turns an array of anything into a predictable string. - This is how all Zedux APIs generate consistent, deterministic ids for atom and selector params. This algorithm is almost exactly like [React Query's hashing algorithm](https://tanstack.com/query/latest/docs/framework/react/guides/query-keys#query-keys-are-hashed-deterministically). - - Note that circular object references are not supported. - - ```ts - // these generate the same hash: - ecosystem.hash(['todos', { status, page }]) - ecosystem.hash(['todos', { page, status }]) - - // these generate different hashes (array item order matters): - ecosystem.hash(['todos', { status, page }]) - ecosystem.hash([{ page, status }, 'todos']) - ``` + This is how all Zedux APIs generate consistent, deterministic ids for atom and selector params. See [`ZeduxNode#params`](./ZeduxNode.mdx#params) for more details. Signature: diff --git a/docs/docs/v2/api/classes/ZeduxNode.mdx b/docs/docs/v2/api/classes/ZeduxNode.mdx index f8745161..f6d11e34 100644 --- a/docs/docs/v2/api/classes/ZeduxNode.mdx +++ b/docs/docs/v2/api/classes/ZeduxNode.mdx @@ -187,18 +187,18 @@ Every node has the following **readonly** properties: instanceB.params // ['a', 'b'] ``` - All params must be serializable (no functions or symbols)! This is because - Zedux converts the params to a stable string representation in order to - efficiently check for an existing atom instance with the "same" params. + All params must be serializable (no functions or symbols)! This is because Zedux converts the params to a stable string representation in order to efficiently check for an existing atom instance with the "same" params. - Sameness is determined by deep value comparison, not reference equality. Order matters! + Sameness is determined by deep value comparison, not reference equality. Order matters! This algorithm is almost exactly like [React Query's hashing algorithm](https://tanstack.com/query/latest/docs/framework/react/guides/query-keys#query-keys-are-hashed-deterministically). + + Note that circular object references are not supported. ```ts // These params are the "same" in Zedux's eyes: useAtomInstance(myAtom, ['a', { b: 'b', c: 'c' }]) useAtomInstance(myAtom, ['a', { c: 'c', b: 'b' }]) - // But these are different: + // But these are different (array item order matters): useAtomInstance(myAtom, ['a', 'b']) useAtomInstance(myAtom, ['b', 'a']) ``` diff --git a/docs/docs/v2/api/factories/atom.mdx b/docs/docs/v2/api/factories/atom.mdx index dce5095c..f33c86cb 100644 --- a/docs/docs/v2/api/factories/atom.mdx +++ b/docs/docs/v2/api/factories/atom.mdx @@ -195,7 +195,7 @@ function App() { - A raw value. Can be anything except a function. When the atom is instantiated, this value as-is will be its initial state. - - A state factory function that returns a raw value. That raw value can be anything (including a function). The returned value will be the atom instance's initial state. + - A [state factory](../glossary.mdx#state-factory) function that returns a raw value. That raw value can be anything (including a function). The returned value will be the atom instance's initial state. - A state factory function that returns a [signal](../classes/Signal). When the atom is instantiated, the new atom instance will become a thin wrapper around the returned signal, forwarding events and state changes between it and the atom's own observers. @@ -215,6 +215,8 @@ function App() { Any [`ttl`](../classes/AtomApi#ttl) configured in the returned Atom API will control the atom instance's destruction timing. + When a state factory function is passed, any parameters defined on the function will become the [params](../classes/AtomInstance.mdx#params) of the atom. + Optional. An [AtomConfig](/not-done?path=../types/AtomConfig) object. diff --git a/docs/docs/v2/api/factories/ion.mdx b/docs/docs/v2/api/factories/ion.mdx new file mode 100644 index 00000000..72f6ed11 --- /dev/null +++ b/docs/docs/v2/api/factories/ion.mdx @@ -0,0 +1,159 @@ +--- +id: ion +title: ion +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +```ts +import { ion } from '@zedux/react' +``` + +A factory for creating specially-configured [atom templates](../classes/AtomTemplate.mdx) that specialize in derivations. Ions are just atoms with two differences: + +- The first parameter of the state factory will be the `ecosystem`, so you can easily `get(otherAtom)` +- The atom's `ttl` will be set to `0` by default, destroying instances of this atom as soon as they're no longer used. You can override this by passing a different `ttl` on the third `config` parameter to the `ion()` factory or by returning an AtomApi from the ion's state factory with a TTL configured via [`.setTtl()`](../classes/AtomApi.mdx#setttl). + +## Example + +```tsx live ecosystemId=ion/example resultVar=App version=2 +const countAtom = atom('count', 0) +const doubleCountAtom = atom('doubleCount', 1) + +const sumIon = ion('sum', ({ get }) => { + const count = get(countAtom) + const doubleCount = get(doubleCountAtom) + + return count + doubleCount +}) + +function App() { + const [count, setCount] = useAtomState(countAtom) + const [doubleCount, setDoubleCount] = useAtomState(doubleCountAtom) + const sum = useAtomValue(sumIon) + + return ( +
+
Count: {count}
+
Double Count: {doubleCount}
+
Sum: {sum}
+ + +
+ ) +} +``` + +## Signature + + + {tab1(`ion = (key, stateFactory, config?) => newIon`)} + {tab2(`declare const ion: { + = None>(key: string, value: (ecosystem: Ecosystem, ...params: Params) => AtomApi<{ + Exports: Exports; + Promise: any; + Signal: undefined; + State: Promise; + }>, config?: AtomConfig): IonTemplateRecursive<{ + State: PromiseState; + Params: Params; + Events: None; + Exports: Exports; + Promise: Promise; + }>; + = None, Params extends any[] = [], Exports extends Record = None, PromiseType extends AtomApiPromise = undefined, ResolvedState = StateType>(key: string, value: (ecosystem: Ecosystem, ...params: Params) => Signal<{ + Events: EventsType; + Params: any; + State: StateType; + Template: any; + }> | AtomApi<{ + Exports: Exports; + Promise: PromiseType; + Signal: Signal<{ + Events: EventsType; + Params: any; + State: StateType; + Template: any; + }>; + State: StateType; + }> | Signal<{ + Events: EventsType; + Params: any; + ResolvedState: ResolvedState; + State: StateType; + Template: any; + }> | AtomApi<{ + Exports: Exports; + Promise: PromiseType; + Signal: Signal<{ + Events: EventsType; + Params: any; + ResolvedState: ResolvedState; + State: StateType; + Template: any; + }>; + State: StateType; + }>, config?: AtomConfig): IonTemplateRecursive<{ + State: StateType; + Params: Params; + Events: EventsType; + Exports: Exports; + Promise: PromiseType; + ResolvedState: ResolvedState; + }>; + = None, PromiseType extends AtomApiPromise = undefined>(key: string, value: (ecosystem: Ecosystem, ...params: Params) => AtomApi<{ + Exports: Exports; + Promise: PromiseType; + Signal: undefined; + State: State; + }> | State, config?: AtomConfig): IonTemplateRecursive<{ + State: State; + Params: Params; + Events: None; + Exports: Exports; + Promise: PromiseType; + }>; + = None, EventsType extends Record = None, PromiseType extends AtomApiPromise = undefined>(key: string, value: IonStateFactory<{ + State: State; + Params: Params; + Events: EventsType; + Exports: Exports; + Promise: PromiseType; + }>, config?: AtomConfig): IonTemplateRecursive<{ + State: State; + Params: Params; + Events: EventsType; + Exports: Exports; + Promise: PromiseType; + }>; +}`)} + + + + + Required. A string. This generally needs to be unique across your codebase. See [the `atom()` factory's key prop](./atom.mdx#key) for details. + + + + Required. A function that receives the ecosystem as the first parameter. The remaining parameters are the parameters of the atom. + + See [the `atom()` factory](./atom.mdx#valueorfactory) for details on what the state factory can return and how that determines the atom's behavior. + + + + Optional. An [AtomConfig](/not-done?path=../types/AtomConfig) object. + + + + An ion template (selector template) that can be used with atom hooks and ecosystem methods. + + + + +## See Also + +- [The SelectorInstance class](../classes/SelectorInstance.mdx) +- [The AtomApi class](../classes/AtomApi.mdx) +- [The Selectors walkthrough](../../../walkthrough/selectors.mdx) diff --git a/docs/docs/v2/api/glossary.mdx b/docs/docs/v2/api/glossary.mdx index bbb159d2..17c60b4b 100644 --- a/docs/docs/v2/api/glossary.mdx +++ b/docs/docs/v2/api/glossary.mdx @@ -86,9 +86,11 @@ An atom that contains one or more [`inject`](/not-done?path=./injectors/inject) ### State Factory -A function passed to [`atom()`](./factories/atom) (or other atom factory functions like [`ion()`](/not-done?path=./factories/ion)). This function is called to produce the initial value of the atom instance. It also runs every time an atom instance reevaluates. +A function passed to [`atom()`](./factories/atom) (or other atom factory functions like [`ion()`](./factories/ion)). This function is called to produce the initial value of the atom instance. It also runs every time an atom instance reevaluates. -These are similar to render functions in React. Except of course they return state instead of UI. +You can add as many parameters as you want to this function. TS users can specify the types of these params to guarantee that all consumers pass the correct values when accessing the atom via Zedux's [injectors](#injector), React hooks, or [ecosystem](./classes/Ecosystem) methods. Each unique set of params will create a new atom instance. See [`ZeduxNode#params`](./classes/ZeduxNode.mdx#params) for details on how params are stringified. + +State factories are similar to render functions in React. Except of course they return state instead of UI. ### Static Graph Dependency diff --git a/docs/sidebars.js b/docs/sidebars.js index 73d86868..a8dedb22 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -190,7 +190,11 @@ module.exports = { { type: 'category', label: 'Factories', - items: ['v2/api/factories/api', 'v2/api/factories/atom'], + items: [ + 'v2/api/factories/api', + 'v2/api/factories/atom', + 'v2/api/factories/ion', + ], }, 'v2/api/glossary', ], From a4bc835eb8794b6085d50f31f78a4ae17e88120e Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Thu, 24 Jul 2025 16:04:12 -0400 Subject: [PATCH 15/49] docs: document `MappedSignal` class for v2 --- docs/docs/v2/api/classes/AtomApi.mdx | 2 +- docs/docs/v2/api/classes/AtomInstance.mdx | 2 + docs/docs/v2/api/classes/MappedSignal.mdx | 118 ++++++++++++++++++++++ docs/docs/v2/api/classes/Signal.mdx | 4 +- docs/sidebars.js | 1 + 5 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 docs/docs/v2/api/classes/MappedSignal.mdx diff --git a/docs/docs/v2/api/classes/AtomApi.mdx b/docs/docs/v2/api/classes/AtomApi.mdx index 5546e472..1c0a6628 100644 --- a/docs/docs/v2/api/classes/AtomApi.mdx +++ b/docs/docs/v2/api/classes/AtomApi.mdx @@ -201,7 +201,7 @@ AtomApis expose the following **readonly** properties:
- A reference to the value passed to the [`api()` factory](../factories/api.mdx) itself. Can be any raw value, a promise, or a [signal](./Signal) (including a [mapped signal](/not-done?path=./MappedSignal) or even another [atom instance](./AtomInstance.mdx)). + A reference to the value passed to the [`api()` factory](../factories/api.mdx) itself. Can be any raw value, a promise, or a [signal](./Signal) (including a [mapped signal](./MappedSignal.mdx) or even another [atom instance](./AtomInstance.mdx)). If it's a signal and this AtomApi is returned from a state factory, the signal should be a stable reference that won't change on subsequent evaluations, e.g. by using [`injectSignal`](../injectors/injectSignal). diff --git a/docs/docs/v2/api/classes/AtomInstance.mdx b/docs/docs/v2/api/classes/AtomInstance.mdx index 091377d4..f4698596 100644 --- a/docs/docs/v2/api/classes/AtomInstance.mdx +++ b/docs/docs/v2/api/classes/AtomInstance.mdx @@ -83,6 +83,8 @@ Since atoms are signals, consumers never need to know if the atom is a signal wr This is a change from v1. In v1, all atoms had a `.store` property. This made atoms a bit heavier and less abstract - for example, there were subtle differences between `atom.store.setState` and `atom.setState`. In v2, you never need to worry about this. Just always call [`atom.set()`](#set) or [`atom.mutate()`](#mutate) directly. +Functionally, this signal wrapping is very similar to how [mapped signals](./MappedSignal.mdx) work. + ## Providing An atom instance can be provided over React context via [``](../components/AtomProvider). diff --git a/docs/docs/v2/api/classes/MappedSignal.mdx b/docs/docs/v2/api/classes/MappedSignal.mdx new file mode 100644 index 00000000..9c7dd9bf --- /dev/null +++ b/docs/docs/v2/api/classes/MappedSignal.mdx @@ -0,0 +1,118 @@ +--- +id: MappedSignal +title: MappedSignal +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +The object returned by [`injectMappedSignal`](/not-done?path=../injectors/injectMappedSignal). Mapped signals _are_ signals, meaning this class extends [the `Signal` class](./Signal.mdx) (which in turn extends [the `ZeduxNode` class](./ZeduxNode.mdx)). All signal operations work on mapped signals. + +Mapped signals are composed of any number of "inner signals" plus any number of other state fields. They're aware of which inner signal controls each piece of state and forward all state changes to the relevant inner signal(s). They also forward events to and from inner signals. + +The state of a mapped signal is always an object. Mapped signals don't support nested objects. To nest signals, create multiple mapped signals. See [`injectMappedSignal`](/not-done?path=../injectors/injectMappedSignal) for an example. + +The keys of a mapped signal's object never change. They're defined when `injectMappedSignal` is first called in an atom and stay for the lifetime of the mapped signal. + +:::tip +You do not need to use mapped signals for all nested state. Normal signals are more than capable of handling even deeply-nested state. + +Mapped signals are primarily for composing multiple signals inside an atom into a single signal that can be returned from the atom's state factory. +::: + +## Creation + +You never instantiate this class yourself. Mapped signals can currently only be created via [the `injectMappedSignal` injector](/not-done?path=../injectors/injectMappedSignal): + +```ts +const exampleAtom = atom('example', () => { + const signal = injectSignal(0) + const mappedSignal = injectMappedSignal({ inner: signal }) + + return mappedSignal // make this signal control the atom's state +}) +``` + +## Generics + +For TypeScript users, mapped signals have the following generics inherited from [`ZeduxNode`](./ZeduxNode.mdx#generics): + + + + See [`ZeduxNode<{ Events }>`](./ZeduxNode.mdx#gevents). + + + See [`ZeduxNode<{ Params }>`](./ZeduxNode.mdx#gparams). + + + See [`ZeduxNode<{ State }>`](./ZeduxNode.mdx#gstate). + + + See [`ZeduxNode<{ Template }>`](./ZeduxNode.mdx#gtemplate). + + + +## Events + +Mapped signals inherit all [custom events](./Signal.mdx#custom-events) from their inner signals. They can also define their own. See [`injectMappedSignal`'s `events`](/not-done?path=../injectors/injectMappedSignal#events) config option. + +Mapped signals also inherit the following built-in events from the [`Signal` class](./Signal.mdx#events): + + + + See the [signal `mutate` event](./Signal.mdx#mutate-event). + + + +Mapped signals also inherit the following built-in events from the [`ZeduxNode` class](./ZeduxNode.mdx#events): + + + + See the [node `change` event](./ZeduxNode.mdx#change-event). + + + See the [node `cycle` event](./ZeduxNode.mdx#cycle-event). + + + +## Properties + +Mapped signals inherit the following **readonly** properties from the [`ZeduxNode` class](./ZeduxNode.mdx#properties): + + + See [`ZeduxNode#id`](./ZeduxNode.mdx#id). + + See [`ZeduxNode#params`](./ZeduxNode.mdx#params). This will always be + undefined - mapped signals don't take params. + + + See [`ZeduxNode#status`](./ZeduxNode.mdx#status). This will never be "Stale" + - mapped signals skip from Active to Destroyed. + + + See [`ZeduxNode#template`](./ZeduxNode.mdx#template). This will always be + undefined - mapped signals don't have templates. + + + +## Methods + +Mapped signals inherit the following methods from the [`Signal` class](./Signal.mdx#methods): + + + See [`Signal#mutate`](./Signal.mdx#mutate). + See [`Signal#send`](./Signal.mdx#send). + See [`Signal#set`](./Signal.mdx#set). + + +Mapped signals also inherit the following methods from the [`ZeduxNode` class](./ZeduxNode.mdx#methods): + + + + See [`ZeduxNode#destroy`](./ZeduxNode.mdx#destroy). + + See [`ZeduxNode#get`](./ZeduxNode.mdx#get). + + See [`ZeduxNode#getOnce`](./ZeduxNode.mdx#getonce). + + See [`ZeduxNode#on`](./ZeduxNode.mdx#on). + diff --git a/docs/docs/v2/api/classes/Signal.mdx b/docs/docs/v2/api/classes/Signal.mdx index 3c477bbd..b480eec5 100644 --- a/docs/docs/v2/api/classes/Signal.mdx +++ b/docs/docs/v2/api/classes/Signal.mdx @@ -9,7 +9,7 @@ The object returned by [`injectSignal`](../injectors/injectSignal). Signals are Atoms themselves are signals. That simply means the [AtomInstance](./AtomInstance) class extend this class. An atom can also contain many inner signals. When a signal is returned from an [atom state factory](../factories/atom#valueorfactory), the atom becomes a thin wrapper around the returned signal. -[Mapped signals](/not-done?path=./MappedSignal) are also signals themselves. +[Mapped signals](./MappedSignal.mdx) are also signals themselves. As of v2, signals have replaced stores as the primary state container. @@ -467,6 +467,6 @@ Signals also inherit the following methods from [`ZeduxNode`](./ZeduxNode#method ## See Also -- [The `MappedSignal` class](/not-done?path=./MappedSignal) +- [The `MappedSignal` class](./MappedSignal.mdx) - [The `ZeduxNode` class](./ZeduxNode) - [The `AtomInstance` class](./AtomInstance) diff --git a/docs/sidebars.js b/docs/sidebars.js index a8dedb22..53a12e28 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -169,6 +169,7 @@ module.exports = { 'v2/api/classes/AtomInstance', 'v2/api/classes/AtomTemplate', 'v2/api/classes/Ecosystem', + 'v2/api/classes/MappedSignal', 'v2/api/classes/SelectorInstance', 'v2/api/classes/Signal', 'v2/api/classes/ZeduxNode', From 040c9b2ecfbfa6381b77060611edc6c238783f6a Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Thu, 24 Jul 2025 16:12:11 -0400 Subject: [PATCH 16/49] docs: document `useEcosystem` hook for v2 --- docs/docs/v2/api/classes/Ecosystem.mdx | 2 +- .../v2/api/components/EcosystemProvider.mdx | 4 +- docs/docs/v2/api/hooks/useEcosystem.mdx | 60 +++++++++++++++++++ docs/sidebars.js | 15 +++-- 4 files changed, 73 insertions(+), 8 deletions(-) create mode 100644 docs/docs/v2/api/hooks/useEcosystem.mdx diff --git a/docs/docs/v2/api/classes/Ecosystem.mdx b/docs/docs/v2/api/classes/Ecosystem.mdx index 5ab313bc..ff3b46b3 100644 --- a/docs/docs/v2/api/classes/Ecosystem.mdx +++ b/docs/docs/v2/api/classes/Ecosystem.mdx @@ -1098,5 +1098,5 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context - [`getDefaultEcosystem()`](/not-done?path=../utils/getDefaultEcosystem) - [`setDefaultEcosystem()`](/not-done?path=../utils/setDefaultEcosystem) - [the `` component](../components/EcosystemProvider) -- [the `useEcosystem` hook](/not-done?path=../hooks/useEcosystem) +- [the `useEcosystem` hook](../hooks/useEcosystem.mdx) - [the `injectEcosystem` injector](/not-done?path=../injectors/injectEcosystem) diff --git a/docs/docs/v2/api/components/EcosystemProvider.mdx b/docs/docs/v2/api/components/EcosystemProvider.mdx index ae48f181..49729d78 100644 --- a/docs/docs/v2/api/components/EcosystemProvider.mdx +++ b/docs/docs/v2/api/components/EcosystemProvider.mdx @@ -140,5 +140,5 @@ You must pass either an `ecosystem` prop or any combination of the ecosystem con - [The Ecosystems walkthrough](../../../walkthrough/ecosystems.mdx) - [The `EcosystemConfig` type](/not-done?path=../types/EcosystemConfig) -- [The `Ecosystem` class](../classes/Ecosystem) -- [The `useEcosystem` hook](/not-done?path=../hooks/useEcosystem) +- [The `Ecosystem` class](../classes/Ecosystem.mdx) +- [The `useEcosystem` hook](../hooks/useEcosystem.mdx) diff --git a/docs/docs/v2/api/hooks/useEcosystem.mdx b/docs/docs/v2/api/hooks/useEcosystem.mdx new file mode 100644 index 00000000..6e0ad98f --- /dev/null +++ b/docs/docs/v2/api/hooks/useEcosystem.mdx @@ -0,0 +1,60 @@ +--- +id: useEcosystem +title: useEcosystem +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +```ts +import { useEcosystem } from '@zedux/react' +``` + +A React hook that returns the [ecosystem](../../../walkthrough/ecosystems.mdx) provided to the current component tree via [``](../components/EcosystemProvider.mdx). + +Returns the [default ecosystem](../classes/Ecosystem.mdx#default-ecosystem) if no ecosystem is provided, creating it if it hasn't been created yet. + +## Example + +```tsx live ecosystemId=useEcosystem noProvide=true resultVar=App version=2 +import { useEcosystem, EcosystemProvider } from '@zedux/react' + +function Example() { + const ecosystem = useEcosystem() + + return
{ecosystem.id}
+} + +function App() { + const ecosystem = useMemo(() => createEcosystem({ id: 'root' }), []) + + return ( + <> + + + + + + + ) +} +``` + +## Signature + + + {tab1(`useEcosystem = () => Ecosystem`)} + {tab2(`declare const useEcosystem: () => Ecosystem`)} + + + + + The [ecosystem](../classes/Ecosystem.mdx) provided to the current component tree. Returns the [default ecosystem](../classes/Ecosystem.mdx#default-ecosystem) if no ecosystem is provided. + + + + +## See Also + +- [The `Ecosystem` class](../classes/Ecosystem.mdx) +- [The `` component](../components/EcosystemProvider.mdx) +- [The `getDefaultEcosystem()` utility](/not-done?path=../utils/getDefaultEcosystem.mdx) diff --git a/docs/sidebars.js b/docs/sidebars.js index 53a12e28..c78430e7 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -183,11 +183,6 @@ module.exports = { 'v2/api/components/EcosystemProvider', ], }, - { - type: 'category', - label: 'Injectors', - items: ['v2/api/injectors/injectSignal'], - }, { type: 'category', label: 'Factories', @@ -197,6 +192,16 @@ module.exports = { 'v2/api/factories/ion', ], }, + { + type: 'category', + label: 'Hooks', + items: ['v2/api/hooks/useEcosystem'], + }, + { + type: 'category', + label: 'Injectors', + items: ['v2/api/injectors/injectSignal'], + }, 'v2/api/glossary', ], }, From 81d257feda79dc5bf9e14c99b90576e857b9fa29 Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Thu, 24 Jul 2025 16:20:07 -0400 Subject: [PATCH 17/49] docs: document `createEcosystem` factory for v2 --- docs/docs/v2/api/classes/Ecosystem.mdx | 10 +-- .../v2/api/components/EcosystemProvider.mdx | 2 +- .../docs/v2/api/factories/createEcosystem.mdx | 63 +++++++++++++++++++ docs/sidebars.js | 1 + 4 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 docs/docs/v2/api/factories/createEcosystem.mdx diff --git a/docs/docs/v2/api/classes/Ecosystem.mdx b/docs/docs/v2/api/classes/Ecosystem.mdx index ff3b46b3..911a2a29 100644 --- a/docs/docs/v2/api/classes/Ecosystem.mdx +++ b/docs/docs/v2/api/classes/Ecosystem.mdx @@ -20,7 +20,7 @@ Ecosystems can be used completely outside of React. This can be helpful for test ## Creation -Create ecosystems with [the `createEcosystem()` factory](/not-done?path=../factories/createEcosystem). +Create ecosystems with [the `createEcosystem()` factory](../factories/createEcosystem.mdx). ```ts import { createEcosystem } from '@zedux/react' @@ -48,7 +48,7 @@ The [default ecosystem](../../../walkthrough/ecosystems#global) will be created The default ecosystem is great for simple apps. It's a full ecosystem, which means you can use features like [overrides](#overrides) and [ecosystem events](#events). However, it comes preconfigured with no (good) way to set config options like [`ssr`](#ssr) and [`onReady`](#onready). -It's generally recommended to create your own ecosystem(s) via [`createEcosystem()`](/not-done?path=../factories/createEcosystem) and provide them to your app via [``](../components/EcosystemProvider) instead of using the default ecosystem. This is especially needed for SSR. +It's generally recommended to create your own ecosystem(s) via [`createEcosystem()`](../factories/createEcosystem.mdx) and provide them to your app via [``](../components/EcosystemProvider) instead of using the default ecosystem. This is especially needed for SSR. ## Providing @@ -336,7 +336,7 @@ Every ecosystem has the following properties. All properties are readonly!
- An object. May be undefined. A reference to the `context` object passed to [the `createEcosystem()` factory](/not-done?path=../factories/createEcosystem) (if any) or the latest [`.reset()`](#reset) call. + An object. May be undefined. A reference to the `context` object passed to [the `createEcosystem()` factory](../factories/createEcosystem.mdx) (if any) or the latest [`.reset()`](#reset) call. When `.reset()` is called, the previous context (if any) will be passed as the second parameter to the `onReady` function as part of the reset. @@ -835,7 +835,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context Generates a consistent id that is guaranteed to be unique in this ecosystem, but not at all guaranteed to be unique globally. - You can override this by passing the [`makeId`](/not-done?path=../types/EcosystemConfig#makeid) option to [`createEcosystem`](/not-done?path=../factories/createEcosystem). The default implementation is suitable for most use cases, including: + You can override this by passing the [`makeId`](/not-done?path=../types/EcosystemConfig#makeid) option to [`createEcosystem`](../factories/createEcosystem.mdx). The default implementation is suitable for most use cases, including: - apps that use only one ecosystem (the most common). - snapshot testing the ecosystem graph and dehydrations - calling `ecosystem.reset()` after each test will reset the ecosystem's id counter. @@ -1092,7 +1092,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context ## See Also - [the Ecosystems walkthrough](../../../walkthrough/ecosystems) -- [the `createEcosystem()` factory](/not-done?path=../factories/createEcosystem) +- [the `createEcosystem()` factory](../factories/createEcosystem.mdx) - [the Overrides walkthrough](../../../walkthrough/overrides) - [the Plugins guide](../../../advanced/plugins) - [`getDefaultEcosystem()`](/not-done?path=../utils/getDefaultEcosystem) diff --git a/docs/docs/v2/api/components/EcosystemProvider.mdx b/docs/docs/v2/api/components/EcosystemProvider.mdx index 49729d78..89be727d 100644 --- a/docs/docs/v2/api/components/EcosystemProvider.mdx +++ b/docs/docs/v2/api/components/EcosystemProvider.mdx @@ -119,7 +119,7 @@ You must pass either an `ecosystem` prop or any combination of the ecosystem con - An ecosystem created via [`createEcosystem()`](/not-done?path=../factories/createEcosystem). + An ecosystem created via [`createEcosystem()`](../factories/createEcosystem.mdx). Passing this gives you the most control over the ecosystem, at the cost of being a little lower-level. diff --git a/docs/docs/v2/api/factories/createEcosystem.mdx b/docs/docs/v2/api/factories/createEcosystem.mdx new file mode 100644 index 00000000..7e453601 --- /dev/null +++ b/docs/docs/v2/api/factories/createEcosystem.mdx @@ -0,0 +1,63 @@ +--- +id: createEcosystem +title: createEcosystem +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +```ts +import { createEcosystem } from '@zedux/react' +``` + +Creates an [ecosystem](../classes/Ecosystem.mdx) for managing atoms, selectors, signals, and the graph they form. + +## Examples + +```ts +import { createEcosystem } from '@zedux/react' + +const rootEcosystem = createEcosystem({ id: 'root' }) + +const withOverrides = createEcosystem({ + id: 'withOverrides', + overrides: [atomA, atomB], +}) + +const withOnReadyFn = createEcosystem({ + id: 'withOnReadyFn', + onReady: ecosystem => { + // `onReady` is passed a reference to the ecosystem + ecosystem.getNode(myAtom) // preload myAtom + }, +}) + +const forSsr = createEcosystem({ ssr: true }) +``` + +## Signature + + + {tab1(`createEcosystem = (config?) => Ecosystem`)} + {tab2(`declare const createEcosystem: < + Context extends Record | undefined = any + >( + config?: EcosystemConfig + ) => Ecosystem`)} + + + + + Optional. An [EcosystemConfig](/not-done?path=../types/EcosystemConfig) object. + + All fields are optional. The `id` is just for debugging and does not have to be unique (though of course uniqueness is recommended). + + + + An [Ecosystem](../classes/Ecosystem.mdx) instance, configured with the passed `config`. + + + +## See Also + +- [The Ecosystems walkthrough](../../../walkthrough/ecosystems.mdx) +- [The `Ecosystem` class](../classes/Ecosystem.mdx) diff --git a/docs/sidebars.js b/docs/sidebars.js index c78430e7..1995dd95 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -189,6 +189,7 @@ module.exports = { items: [ 'v2/api/factories/api', 'v2/api/factories/atom', + 'v2/api/factories/createEcosystem', 'v2/api/factories/ion', ], }, From ecf9bd9673a95c644d0c1ca9e61e1ffe0212b0ee Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Thu, 24 Jul 2025 16:24:53 -0400 Subject: [PATCH 18/49] docs: document `injectEcosystem` injector for v2 --- docs/docs/v2/api/classes/Ecosystem.mdx | 2 +- docs/docs/v2/api/glossary.mdx | 4 +- .../docs/v2/api/injectors/injectEcosystem.mdx | 76 +++++++++++++++++++ docs/sidebars.js | 5 +- 4 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 docs/docs/v2/api/injectors/injectEcosystem.mdx diff --git a/docs/docs/v2/api/classes/Ecosystem.mdx b/docs/docs/v2/api/classes/Ecosystem.mdx index 911a2a29..c9ae4ad5 100644 --- a/docs/docs/v2/api/classes/Ecosystem.mdx +++ b/docs/docs/v2/api/classes/Ecosystem.mdx @@ -1099,4 +1099,4 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context - [`setDefaultEcosystem()`](/not-done?path=../utils/setDefaultEcosystem) - [the `` component](../components/EcosystemProvider) - [the `useEcosystem` hook](../hooks/useEcosystem.mdx) -- [the `injectEcosystem` injector](/not-done?path=../injectors/injectEcosystem) +- [the `injectEcosystem` injector](../injectors/injectEcosystem.mdx) diff --git a/docs/docs/v2/api/glossary.mdx b/docs/docs/v2/api/glossary.mdx index 17c60b4b..3cc2e1d0 100644 --- a/docs/docs/v2/api/glossary.mdx +++ b/docs/docs/v2/api/glossary.mdx @@ -41,7 +41,7 @@ There are 3 basic types of injectors: - React-hook equivalents, like [`injectEffect`](/not-done?path=./injectors/injectEffect), [`injectMemo`](/not-done?path=./injectors/injectMemo), and [`injectRef`](/not-done?path=./injectors/injectRef). - Dependency injectors, like [`injectAtomValue`](/not-done?path=./injectors/injectAtomValue) and [`injectAtomInstance`](/not-done?path=./injectors/injectAtomInstance). -- Utility or dev X injectors, such as [`injectEcosystem`](/not-done?path=./injectors/injectEcosystem) and [`injectWhy`](/not-done?path=./injectors/injectWhy). +- Utility or dev X injectors, such as [`injectEcosystem`](./injectors/injectEcosystem.mdx) and [`injectWhy`](/not-done?path=./injectors/injectWhy). Injectors should only be used at the top level of [atom state factories](#state-factory). Don't use them in loops or conditional statements. @@ -106,4 +106,4 @@ An [injector](#injector) whose use isn't restricted like normal injectors. An un You usually won't need to worry about this distinction. Just use them like normal injectors and you'll be fine. -Examples of unrestricted injectors include [`injectEcosystem()`](/not-done?path=./injectors/injectEcosystem), [`injectSelf()`](/not-done?path=./injectors/injectSelf), and [`injectWhy()`](/not-done?path=./injectors/injectWhy). +Examples of unrestricted injectors include [`injectEcosystem()`](./injectors/injectEcosystem.mdx), [`injectSelf()`](/not-done?path=./injectors/injectSelf), and [`injectWhy()`](/not-done?path=./injectors/injectWhy). diff --git a/docs/docs/v2/api/injectors/injectEcosystem.mdx b/docs/docs/v2/api/injectors/injectEcosystem.mdx new file mode 100644 index 00000000..615d97c1 --- /dev/null +++ b/docs/docs/v2/api/injectors/injectEcosystem.mdx @@ -0,0 +1,76 @@ +--- +id: injectEcosystem +title: injectEcosystem +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +```ts +import { injectEcosystem } from '@zedux/react' +``` + +An [injector](../glossary.mdx#injector) that returns the [ecosystem](../classes/Ecosystem.mdx) the currently-evaluating atom belongs to. + +You can use any methods or properties on the ecosystem either during or after evaluation. Note that the ecosystem's getter methods may register dependencies depending on when they're called: + +Calling [`ecosystem.get`](../classes/Ecosystem.mdx#get) during evaluation will register a [dynamic dependency](../glossary.mdx#dynamic-graph-dependency) on the retrieved atom, or selector. Calling [`ecosystem.getNode`](../classes/Ecosystem.mdx#getnode) during evaluation will register a [static dependency](../glossary.mdx#static-graph-dependency) on the retrieved atom or selector. + +Use [`ecosystem.getOnce`](../classes/Ecosystem.mdx#getonce) to retrieve an atom or selector value without registering a dependency. Use [`ecosystem.getNodeOnce`](../classes/Ecosystem.mdx#getnodeonce) to retrieve an atom or selector instance without registering a dependency. + +## Example + +```tsx live ecosystemId=injectEcosystem/example resultVar=App version=2 +const helloAtom = atom('hello', 'Hello') + +const derivedAtom = atom('derived', () => { + const ecosystem = injectEcosystem() + + // register a dynamic dependency on `helloAtom` via `ecosystem.get` + const hello = ecosystem.get(helloAtom) + const timestamp = new Date().toISOString() + + // set atoms via `ecosystem.getNode(...).set + const setSource = (newValue: string) => { + ecosystem.getNode(helloAtom).set(newValue) + } + + return api(`${hello} at ${timestamp}`).setExports({ setSource }) +}) + +function App() { + const [hello, setHello] = useAtomState(helloAtom) + const [derived, { setSource }] = useAtomState(derivedAtom) + const ecosystem = useEcosystem() + + return ( +
+
Source: {hello}
+
Derived: {derived}
+ + +
+ ) +} +``` + + + {tab1(`injectEcosystem = () => ecosystem`)} + {tab2(`declare const injectEcosystem: () => Ecosystem`)} + + + + + An [ecosystem](../classes/Ecosystem.mdx) class instance. This is the + ecosystem that controls the currently-evaluating atom. + + + +## See Also + +- [The `Ecosystem` class](../classes/Ecosystem.mdx) +- [The `useEcosystem` hook](../hooks/useEcosystem.mdx) +- [The Ecosystems walkthrough](../../../walkthrough/ecosystems.mdx) diff --git a/docs/sidebars.js b/docs/sidebars.js index 1995dd95..791ee8a9 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -201,7 +201,10 @@ module.exports = { { type: 'category', label: 'Injectors', - items: ['v2/api/injectors/injectSignal'], + items: [ + 'v2/api/injectors/injectEcosystem', + 'v2/api/injectors/injectSignal', + ], }, 'v2/api/glossary', ], From 2a4365c38efaf205a1a136412c6bcff0c0e98902 Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Thu, 24 Jul 2025 16:42:38 -0400 Subject: [PATCH 19/49] docs: document `useAtomContext` hook for v2 --- docs/docs/v2/api/classes/AtomInstance.mdx | 2 +- docs/docs/v2/api/components/AtomProvider.mdx | 4 +- docs/docs/v2/api/hooks/useAtomContext.mdx | 117 +++++++++++++++++++ docs/sidebars.js | 2 +- 4 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 docs/docs/v2/api/hooks/useAtomContext.mdx diff --git a/docs/docs/v2/api/classes/AtomInstance.mdx b/docs/docs/v2/api/classes/AtomInstance.mdx index f4698596..c6d6125d 100644 --- a/docs/docs/v2/api/classes/AtomInstance.mdx +++ b/docs/docs/v2/api/classes/AtomInstance.mdx @@ -104,7 +104,7 @@ function Parent() { } ``` -Consume provided instances with [`useAtomContext()`](/not-done?path=../hooks/useAtomContext) +Consume provided instances with [`useAtomContext()`](../hooks/useAtomContext.mdx). ```ts import { useAtomContext } from '@zedux/react' diff --git a/docs/docs/v2/api/components/AtomProvider.mdx b/docs/docs/v2/api/components/AtomProvider.mdx index 10a6019a..cee988ca 100644 --- a/docs/docs/v2/api/components/AtomProvider.mdx +++ b/docs/docs/v2/api/components/AtomProvider.mdx @@ -11,7 +11,7 @@ import { AtomProvider } from '@zedux/react' A component that provides one or more [atom instances](../classes/AtomInstance) over React context. -Use [`useAtomContext()`](/not-done?path=../hooks/useAtomContext) to consume the provided instance(s). +Use [`useAtomContext()`](../hooks/useAtomContext.mdx) to consume the provided instance(s). ## Example @@ -159,5 +159,5 @@ AtomProvider accepts **either** an `instance` prop to provide a single atom inst ## See Also - [The React Context walkthrough](../../../walkthrough/react-context.mdx) -- [The `useAtomContext` hook](/not-done?path=../hooks/useAtomContext) +- [The `useAtomContext` hook](../hooks/useAtomContext.mdx) - [The `useAtomInstance` hook](/not-done?path=../hooks/useAtomInstance) diff --git a/docs/docs/v2/api/hooks/useAtomContext.mdx b/docs/docs/v2/api/hooks/useAtomContext.mdx new file mode 100644 index 00000000..6a22fa13 --- /dev/null +++ b/docs/docs/v2/api/hooks/useAtomContext.mdx @@ -0,0 +1,117 @@ +--- +id: useAtomContext +title: useAtomContext +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +```ts +import { useAtomContext } from '@zedux/react' +``` + +A React hook that retrieves an [atom instance](../classes/AtomInstance.mdx) provided over React context via an [``](../components/AtomProvider.mdx). + +This hook only returns the atom instance itself. To make the consuming component rerender when the provided atom updates, pass the returned atom instance to [`useAtomValue`](/not-done?path=./useAtomValue.mdx) or [`useAtomState`](/not-done?path=./useAtomState.mdx). + +## Example + +```tsx live ecosystemId=useAtomContext/example resultVar=App version=2 +const themeAtom = atom('theme', (_id: number) => 'light') + +function ThemedComponent() { + const themeInstance = useAtomContext(themeAtom) + const theme = useAtomValue(themeInstance) + + return ( +
+ Current theme: {theme} +
+ ) +} + +function ThemeToggle() { + const themeInstance = useAtomContext(themeAtom) + const [theme, setTheme] = useAtomState(themeInstance) + + return ( + + ) +} + +function App() { + const instance1 = useAtomInstance(themeAtom, [1]) + const instance2 = useAtomInstance(themeAtom, [2]) + + return ( + <> + +
+ + +
+
+ +
+ + +
+
+ + ) +} +``` + +## Signature + + + {tab1(`useAtomContext = (template, defaultParams?) => atomInstance + // or + useAtomContext = (template, throwIfNotProvided?) => providedAtomInstance`)} + {tab2(`declare const useAtomContext: { +
(template: A): NodeOf | undefined; + (template: A, defaultParams: ParamsOf): NodeOf; + (template: A, throwIfNotProvided: boolean): NodeOf; + }`)} + + + + + Required. The atom template that identifies which atom instance to retrieve from context. + + + + Optional. An array. + + If no atom instance was provided for the given template, Zedux will use these params to create and return a new atom instance. + + + + Optional. A boolean. Whether to throw an error if no atom instance was provided. + + This is the recommended overload in almost all situations: + + ```tsx + const providedInstance = useAtomContext(myTemplate, true) + ``` + + + + The provided [atom instance](../classes/AtomInstance.mdx), or the newly-created atom instance if no atom instance was provided and `defaultParams` were passed. + + + + +## See Also + +- [The `AtomProvider` component](../components/AtomProvider.mdx) +- [The `useAtomValue` hook](/not-done?path=./useAtomValue.mdx) +- [The React Context walkthrough](../../../walkthrough/react-context.mdx) +- [The `AtomInstance` class](../classes/AtomInstance.mdx) diff --git a/docs/sidebars.js b/docs/sidebars.js index 791ee8a9..f4f3eb80 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -196,7 +196,7 @@ module.exports = { { type: 'category', label: 'Hooks', - items: ['v2/api/hooks/useEcosystem'], + items: ['v2/api/hooks/useAtomContext', 'v2/api/hooks/useEcosystem'], }, { type: 'category', From 4f0314521a73e78fab7bf241a43cd258bac24529 Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Thu, 24 Jul 2025 17:22:08 -0400 Subject: [PATCH 20/49] docs: document `useAtomInstance` hook for v2 --- docs/docs/v2/api/classes/AtomInstance.mdx | 15 +- docs/docs/v2/api/classes/AtomTemplate.mdx | 9 ++ docs/docs/v2/api/hooks/useAtomInstance.mdx | 167 +++++++++++++++++++++ docs/sidebars.js | 6 +- 4 files changed, 190 insertions(+), 7 deletions(-) create mode 100644 docs/docs/v2/api/hooks/useAtomInstance.mdx diff --git a/docs/docs/v2/api/classes/AtomInstance.mdx b/docs/docs/v2/api/classes/AtomInstance.mdx index c6d6125d..129a18db 100644 --- a/docs/docs/v2/api/classes/AtomInstance.mdx +++ b/docs/docs/v2/api/classes/AtomInstance.mdx @@ -253,7 +253,7 @@ Atom instances have the following **readonly** properties: - A promise. May be undefined if no promise was set on a returned [AtomApi](./AtomApi). + A promise. This is set via [`.setPromise()`](./AtomApi.mdx#setpromise) on a returned [AtomApi](./AtomApi.mdx). Will be undefined if no promise was set. This promise will be used to cause React to suspend whenever this atom instance is used in a component until the promise completes. This promise reference will change if a subsequent evaluation returns a new promise. @@ -262,7 +262,7 @@ Atom instances have the following **readonly** properties: The rejection value caught from the instance's [`.promise`](#promise). `undefined` if the promise did not reject. - A string or undefined. The status of the instance's [`.promise`](#promise). Will be `undefined` if the atom did not [set a promise](./AtomApi#setpromise). + A string or undefined. The status of the instance's [`.promise`](#promise). Will be `undefined` if the atom did not [set a promise](./AtomApi.mdx#setpromise). Possible values: @@ -348,8 +348,11 @@ Atom instances also inherit the following methods from [`ZeduxNode`](./ZeduxNode ## See Also -- [The Atom Instances walkthrough](../../../walkthrough/atom-instances) -- [The `Signal` class](./Signal) +- [The Atom Instances walkthrough](../../../walkthrough/atom-instances.mdx) +- [The `Signal` class](./Signal.mdx) - [The `ZeduxNode` class](./ZeduxNode.mdx) -- [The `AtomTemplate` class](./AtomTemplate) -- [The `AtomApi` class](./AtomApi) +- [The `AtomTemplate` class](./AtomTemplate.mdx) +- [The `AtomApi` class](./AtomApi.mdx) +- [The `useAtomInstance` hook](../hooks/useAtomInstance.mdx) +- [The `injectAtomInstance` injector](/not-done?path=../injectors/injectAtomInstance.mdx) +- [`ecosystem.getNode`](./Ecosystem.mdx#getnode) diff --git a/docs/docs/v2/api/classes/AtomTemplate.mdx b/docs/docs/v2/api/classes/AtomTemplate.mdx index 23bb6f16..1c64914a 100644 --- a/docs/docs/v2/api/classes/AtomTemplate.mdx +++ b/docs/docs/v2/api/classes/AtomTemplate.mdx @@ -151,3 +151,12 @@ When creating your own, custom atom types, you'll usually want to extend this cl - [`atom()`](../factories/atom) - [The Configuring Atoms walkthrough](../../../walkthrough/configuring-atoms) +- [The `AtomInstance` class](./AtomInstance.mdx) +- Hooks and injectors that accept atom templates: + - [`useAtomContext`](../hooks/useAtomContext.mdx) + - [`useAtomInstance`](../hooks/useAtomInstance.mdx) + - [`useAtomState`](/not-done?path=../hooks/useAtomState.mdx) + - [`useAtomValue`](/not-done?path=../hooks/useAtomValue.mdx) + - [`injectAtomInstance`](/not-done?path=../injectors/injectAtomInstance.mdx) + - [`injectAtomState`](/not-done?path=../injectors/injectAtomState.mdx) + - [`injectAtomValue`](/not-done?path=../injectors/injectAtomValue.mdx) diff --git a/docs/docs/v2/api/hooks/useAtomInstance.mdx b/docs/docs/v2/api/hooks/useAtomInstance.mdx new file mode 100644 index 00000000..7491ca50 --- /dev/null +++ b/docs/docs/v2/api/hooks/useAtomInstance.mdx @@ -0,0 +1,167 @@ +--- +id: useAtomInstance +title: useAtomInstance +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +```ts +import { useAtomInstance } from '@zedux/react' +``` + +A React hook that retrieves a cached [atom instance](../classes/AtomInstance.mdx) for an [atom template](../classes/AtomTemplate.mdx). If the template hasn't been instantiated yet, `useAtomInstance` creates a new atom instance and caches it. + +If the atom template takes [params](../classes/AtomInstance.mdx#params), `useAtomInstance` will [deterministically hash](../classes/ZeduxNode.mdx#params) the params you pass to find a cached atom with matching params. If no instance exists for the passed template + params combo, `useAtomInstance` will create a new atom instance and cache it. + +Unlike [`useAtomValue`](/not-done?path=./useAtomValue.mdx) and [`useAtomState`](/not-done?path=./useAtomState.mdx), `useAtomInstance` creates a [static graph dependency](../glossary.mdx#static-graph-dependency) that does not subscribe to state changes in the retrieved atom. This means the component will not re-render when the atom's state changes. + +Some primary uses for this hook are when: + +- you only need to acces the atom to update its state, e.g. via [`.set`](../classes/AtomInstance.mdx#set), [`.mutate`](../classes/AtomInstance.mdx#mutate), or by using any custom [`.exports`](../classes/AtomInstance.mdx#exports). +- you need to provide an atom to a component subtree via [``](../components/AtomProvider.mdx). + +This hook has an equivalent injector: [`injectAtomInstance`](/not-done?path=../injectors/injectAtomInstance.mdx), though it's more common to use [`ecosystem.getNode`](../classes/Ecosystem.mdx#getnode) to create/retrieve atom instances inside other atoms. + +## Example + +```tsx live ecosystemId=useAtomInstance/example resultVar=App version=2 +const countAtom = atom('count', () => { + const signal = injectSignal(0) + + const decrement = () => signal.set(state => state - 1) + const increment = () => signal.set(state => state + 1) + + return api(signal).setExports({ decrement, increment }) +}) + +function CounterControls() { + // This component doesn't need to rerender when `countAtom`'s state changes. + // So use `useAtomInstance`: + const instance = useAtomInstance(countAtom) + + return ( +
+ + + +
+ ) +} + +function CounterDisplay() { + // `useAtomValue` does subscribe to state changes: + const count = useAtomValue(countAtom) + + return
Count: {count}
+} + +function App() { + return ( +
+ + +
+ ) +} +``` + +## Dynamicizing the Dependency + +Sometimes, you need both a reference to the atom instance _and_ you want the current component to rerender when the atom's state changes. + +There are a few ways to do this: + +1. Pass the atom instance directly to a dynamic hook like [`useAtomValue`](/not-done?path=./useAtomValue.mdx) or [`useAtomState`](/not-done?path=./useAtomState.mdx). + +```tsx +function ExampleComponent() { + const instance = useAtomInstance(exampleAtom) + + // pass the instance directly to `useAtomValue` to subscribe to updates: + const dynamicValue = useAtomValue(instance) +} +``` + +2. Pass `{ subscribe: true }` as the third argument to `useAtomInstance`. + +```tsx +function ExampleComponent() { + // pass `[]` if the atom doesn't take params: + const instance = useAtomInstance(exampleAtom, [], { subscribe: true }) + + // This is okay now! The component will rerender when the atom's state changes + const dynamicValue = instance.get() +} +``` + +:::tip +[`useAtomState`](/not-done?path=./useAtomState.mdx) is often good enough when you don't need a reference to the full atom instance. +::: + +## Signature + + + {tab1(`useAtomInstance = (template, params?, config?) => atomInstance`)} + {tab2(`declare const useAtomInstance: { +
(template: A, params: ParamsOf, config?: ZeduxHookConfig): NodeOf; + >(template: A): NodeOf; + (template: ParamlessTemplate): NodeOf; + (instance: I, params?: [], config?: ZeduxHookConfig): I; + (template: S, params: ParamsOf, config?: Omit): NodeOf; + >(template: S): NodeOf; + (template: ParamlessTemplate): NodeOf; + }`)} + + + + + Required. An [atom template](../classes/AtomTemplate.mdx) to find or create an instance of. + + You can also pass an [atom instance](../classes/AtomInstance.mdx) directly. This is useful when receiving an atom instance from other sources (outside React) that you don't want to cause rerenders, but that you also need to prevent from being destroyed until the component unmounts. This is a rare use case. + + + + An array of the atom's [params](../classes/AtomInstance.mdx#params). + + TypeScript users will see that this is required if the atom has required params. + + + + An object with the following optional properties: + + ```ts + { operation, subscribe, suspend } + ``` + + + + A string. Default: `useAtomInstance`. Used for debugging to describe the reason for creating this graph edge. This default is usually fine. + + + A boolean. Default: `false`. Pass `subscribe: true` to make `useAtomInstance` create a [dynamic graph dependency](../glossary.mdx#dynamic-graph-dependency) instead. + + + A boolean. Default: `true`. Whether the component should suspend if the resolved atom has a [promise](../classes/AtomInstance.mdx#promise) set. Pass `suspend: false` to prevent this hook from triggering React suspense. + + + + See the [React Hooks walkthrough](/not-done?path=/../../walkthrough/react-hooks.mdx) for more details. + + + + The [atom instance](../classes/AtomInstance.mdx) object. + + + + +## See Also + +- [The `AtomInstance` class](../classes/AtomInstance.mdx) +- [The `useAtomState` hook](/not-done?path=./useAtomState.mdx) +- [The `useAtomValue` hook](/not-done?path=./useAtomValue.mdx) +- [The `injectAtomInstance` injector](/not-done?path=../injectors/injectAtomInstance.mdx) +- [The React Hooks walkthrough](/not-done?path=/../../walkthrough/react-hooks.mdx) diff --git a/docs/sidebars.js b/docs/sidebars.js index f4f3eb80..c6c1b096 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -196,7 +196,11 @@ module.exports = { { type: 'category', label: 'Hooks', - items: ['v2/api/hooks/useAtomContext', 'v2/api/hooks/useEcosystem'], + items: [ + 'v2/api/hooks/useAtomContext', + 'v2/api/hooks/useAtomInstance', + 'v2/api/hooks/useEcosystem', + ], }, { type: 'category', From 09f51371632dcfec98b8958327212b11282745c8 Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Fri, 25 Jul 2025 12:01:37 -0400 Subject: [PATCH 21/49] docs: document `useAtomSelector` hook for v2 --- docs/docs/v2/api/hooks/useAtomInstance.mdx | 4 +- docs/docs/v2/api/hooks/useAtomSelector.mdx | 71 ++++++++++++++++++++++ docs/sidebars.js | 1 + 3 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 docs/docs/v2/api/hooks/useAtomSelector.mdx diff --git a/docs/docs/v2/api/hooks/useAtomInstance.mdx b/docs/docs/v2/api/hooks/useAtomInstance.mdx index 7491ca50..3a22ddf4 100644 --- a/docs/docs/v2/api/hooks/useAtomInstance.mdx +++ b/docs/docs/v2/api/hooks/useAtomInstance.mdx @@ -17,7 +17,7 @@ Unlike [`useAtomValue`](/not-done?path=./useAtomValue.mdx) and [`useAtomState`]( Some primary uses for this hook are when: -- you only need to acces the atom to update its state, e.g. via [`.set`](../classes/AtomInstance.mdx#set), [`.mutate`](../classes/AtomInstance.mdx#mutate), or by using any custom [`.exports`](../classes/AtomInstance.mdx#exports). +- you only need to access the atom to update its state, e.g. via [`.set`](../classes/AtomInstance.mdx#set), [`.mutate`](../classes/AtomInstance.mdx#mutate), or by using any custom [`.exports`](../classes/AtomInstance.mdx#exports). - you need to provide an atom to a component subtree via [``](../components/AtomProvider.mdx). This hook has an equivalent injector: [`injectAtomInstance`](/not-done?path=../injectors/injectAtomInstance.mdx), though it's more common to use [`ecosystem.getNode`](../classes/Ecosystem.mdx#getnode) to create/retrieve atom instances inside other atoms. @@ -153,7 +153,7 @@ function ExampleComponent() { - The [atom instance](../classes/AtomInstance.mdx) object. + The resolved [atom instance](../classes/AtomInstance.mdx) for the passed template + params combo. diff --git a/docs/docs/v2/api/hooks/useAtomSelector.mdx b/docs/docs/v2/api/hooks/useAtomSelector.mdx new file mode 100644 index 00000000..5715a40c --- /dev/null +++ b/docs/docs/v2/api/hooks/useAtomSelector.mdx @@ -0,0 +1,71 @@ +--- +id: useAtomSelector +title: useAtomSelector +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +:::warning Deprecated +As of v2, this hook is deprecated. Use [`useAtomValue`](/not-done?path=./useAtomValue.mdx) instead - it now handles all Zedux's reactive primitives, making refactoring between them easier. +::: + +```ts +import { useAtomSelector } from '@zedux/react' +``` + +A React hook that retrieves a cached [atom selector](/not-done?path=../types/SelectorTemplate.mdx) value. If the selector is not cached yet, `useAtomSelector` caches it by creating a new [selector instance](../classes/SelectorInstance.mdx) and returns the value. + +If the selector takes params, TypeScript will enforce that those are passed correctly. + +## Examples + +```tsx live ecosystemId=useAtomSelector/example resultVar=Count version=2 +const countAtom = atom('count', 0) +const getDoubledCount = ({ get }) => get(countAtom) * 2 + +function Count() { + const [count, setCount] = useAtomState(countAtom) + const doubledCount = useAtomSelector(getDoubledCount) + + return ( +
+
Count: {count}
+
Doubled count: {doubledCount}
+ + +
+ ) +} +``` + +## Signature + + + {tab1(`useAtomSelector = (template, params?) => selectedValue`)} + {tab2( + `declare const useAtomSelector: (template: S, ...args: ParamsOf) => StateOf` + )} + + + + + Required. Either a [selector function](/not-done?path=../types/SelectorTemplate.mdx#atomselector) or an [atom selector config object](/not-done?path=../types/SelectorTemplate.mdx#atomselectorconfig). + + See [the `SelectorTemplate` type](/not-done?path=../types/SelectorTemplate.mdx) for more details. + + + + The selector's [params](../classes/SelectorInstance.mdx#params) spread into the rest of the arguments to `useAtomSelector`. Note that this is different from [`useAtomValue`](/not-done?path=./useAtomValue.mdx) (and all Zedux APIs in v2) where params are passed via the second argument as an array. + + Required if the selector has required parameters. + + + + The cached value of the resolved [selector instance](../classes/SelectorInstance.mdx) for the passed [selector template](/not-done?path=../types/SelectorTemplate.mdx) + [params](../classes/SelectorInstance.mdx#params) combo. + + + +## See Also + +- [The `useAtomValue` hook](/not-done?path=./useAtomValue.mdx) +- [The Selectors walkthrough](../../../walkthrough/selectors.mdx) diff --git a/docs/sidebars.js b/docs/sidebars.js index c6c1b096..f57d4d70 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -199,6 +199,7 @@ module.exports = { items: [ 'v2/api/hooks/useAtomContext', 'v2/api/hooks/useAtomInstance', + 'v2/api/hooks/useAtomSelector', 'v2/api/hooks/useEcosystem', ], }, From f4797fc129ecc9dc664b2aa748624c022dc91b60 Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Fri, 25 Jul 2025 12:20:22 -0400 Subject: [PATCH 22/49] docs: document `SelectorTemplate` type for v2 --- docs/docs/v2/api/classes/Ecosystem.mdx | 4 +- docs/docs/v2/api/classes/ZeduxNode.mdx | 6 +- docs/docs/v2/api/hooks/useAtomSelector.mdx | 8 +- docs/docs/v2/api/types/SelectorTemplate.mdx | 189 ++++++++++++++++++++ docs/sidebars.js | 5 + 5 files changed, 203 insertions(+), 9 deletions(-) create mode 100644 docs/docs/v2/api/types/SelectorTemplate.mdx diff --git a/docs/docs/v2/api/classes/Ecosystem.mdx b/docs/docs/v2/api/classes/Ecosystem.mdx index c9ae4ad5..b38dfca9 100644 --- a/docs/docs/v2/api/classes/Ecosystem.mdx +++ b/docs/docs/v2/api/classes/Ecosystem.mdx @@ -665,7 +665,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context - Required. An [atom template](./AtomTemplate), [selector template](/not-done?path=../types/SelectorTemplate), or atom instance. + Required. An [atom template](./AtomTemplate), [selector template](../types/SelectorTemplate.mdx), or atom instance. Optional (required if the template takes params). An array of the atom or selector's params. Only relevant when passing an atom or selector template. @@ -740,7 +740,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context - Required. An [atom template](./AtomTemplate), [selector template](/not-done?path=../types/SelectorTemplate), or [atom instance](./AtomInstance). + Required. An [atom template](./AtomTemplate.mdx), [selector template](../types/SelectorTemplate.mdx), or [atom instance](./AtomInstance.mdx). When an instance is passed, `getNode` registers a static graph dependency on that instance (when called during evaluation) and returns it as-is. diff --git a/docs/docs/v2/api/classes/ZeduxNode.mdx b/docs/docs/v2/api/classes/ZeduxNode.mdx index f6d11e34..2757350c 100644 --- a/docs/docs/v2/api/classes/ZeduxNode.mdx +++ b/docs/docs/v2/api/classes/ZeduxNode.mdx @@ -242,7 +242,7 @@ function Shout() { A reference to the template this node was created from. `undefined` if none. Only [atom instances](./AtomInstance) and [selector instances](./SelectorInstance.mdx) have templates. - For atom instances, this will be the [atom template](./AtomTemplate). For selector instances, this will be the [selector template](/not-done?path=../types/SelectorTemplate). + For atom instances, this will be the [atom template](./AtomTemplate). For selector instances, this will be the [selector template](../types/SelectorTemplate.mdx). @@ -267,7 +267,7 @@ Every node has the following methods: - See the [destruction walkthrough](/not-done?path=../../../walkthrough/destruction) for more information. + See the [destruction walkthrough](../../../walkthrough/destruction.mdx) for more information. @@ -323,7 +323,7 @@ Every node has the following methods: Active listeners each create their own graph node that observes the target node. They prevent automatic node cleanup and will recreate the target node if it's force-destroyed. :::tip - As of Zedux v2, active listeners are the key to [manual graphing](/not-done?path=../../../walkthrough/destruction#manual-graphing). + As of Zedux v2, active listeners are the key to [manual graphing](../../../walkthrough/destruction.mdx#manual-graphing). ::: diff --git a/docs/docs/v2/api/hooks/useAtomSelector.mdx b/docs/docs/v2/api/hooks/useAtomSelector.mdx index 5715a40c..0c3b3537 100644 --- a/docs/docs/v2/api/hooks/useAtomSelector.mdx +++ b/docs/docs/v2/api/hooks/useAtomSelector.mdx @@ -13,7 +13,7 @@ As of v2, this hook is deprecated. Use [`useAtomValue`](/not-done?path=./useAtom import { useAtomSelector } from '@zedux/react' ``` -A React hook that retrieves a cached [atom selector](/not-done?path=../types/SelectorTemplate.mdx) value. If the selector is not cached yet, `useAtomSelector` caches it by creating a new [selector instance](../classes/SelectorInstance.mdx) and returns the value. +A React hook that retrieves a cached [atom selector](../types/SelectorTemplate.mdx) value. If the selector is not cached yet, `useAtomSelector` caches it by creating a new [selector instance](../classes/SelectorInstance.mdx) and returns the value. If the selector takes params, TypeScript will enforce that those are passed correctly. @@ -49,9 +49,9 @@ function Count() { - Required. Either a [selector function](/not-done?path=../types/SelectorTemplate.mdx#atomselector) or an [atom selector config object](/not-done?path=../types/SelectorTemplate.mdx#atomselectorconfig). + Required. Either a [selector function](../types/SelectorTemplate.mdx#atomselector) or an [atom selector config object](../types/SelectorTemplate.mdx#atomselectorconfig). - See [the `SelectorTemplate` type](/not-done?path=../types/SelectorTemplate.mdx) for more details. + See [the `SelectorTemplate` type](../types/SelectorTemplate.mdx) for more details. @@ -61,7 +61,7 @@ function Count() { - The cached value of the resolved [selector instance](../classes/SelectorInstance.mdx) for the passed [selector template](/not-done?path=../types/SelectorTemplate.mdx) + [params](../classes/SelectorInstance.mdx#params) combo. + The cached value of the resolved [selector instance](../classes/SelectorInstance.mdx) for the passed [selector template](../types/SelectorTemplate.mdx) + [params](../classes/SelectorInstance.mdx#params) combo. diff --git a/docs/docs/v2/api/types/SelectorTemplate.mdx b/docs/docs/v2/api/types/SelectorTemplate.mdx new file mode 100644 index 00000000..694fb97f --- /dev/null +++ b/docs/docs/v2/api/types/SelectorTemplate.mdx @@ -0,0 +1,189 @@ +--- +id: SelectorTemplate +title: SelectorTemplate +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +```ts +import type { + AtomSelector, + AtomSelectorConfig, + SelectorTemplate, +} from '@zedux/react' +``` + +Selectors are the lightweight champions of the Zedux world. They're easy to create and use. They have a small footprint and give you a quick way to tap into Zedux's reactive power. + +Unlike [atom templates](../classes/AtomTemplate.mdx), which are instances of a class, "selector templates" in Zedux are just plain functions or objects that define a selection (aka derivation) operation. + +The `SelectorTemplate` type is essentially just this: + +```ts +type SelectorTemplate = AtomSelector | AtomSelectorConfig +``` + +This type is used by all Zedux APIs that accept selectors. In other words, anywhere you can pass a [selector](#atomselector) function, you can also pass an [`AtomSelectorConfig`](#atomselectorconfig) object. + +## `AtomSelector` + +A function that takes an [ecosystem](../classes/Ecosystem.mdx) as its first parameter and can return absolutely anything. + +```ts +const getAdminUsers = ({ get }: Ecosystem) => + get(usersAtom).filter(user => user.role === 'admin') +``` + +The return value will be cached, using the selector function reference as the cache key. This uses a `WeakMap` internally. + +Any [`ecosystem.get`](../classes/Ecosystem.mdx#get) calls inside the selector will automatically create a [dynamic graph dependency](../glossary.mdx#dynamic-graph-dependency) between the selector and the retrieved node. The selector will automatically reevaluate whenever the retrieved node's state changes. + +Any [`ecosystem.getNode`](../classes/Ecosystem.mdx#getnode) calls inside the selector will automatically create a [static graph dependency](../glossary.mdx#static-graph-dependency) between the selector and the retrieved node. These dependencies will not cause the selector to reevaluate. The main use for this is to prevent the retrieved node from being destroyed while the selector still needs it. + +Selectors can take other parameters too. Any parameters after the first `ecosystem` parameter will become the selector's [params](../classes/SelectorInstance.mdx#params). + +```ts +const getUsersByRole = ({ get }, role: string) => + get(usersAtom).filter(user => user.role === role) + +// since the `role` param is required, consumers must pass it: +const adminUsers = useAtomValue(getUsersByRole) // ❌ Error! +const adminUsers = useAtomValue(getUsersByRole, ['admin']) // ✅ +``` + +TypeScript will enforce that consumers pass the correct params when using the selector via any hooks, injectors, or ecosystem methods. + +## `AtomSelectorConfig` + +An object that defines a selector plus some additional configuration: + +```ts +{ argsComparator?, name?, resultsComparator, selector? } +``` + + + + Optional. A function that receives the params from the last selector run and the latest arguments passed to the selector. It should compare the two and return a boolean indicating whether they should be considered "equal". + + Return true to prevent the selector from reevaluating. + + Return false to allow the selector to reevaluate. In this case, if the params create a different [hash](../classes/ZeduxNode.mdx#params), the selector will be reevaluated. + + This config option is only respected by Zedux's React hooks and only runs after the selector has already evaluated once and is about to reevaluate. Its use is to prevent excess React rerenders from running the selector unnecessarily. This option is a noop when passing selectors to other Zedux APIs like [`ecosystem.get`](../classes/Ecosystem.mdx#get), [`ecosystem.getNode`](../classes/Ecosystem.mdx#getnode), or [`injectAtomValue`](/not-done?path=../injectors/injectAtomValue.mdx). + + + + Optional. A string that will be used to create user-friendly [ids](../classes/SelectorInstance.mdx#id) for the selector. This will take precedence over the [`selector`](#selector) function's name. + + This can be useful when programmatically generating selectors, or if you prefer setting the [`selector`](#selector) field to anonymous arrow functions. + + + + Optional. A function that receives the previous and latest return values of the selector. Return true if the results should be considered "equal". + + When a selector returns "equivalent" values on subsequent reevaluations, the latest result is discarded, the previous result remains cached, and consumers of the selector are not notified of any change (since there wasn't one). + + + + Required. The actual [selector function](#atomselector) to run. + + :::note + `AtomSelectorConfig` objects are cached by object reference, not by their `selector` function reference. + ::: + + + + +## Inline Selectors + +Since selectors are just functions, it's easy to create them on the fly: + +```ts +const adminUsers = useAtomValue(({ get }) => + get(usersAtom).filter(user => user.role === 'admin') +) +``` + +:::note +This works for `AtomSelectorConfig` objects too: + +```ts +const adminUsers = useAtomValue( + { + name: 'getAdminUsersForUsersTable', + selector: getUsersByRole, + }, + ['admin'] +) +``` + +::: + +This is supported, and you typically shouldn't need to worry about it. But since the selector function reference is recreated every render, Zedux has to do extra work to handle this. + +Because of this, for larger apps, it's generally recommended to define selectors outside of components, atoms, or other selectors. + +This is also useful for debugging, since Zedux uses the selector's function name to generate a user-friendly id for each [instance](../classes/SelectorInstance.mdx) of the selector. + +## Refactoring to Ions + +Selectors are designed for simple, pure calculations. They always reevaluate when any of their dependencies update. Sometimes you need more control over when/how often a selector evaluates. + +It's common to refactor a selector to an ion for better control over memoization details via [`injectMemo`](/not-done?path=../injectors/injectMemo.mdx) or even combining [`injectEffect`](/not-done?path=../injectors/injectEffect.mdx) with async tools like RxJS to throttle or buffer updates. + +```tsx +// Before (using an atom selector): +const getUserName = ({ get }: Ecosystem) => get(userAtom).name + +// After (using an ion): +const userNameAtom = ion('userName', ({ get }) => get(userAtom).name) + +// Consumers of the selector require minimal changes: +const userName = useAtomValue(getUserName) // Before +const userName = useAtomValue(userNameAtom) // After +``` + +This is one of the primary reasons why Zedux v2 deprecated many selector-specific APIs like [`useAtomSelector`](../hooks/useAtomSelector.mdx) and [`ecosystem.select`](../classes/Ecosystem.mdx#select). + +## Interface + +The `SelectorTemplate` type itself is just a union of the `AtomSelector` and `AtomSelectorConfig` types: + +```ts +type AtomSelector = ( + ecosystem: Ecosystem, + ...args: Params +) => State + +interface AtomSelectorConfig { + argsComparator?: ( + newArgs: NoInfer, + oldArgs: NoInfer + ) => boolean + name?: string + resultsComparator?: ( + newResult: NoInfer, + oldResult: NoInfer + ) => boolean + selector: AtomSelector +} + +type SelectorTemplate = + | AtomSelector + | AtomSelectorConfig +``` + + + {tab1(`interface AtomConfig`)} + {tab2(`interface AtomConfig { + dehydrate?: (state: State) => any + tags?: string[] + hydrate?: (dehydratedState: unknown) => State + ttl?: number +}`)} + + +## See Also + +- [The `SelectorInstance` class](../classes/SelectorInstance.mdx) +- [The selectors walkthrough](../../../walkthrough/selectors.mdx) diff --git a/docs/sidebars.js b/docs/sidebars.js index f57d4d70..84149437 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -211,6 +211,11 @@ module.exports = { 'v2/api/injectors/injectSignal', ], }, + { + type: 'category', + label: 'Types', + items: ['v2/api/types/SelectorTemplate'], + }, 'v2/api/glossary', ], }, From 6a960821e0e90e2fdd96ba500bed50c8a52c05fd Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Fri, 25 Jul 2025 12:53:13 -0400 Subject: [PATCH 23/49] docs: document `useAtomState` hook for v2 --- docs/docs/v2/api/classes/AtomInstance.mdx | 4 +- docs/docs/v2/api/classes/AtomTemplate.mdx | 2 +- docs/docs/v2/api/hooks/useAtomContext.mdx | 2 +- docs/docs/v2/api/hooks/useAtomInstance.mdx | 38 +++--- docs/docs/v2/api/hooks/useAtomState.mdx | 140 +++++++++++++++++++++ docs/sidebars.js | 1 + 6 files changed, 165 insertions(+), 22 deletions(-) create mode 100644 docs/docs/v2/api/hooks/useAtomState.mdx diff --git a/docs/docs/v2/api/classes/AtomInstance.mdx b/docs/docs/v2/api/classes/AtomInstance.mdx index 129a18db..c9b90d40 100644 --- a/docs/docs/v2/api/classes/AtomInstance.mdx +++ b/docs/docs/v2/api/classes/AtomInstance.mdx @@ -255,7 +255,9 @@ Atom instances have the following **readonly** properties: A promise. This is set via [`.setPromise()`](./AtomApi.mdx#setpromise) on a returned [AtomApi](./AtomApi.mdx). Will be undefined if no promise was set. - This promise will be used to cause React to suspend whenever this atom instance is used in a component until the promise completes. This promise reference will change if a subsequent evaluation returns a new promise. + This promise will be used to cause React to suspend whenever this atom instance is used in a component until the promise completes. + + This promise reference will change if a subsequent evaluation returns a new promise. When it changes, all observers of this atom will reevaluate (including [static dependents](../glossary.mdx#static-graph-dependency)) and components will re-suspend. diff --git a/docs/docs/v2/api/classes/AtomTemplate.mdx b/docs/docs/v2/api/classes/AtomTemplate.mdx index 1c64914a..2c633a8e 100644 --- a/docs/docs/v2/api/classes/AtomTemplate.mdx +++ b/docs/docs/v2/api/classes/AtomTemplate.mdx @@ -155,7 +155,7 @@ When creating your own, custom atom types, you'll usually want to extend this cl - Hooks and injectors that accept atom templates: - [`useAtomContext`](../hooks/useAtomContext.mdx) - [`useAtomInstance`](../hooks/useAtomInstance.mdx) - - [`useAtomState`](/not-done?path=../hooks/useAtomState.mdx) + - [`useAtomState`](../hooks/useAtomState.mdx) - [`useAtomValue`](/not-done?path=../hooks/useAtomValue.mdx) - [`injectAtomInstance`](/not-done?path=../injectors/injectAtomInstance.mdx) - [`injectAtomState`](/not-done?path=../injectors/injectAtomState.mdx) diff --git a/docs/docs/v2/api/hooks/useAtomContext.mdx b/docs/docs/v2/api/hooks/useAtomContext.mdx index 6a22fa13..a4fce569 100644 --- a/docs/docs/v2/api/hooks/useAtomContext.mdx +++ b/docs/docs/v2/api/hooks/useAtomContext.mdx @@ -11,7 +11,7 @@ import { useAtomContext } from '@zedux/react' A React hook that retrieves an [atom instance](../classes/AtomInstance.mdx) provided over React context via an [``](../components/AtomProvider.mdx). -This hook only returns the atom instance itself. To make the consuming component rerender when the provided atom updates, pass the returned atom instance to [`useAtomValue`](/not-done?path=./useAtomValue.mdx) or [`useAtomState`](/not-done?path=./useAtomState.mdx). +This hook only returns the atom instance itself. To make the consuming component rerender when the provided atom updates, pass the returned atom instance to [`useAtomValue`](/not-done?path=./useAtomValue.mdx) or [`useAtomState`](./useAtomState.mdx). ## Example diff --git a/docs/docs/v2/api/hooks/useAtomInstance.mdx b/docs/docs/v2/api/hooks/useAtomInstance.mdx index 3a22ddf4..c3e92247 100644 --- a/docs/docs/v2/api/hooks/useAtomInstance.mdx +++ b/docs/docs/v2/api/hooks/useAtomInstance.mdx @@ -13,7 +13,7 @@ A React hook that retrieves a cached [atom instance](../classes/AtomInstance.mdx If the atom template takes [params](../classes/AtomInstance.mdx#params), `useAtomInstance` will [deterministically hash](../classes/ZeduxNode.mdx#params) the params you pass to find a cached atom with matching params. If no instance exists for the passed template + params combo, `useAtomInstance` will create a new atom instance and cache it. -Unlike [`useAtomValue`](/not-done?path=./useAtomValue.mdx) and [`useAtomState`](/not-done?path=./useAtomState.mdx), `useAtomInstance` creates a [static graph dependency](../glossary.mdx#static-graph-dependency) that does not subscribe to state changes in the retrieved atom. This means the component will not re-render when the atom's state changes. +Unlike [`useAtomValue`](/not-done?path=./useAtomValue.mdx) and [`useAtomState`](./useAtomState.mdx), `useAtomInstance` creates a [static graph dependency](../glossary.mdx#static-graph-dependency) that does not subscribe to state changes in the retrieved atom. This means the component will not re-render when the atom's state changes. Some primary uses for this hook are when: @@ -73,31 +73,31 @@ Sometimes, you need both a reference to the atom instance _and_ you want the cur There are a few ways to do this: -1. Pass the atom instance directly to a dynamic hook like [`useAtomValue`](/not-done?path=./useAtomValue.mdx) or [`useAtomState`](/not-done?path=./useAtomState.mdx). +1. Pass the atom instance to a dynamic hook like [`useAtomValue`](/not-done?path=./useAtomValue.mdx) or [`useAtomState`](./useAtomState.mdx) after retrieving it from `useAtomInstance`. -```tsx -function ExampleComponent() { - const instance = useAtomInstance(exampleAtom) + ```tsx + function ExampleComponent() { + const instance = useAtomInstance(exampleAtom) - // pass the instance directly to `useAtomValue` to subscribe to updates: - const dynamicValue = useAtomValue(instance) -} -``` + // pass the instance directly to `useAtomValue` to subscribe to updates: + const dynamicValue = useAtomValue(instance) + } + ``` 2. Pass `{ subscribe: true }` as the third argument to `useAtomInstance`. -```tsx -function ExampleComponent() { - // pass `[]` if the atom doesn't take params: - const instance = useAtomInstance(exampleAtom, [], { subscribe: true }) + ```tsx + function ExampleComponent() { + // pass `[]` if the atom doesn't take params: + const instance = useAtomInstance(exampleAtom, [], { subscribe: true }) - // This is okay now! The component will rerender when the atom's state changes - const dynamicValue = instance.get() -} -``` + // This is okay now! The component will rerender when the atom's state changes + const dynamicValue = instance.get() + } + ``` :::tip -[`useAtomState`](/not-done?path=./useAtomState.mdx) is often good enough when you don't need a reference to the full atom instance. +[`useAtomState`](./useAtomState.mdx) is often good enough when you don't need a reference to the full atom instance. ::: ## Signature @@ -161,7 +161,7 @@ function ExampleComponent() { ## See Also - [The `AtomInstance` class](../classes/AtomInstance.mdx) -- [The `useAtomState` hook](/not-done?path=./useAtomState.mdx) +- [The `useAtomState` hook](./useAtomState.mdx) - [The `useAtomValue` hook](/not-done?path=./useAtomValue.mdx) - [The `injectAtomInstance` injector](/not-done?path=../injectors/injectAtomInstance.mdx) - [The React Hooks walkthrough](/not-done?path=/../../walkthrough/react-hooks.mdx) diff --git a/docs/docs/v2/api/hooks/useAtomState.mdx b/docs/docs/v2/api/hooks/useAtomState.mdx new file mode 100644 index 00000000..5d8ee688 --- /dev/null +++ b/docs/docs/v2/api/hooks/useAtomState.mdx @@ -0,0 +1,140 @@ +--- +id: useAtomState +title: useAtomState +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +```ts +import { useAtomState } from '@zedux/react' +``` + +A React hook that creates a [dynamic graph dependency](../glossary.mdx#dynamic-graph-dependency) on an [atom](../classes/AtomInstance.mdx), triggering component rerenders when the atom updates. This is exactly the same as [`useAtomValue`](/not-done?path=./useAtomValue.mdx), except it returns a `[state, setState]` tuple similar to React's [`useState` hook](https://react.dev/reference/react/useState). + +Just like `useAtomValue`, `useAtomState` creates an atom instance if none exists for the given [atom template](../classes/AtomTemplate.mdx) (+ [params](../classes/AtomInstance.mdx#params) combo, if the atom takes params). + +## Examples + +`useAtomState` is the simplest way to consume zero-config atoms: + +```tsx live ecosystemId=useAtomState/example resultVar=Counter version=2 +const countAtom = atom('count', 0) + +function Counter() { + const [count, setCount] = useAtomState(countAtom) + + return ( +
+
Count: {count}
+ + + +
+ ) +} +``` + +More sophisticated atoms can have [atom exports](../classes/AtomInstance.mdx#exports). These exports can be destructured directly off the `setState` function returned by `useAtomState`: + +```tsx live ecosystemId=useAtomState/exports resultVar=Counter version=2 +const counterAtom = atom('counter', () => { + const signal = injectSignal(0) + + const decrement = () => signal.set(state => state - 1) + const increment = () => signal.set(state => state + 1) + const reset = () => signal.set(0) + + return api(signal).setExports({ decrement, increment, reset }) +}) + +function Counter() { + // destructure the exports! + const [count, { decrement, increment, reset }] = useAtomState(counterAtom) + + return ( +
+
Count: {count}
+ + + +
+ ) +} +``` + +Miscellaneous: + +```ts +const [state, setState] = useAtomState(myAtom) +const withParams = useAtomState(myAtom, ['param 1', 'param 2']) +const fromInstance = useAtomState(myAtomInstance) +const [, setterOnly] = useAtomState(myAtom) +const [, { exports, only }] = useAtomState(myAtom) +``` + +## Signature + + + {tab1(`useAtomState = (template, params?, config?) => [state, setState]`)} + {tab2(`declare const useAtomState: { +
>(template: A, params: ParamsOf, config: Omit & { + suspend: false; + }): StateHookTuple, ExportsOf>; + >(template: A, params: ParamsOf, config?: Omit): StateHookTuple, ExportsOf>; + >(template: A): StateHookTuple, ExportsOf>; + >(template: ParamlessTemplate): StateHookTuple, ExportsOf>; + (instance: I, params: [], config: Omit & { + suspend: false; + }): StateHookTuple, ExportsOf>; + (instance: I, params?: [], config?: Omit): StateHookTuple, ExportsOf>; +}`)} + + + + + Required. The [atom template](../classes/AtomTemplate.mdx) to find or create a cached [instance](../classes/AtomInstance.mdx) of. + + Also accepts an [atom instance](../classes/AtomInstance.mdx) directly. In this case, any passed [`params`](#params) will be ignored and `useAtomState` will subscribe to the passed instance. + + + + An array. Required if the atom template has required parameters. The parameters to pass to the atom state factory. + + These will be [hashed deterministically](../classes/ZeduxNode.mdx#params) to find a cached instance with matching params. + + + + An object with the following optional fields: + + ```ts + { operation?, suspend? } + ``` + + - **`operation`** - A string. Default: `'useAtomState'`. This is just for debugging. The default is usually good enough. + - **`suspend`** - A boolean. Default: `true`. Whether `useAtomState` should trigger React suspense if the atom has a [promise](../classes/AtomInstance.mdx#promise) set. + + + + A `[state, setState]` tuple: + + - **`state`** - The current state of the atom instance. This will be kept up to date, as the component will rerender if the atom's state changes. + - **`setState`** - An "exports-infused" setter function that's a thin wrapper around [`atom.set`](../classes/AtomInstance.mdx#set). This function has all the atom's exports attached to the function object itself. + + + + +## See Also + +- [The `useAtomValue` hook](/not-done?path=./useAtomValue.mdx) +- [The `useAtomInstance` hook](./useAtomInstance.mdx) +- [The `injectAtomState` injector](/not-done?path=../injectors/injectAtomState.mdx) +- [The React Hooks walkthrough](/not-done?path=../../../walkthrough/react-hooks.mdx) diff --git a/docs/sidebars.js b/docs/sidebars.js index 84149437..45f88ebc 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -200,6 +200,7 @@ module.exports = { 'v2/api/hooks/useAtomContext', 'v2/api/hooks/useAtomInstance', 'v2/api/hooks/useAtomSelector', + 'v2/api/hooks/useAtomState', 'v2/api/hooks/useEcosystem', ], }, From 12d4322ddb109d241c0d644cd7e71d5d45ae8b29 Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Fri, 25 Jul 2025 13:13:10 -0400 Subject: [PATCH 24/49] docs: document `useAtomValue` hook for v2 --- docs/docs/v2/api/classes/AtomTemplate.mdx | 2 +- docs/docs/v2/api/hooks/useAtomContext.mdx | 4 +- docs/docs/v2/api/hooks/useAtomInstance.mdx | 6 +- docs/docs/v2/api/hooks/useAtomSelector.mdx | 6 +- docs/docs/v2/api/hooks/useAtomState.mdx | 4 +- docs/docs/v2/api/hooks/useAtomValue.mdx | 177 +++++++++++++++++++++ docs/sidebars.js | 1 + 7 files changed, 189 insertions(+), 11 deletions(-) create mode 100644 docs/docs/v2/api/hooks/useAtomValue.mdx diff --git a/docs/docs/v2/api/classes/AtomTemplate.mdx b/docs/docs/v2/api/classes/AtomTemplate.mdx index 2c633a8e..f3c80c33 100644 --- a/docs/docs/v2/api/classes/AtomTemplate.mdx +++ b/docs/docs/v2/api/classes/AtomTemplate.mdx @@ -156,7 +156,7 @@ When creating your own, custom atom types, you'll usually want to extend this cl - [`useAtomContext`](../hooks/useAtomContext.mdx) - [`useAtomInstance`](../hooks/useAtomInstance.mdx) - [`useAtomState`](../hooks/useAtomState.mdx) - - [`useAtomValue`](/not-done?path=../hooks/useAtomValue.mdx) + - [`useAtomValue`](../hooks/useAtomValue.mdx) - [`injectAtomInstance`](/not-done?path=../injectors/injectAtomInstance.mdx) - [`injectAtomState`](/not-done?path=../injectors/injectAtomState.mdx) - [`injectAtomValue`](/not-done?path=../injectors/injectAtomValue.mdx) diff --git a/docs/docs/v2/api/hooks/useAtomContext.mdx b/docs/docs/v2/api/hooks/useAtomContext.mdx index a4fce569..8a67eb5f 100644 --- a/docs/docs/v2/api/hooks/useAtomContext.mdx +++ b/docs/docs/v2/api/hooks/useAtomContext.mdx @@ -11,7 +11,7 @@ import { useAtomContext } from '@zedux/react' A React hook that retrieves an [atom instance](../classes/AtomInstance.mdx) provided over React context via an [``](../components/AtomProvider.mdx). -This hook only returns the atom instance itself. To make the consuming component rerender when the provided atom updates, pass the returned atom instance to [`useAtomValue`](/not-done?path=./useAtomValue.mdx) or [`useAtomState`](./useAtomState.mdx). +This hook only returns the atom instance itself. To make the consuming component rerender when the provided atom updates, pass the returned atom instance to [`useAtomValue`](./useAtomValue.mdx) or [`useAtomState`](./useAtomState.mdx). ## Example @@ -112,6 +112,6 @@ function App() { ## See Also - [The `AtomProvider` component](../components/AtomProvider.mdx) -- [The `useAtomValue` hook](/not-done?path=./useAtomValue.mdx) +- [The `useAtomValue` hook](./useAtomValue.mdx) - [The React Context walkthrough](../../../walkthrough/react-context.mdx) - [The `AtomInstance` class](../classes/AtomInstance.mdx) diff --git a/docs/docs/v2/api/hooks/useAtomInstance.mdx b/docs/docs/v2/api/hooks/useAtomInstance.mdx index c3e92247..3922bee9 100644 --- a/docs/docs/v2/api/hooks/useAtomInstance.mdx +++ b/docs/docs/v2/api/hooks/useAtomInstance.mdx @@ -13,7 +13,7 @@ A React hook that retrieves a cached [atom instance](../classes/AtomInstance.mdx If the atom template takes [params](../classes/AtomInstance.mdx#params), `useAtomInstance` will [deterministically hash](../classes/ZeduxNode.mdx#params) the params you pass to find a cached atom with matching params. If no instance exists for the passed template + params combo, `useAtomInstance` will create a new atom instance and cache it. -Unlike [`useAtomValue`](/not-done?path=./useAtomValue.mdx) and [`useAtomState`](./useAtomState.mdx), `useAtomInstance` creates a [static graph dependency](../glossary.mdx#static-graph-dependency) that does not subscribe to state changes in the retrieved atom. This means the component will not re-render when the atom's state changes. +Unlike [`useAtomValue`](./useAtomValue.mdx) and [`useAtomState`](./useAtomState.mdx), `useAtomInstance` creates a [static graph dependency](../glossary.mdx#static-graph-dependency) that does not subscribe to state changes in the retrieved atom. This means the component will not re-render when the atom's state changes. Some primary uses for this hook are when: @@ -73,7 +73,7 @@ Sometimes, you need both a reference to the atom instance _and_ you want the cur There are a few ways to do this: -1. Pass the atom instance to a dynamic hook like [`useAtomValue`](/not-done?path=./useAtomValue.mdx) or [`useAtomState`](./useAtomState.mdx) after retrieving it from `useAtomInstance`. +1. Pass the atom instance to a dynamic hook like [`useAtomValue`](./useAtomValue.mdx) or [`useAtomState`](./useAtomState.mdx) after retrieving it from `useAtomInstance`. ```tsx function ExampleComponent() { @@ -162,6 +162,6 @@ There are a few ways to do this: - [The `AtomInstance` class](../classes/AtomInstance.mdx) - [The `useAtomState` hook](./useAtomState.mdx) -- [The `useAtomValue` hook](/not-done?path=./useAtomValue.mdx) +- [The `useAtomValue` hook](./useAtomValue.mdx) - [The `injectAtomInstance` injector](/not-done?path=../injectors/injectAtomInstance.mdx) - [The React Hooks walkthrough](/not-done?path=/../../walkthrough/react-hooks.mdx) diff --git a/docs/docs/v2/api/hooks/useAtomSelector.mdx b/docs/docs/v2/api/hooks/useAtomSelector.mdx index 0c3b3537..8e423326 100644 --- a/docs/docs/v2/api/hooks/useAtomSelector.mdx +++ b/docs/docs/v2/api/hooks/useAtomSelector.mdx @@ -6,7 +6,7 @@ title: useAtomSelector import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' :::warning Deprecated -As of v2, this hook is deprecated. Use [`useAtomValue`](/not-done?path=./useAtomValue.mdx) instead - it now handles all Zedux's reactive primitives, making refactoring between them easier. +As of v2, this hook is deprecated. Use [`useAtomValue`](./useAtomValue.mdx) instead - it now handles all Zedux's reactive primitives, making refactoring between them easier. ::: ```ts @@ -55,7 +55,7 @@ function Count() { - The selector's [params](../classes/SelectorInstance.mdx#params) spread into the rest of the arguments to `useAtomSelector`. Note that this is different from [`useAtomValue`](/not-done?path=./useAtomValue.mdx) (and all Zedux APIs in v2) where params are passed via the second argument as an array. + The selector's [params](../classes/SelectorInstance.mdx#params) spread into the rest of the arguments to `useAtomSelector`. Note that this is different from [`useAtomValue`](./useAtomValue.mdx) (and all Zedux APIs in v2) where params are passed via the second argument as an array. Required if the selector has required parameters. @@ -67,5 +67,5 @@ function Count() { ## See Also -- [The `useAtomValue` hook](/not-done?path=./useAtomValue.mdx) +- [The `useAtomValue` hook](./useAtomValue.mdx) - [The Selectors walkthrough](../../../walkthrough/selectors.mdx) diff --git a/docs/docs/v2/api/hooks/useAtomState.mdx b/docs/docs/v2/api/hooks/useAtomState.mdx index 5d8ee688..63e16561 100644 --- a/docs/docs/v2/api/hooks/useAtomState.mdx +++ b/docs/docs/v2/api/hooks/useAtomState.mdx @@ -9,7 +9,7 @@ import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' import { useAtomState } from '@zedux/react' ``` -A React hook that creates a [dynamic graph dependency](../glossary.mdx#dynamic-graph-dependency) on an [atom](../classes/AtomInstance.mdx), triggering component rerenders when the atom updates. This is exactly the same as [`useAtomValue`](/not-done?path=./useAtomValue.mdx), except it returns a `[state, setState]` tuple similar to React's [`useState` hook](https://react.dev/reference/react/useState). +A React hook that creates a [dynamic graph dependency](../glossary.mdx#dynamic-graph-dependency) on an [atom](../classes/AtomInstance.mdx), triggering component rerenders when the atom updates. This is exactly the same as [`useAtomValue`](./useAtomValue.mdx), except it returns a `[state, setState]` tuple similar to React's [`useState` hook](https://react.dev/reference/react/useState). Just like `useAtomValue`, `useAtomState` creates an atom instance if none exists for the given [atom template](../classes/AtomTemplate.mdx) (+ [params](../classes/AtomInstance.mdx#params) combo, if the atom takes params). @@ -134,7 +134,7 @@ const [, { exports, only }] = useAtomState(myAtom) ## See Also -- [The `useAtomValue` hook](/not-done?path=./useAtomValue.mdx) +- [The `useAtomValue` hook](./useAtomValue.mdx) - [The `useAtomInstance` hook](./useAtomInstance.mdx) - [The `injectAtomState` injector](/not-done?path=../injectors/injectAtomState.mdx) - [The React Hooks walkthrough](/not-done?path=../../../walkthrough/react-hooks.mdx) diff --git a/docs/docs/v2/api/hooks/useAtomValue.mdx b/docs/docs/v2/api/hooks/useAtomValue.mdx new file mode 100644 index 00000000..121e1cf3 --- /dev/null +++ b/docs/docs/v2/api/hooks/useAtomValue.mdx @@ -0,0 +1,177 @@ +--- +id: useAtomValue +title: useAtomValue +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +```ts +import { useAtomValue } from '@zedux/react' +``` + +A React hook that creates a [dynamic graph dependency](../glossary.mdx#dynamic-graph-dependency) on an [atom instance](../classes/AtomInstance.mdx), triggering rerenders when the atom updates. Returns the atom's current state. + +If no cached atom instance exists for the given [atom template](../classes/AtomTemplate.mdx) (+ [params](../classes/AtomInstance.mdx#params) combo, if the atom takes params), `useAtomValue` will create one. + +This is the primary hook for consuming atom state in React components. + +## Examples + +Simple example: + +```tsx live ecosystemId=useAtomValue/example resultVar=App version=2 +const greetingAtom = atom('greeting', 'Hello, World!') + +function Greeting() { + // use `useAtomValue` to subscribe to state changes: + const greeting = useAtomValue(greetingAtom) + + return
{greeting}
+} + +function Controls() { + // use `useAtomInstance` to avoid subscribing to state changes: + const greetingInstance = useAtomInstance(greetingAtom) + + return ( + + ) +} + +function App() { + return ( +
+ + +
+ ) +} +``` + +Example using `useAtomValue` to offload side effects from React components to atoms: + +```tsx live ecosystemId=useAtomValue/example resultVar=Seconds version=2 +const secondsAtom = atom('seconds', () => { + const signal = injectSignal(0) + + injectEffect(() => { + const intervalId = setInterval(() => signal.set(state => state + 1), 1000) + + return () => clearInterval(intervalId) + }, []) + + return signal +}) + +function Seconds() { + const state = useAtomValue(secondsAtom) + + return
Seconds: {state}
+} +``` + +A more complex example with React suspense, a [query atom](../../../walkthrough/query-atoms.mdx), and [atom params](../classes/AtomInstance.mdx#params): + +```tsx live ecosystemId=useAtomValue/params resultVar=App version=2 +const userAtom = atom('user', (id: number) => + api( + fetch(`https://jsonplaceholder.typicode.com/users/${id}`).then(res => + res.json() + ) + ) +) + +function UserProfile({ userId }: { userId: number }) { + const { data } = useAtomValue(userAtom, [userId]) + + return ( +
+

{data.name}

+

{data.email}

+
+ ) +} + +function App() { + const [selectedUser, setSelectedUser] = useState(1) + + return ( +
+ + Loading...
}> + + + + ) +} +``` + +Miscellaneous: + +```ts +const val = useAtomValue(myAtom) +const withParams = useAtomValue(myAtom, ['param 1', 'param 2']) +const fromInstance = useAtomValue(instance) +``` + +## Signature + + + {tab1(`useAtomValue = (template, params?, config?) => currentState`)} + {tab2(`declare const useAtomValue: { +
(template: A, params: ParamsOf, config: Omit & { + suspend: false; + }): StateOf; + (template: A, params: ParamsOf, config?: Omit): ResolvedStateOf; + >(template: A): ResolvedStateOf; + (template: ParamlessTemplate): ResolvedStateOf; + (instance: I, params: [], config: Omit & { + suspend: false; + }): StateOf; + (instance: I, params?: [], config?: Omit): ResolvedStateOf; + (template: S, params: ParamsOf, config?: Omit): StateOf; + >(template: S): StateOf; + (template: ParamlessTemplate): StateOf; +}`)} + + + + + Required. The [atom template](../classes/AtomTemplate.mdx) to find or create a cached [instance](../classes/AtomInstance.mdx) of. + + Also accepts an [atom instance](../classes/AtomInstance.mdx) directly. In this case, any passed [`params`](#params) will be ignored and `useAtomValue` will subscribe to the passed instance. + + + + An array. Required if the atom template has required parameters. The parameters to pass to the atom state factory. + + These will be [hashed deterministically](../classes/ZeduxNode.mdx#params) to find a cached instance with matching params. + + + + An object with the following optional fields: + + ```ts + { operation?, suspend? } + ``` + + - **`operation`** - A string. Default: `'useAtomValue'`. This is just for debugging. The default is usually good enough. + - **`suspend`** - A boolean. Default: `true`. Whether `useAtomValue` should trigger React suspense if the atom has a [promise](../classes/AtomInstance.mdx#promise) set. + + + + The current state of the atom instance. This will be kept up to date, as the component will rerender if the atom's state changes. + + + + +## See Also + +- [The `useAtomState` hook](./useAtomState.mdx) +- [The `useAtomInstance` hook](./useAtomInstance.mdx) +- [The `injectAtomValue` injector](/not-done?path=../injectors/injectAtomValue.mdx) +- [The React Hooks walkthrough](/not-done?path=../../../walkthrough/react-hooks.mdx) diff --git a/docs/sidebars.js b/docs/sidebars.js index 45f88ebc..29aeab0b 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -202,6 +202,7 @@ module.exports = { 'v2/api/hooks/useAtomSelector', 'v2/api/hooks/useAtomState', 'v2/api/hooks/useEcosystem', + 'v2/api/hooks/useAtomValue', ], }, { From f4b616fb73cf1ff6808f0b78884537c2ebb85990 Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Fri, 25 Jul 2025 14:06:32 -0400 Subject: [PATCH 25/49] docs: document `injectAtomInstance` for v2 --- docs/docs/v2/api/classes/AtomInstance.mdx | 2 +- docs/docs/v2/api/classes/AtomTemplate.mdx | 2 +- docs/docs/v2/api/glossary.mdx | 2 +- docs/docs/v2/api/hooks/useAtomInstance.mdx | 4 +- .../v2/api/injectors/injectAtomInstance.mdx | 187 ++++++++++++++++++ docs/sidebars.js | 1 + 6 files changed, 193 insertions(+), 5 deletions(-) create mode 100644 docs/docs/v2/api/injectors/injectAtomInstance.mdx diff --git a/docs/docs/v2/api/classes/AtomInstance.mdx b/docs/docs/v2/api/classes/AtomInstance.mdx index c9b90d40..f5342a0f 100644 --- a/docs/docs/v2/api/classes/AtomInstance.mdx +++ b/docs/docs/v2/api/classes/AtomInstance.mdx @@ -356,5 +356,5 @@ Atom instances also inherit the following methods from [`ZeduxNode`](./ZeduxNode - [The `AtomTemplate` class](./AtomTemplate.mdx) - [The `AtomApi` class](./AtomApi.mdx) - [The `useAtomInstance` hook](../hooks/useAtomInstance.mdx) -- [The `injectAtomInstance` injector](/not-done?path=../injectors/injectAtomInstance.mdx) +- [The `injectAtomInstance` injector](../injectors/injectAtomInstance.mdx) - [`ecosystem.getNode`](./Ecosystem.mdx#getnode) diff --git a/docs/docs/v2/api/classes/AtomTemplate.mdx b/docs/docs/v2/api/classes/AtomTemplate.mdx index f3c80c33..a2772aee 100644 --- a/docs/docs/v2/api/classes/AtomTemplate.mdx +++ b/docs/docs/v2/api/classes/AtomTemplate.mdx @@ -157,6 +157,6 @@ When creating your own, custom atom types, you'll usually want to extend this cl - [`useAtomInstance`](../hooks/useAtomInstance.mdx) - [`useAtomState`](../hooks/useAtomState.mdx) - [`useAtomValue`](../hooks/useAtomValue.mdx) - - [`injectAtomInstance`](/not-done?path=../injectors/injectAtomInstance.mdx) + - [`injectAtomInstance`](../injectors/injectAtomInstance.mdx) - [`injectAtomState`](/not-done?path=../injectors/injectAtomState.mdx) - [`injectAtomValue`](/not-done?path=../injectors/injectAtomValue.mdx) diff --git a/docs/docs/v2/api/glossary.mdx b/docs/docs/v2/api/glossary.mdx index 3cc2e1d0..0a7e0345 100644 --- a/docs/docs/v2/api/glossary.mdx +++ b/docs/docs/v2/api/glossary.mdx @@ -40,7 +40,7 @@ Injectors are the "hooks" of Atoms. Zedux exports several injectors. There are 3 basic types of injectors: - React-hook equivalents, like [`injectEffect`](/not-done?path=./injectors/injectEffect), [`injectMemo`](/not-done?path=./injectors/injectMemo), and [`injectRef`](/not-done?path=./injectors/injectRef). -- Dependency injectors, like [`injectAtomValue`](/not-done?path=./injectors/injectAtomValue) and [`injectAtomInstance`](/not-done?path=./injectors/injectAtomInstance). +- Dependency injectors, like [`injectAtomValue`](/not-done?path=./injectors/injectAtomValue) and [`injectAtomInstance`](./injectors/injectAtomInstance.mdx). - Utility or dev X injectors, such as [`injectEcosystem`](./injectors/injectEcosystem.mdx) and [`injectWhy`](/not-done?path=./injectors/injectWhy). Injectors should only be used at the top level of [atom state factories](#state-factory). Don't use them in loops or conditional statements. diff --git a/docs/docs/v2/api/hooks/useAtomInstance.mdx b/docs/docs/v2/api/hooks/useAtomInstance.mdx index 3922bee9..607a0fc9 100644 --- a/docs/docs/v2/api/hooks/useAtomInstance.mdx +++ b/docs/docs/v2/api/hooks/useAtomInstance.mdx @@ -20,7 +20,7 @@ Some primary uses for this hook are when: - you only need to access the atom to update its state, e.g. via [`.set`](../classes/AtomInstance.mdx#set), [`.mutate`](../classes/AtomInstance.mdx#mutate), or by using any custom [`.exports`](../classes/AtomInstance.mdx#exports). - you need to provide an atom to a component subtree via [``](../components/AtomProvider.mdx). -This hook has an equivalent injector: [`injectAtomInstance`](/not-done?path=../injectors/injectAtomInstance.mdx), though it's more common to use [`ecosystem.getNode`](../classes/Ecosystem.mdx#getnode) to create/retrieve atom instances inside other atoms. +This hook has an equivalent injector: [`injectAtomInstance`](../injectors/injectAtomInstance.mdx), though it's more common to use [`ecosystem.getNode`](../classes/Ecosystem.mdx#getnode) to create/retrieve atom instances inside other atoms. ## Example @@ -163,5 +163,5 @@ There are a few ways to do this: - [The `AtomInstance` class](../classes/AtomInstance.mdx) - [The `useAtomState` hook](./useAtomState.mdx) - [The `useAtomValue` hook](./useAtomValue.mdx) -- [The `injectAtomInstance` injector](/not-done?path=../injectors/injectAtomInstance.mdx) +- [The `injectAtomInstance` injector](../injectors/injectAtomInstance.mdx) - [The React Hooks walkthrough](/not-done?path=/../../walkthrough/react-hooks.mdx) diff --git a/docs/docs/v2/api/injectors/injectAtomInstance.mdx b/docs/docs/v2/api/injectors/injectAtomInstance.mdx new file mode 100644 index 00000000..07c8880f --- /dev/null +++ b/docs/docs/v2/api/injectors/injectAtomInstance.mdx @@ -0,0 +1,187 @@ +--- +id: injectAtomInstance +title: injectAtomInstance +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +```ts +import { injectAtomInstance } from '@zedux/react' +``` + +An [injector](../glossary.mdx#injector) that returns a cached [atom instance](../classes/AtomInstance.mdx) for an [atom template](../classes/AtomTemplate.mdx). + +`injectAtomInstance` registers a [static graph dependency](../glossary.mdx#static-graph-dependency) on the resolved atom instance, preventing it from being destroyed automatically. + +Unlike [`injectAtomValue`](/not-done?path=./injectAtomValue.mdx) or [`injectAtomState`](/not-done?path=./injectAtomState.mdx), `injectAtomInstance` doesn't subscribe to state changes by default. + +This injector has an equivalent hook: [`useAtomInstance`](../hooks/useAtomInstance.mdx). + +:::tip +While there's nothing wrong with using this injector if you prefer, it's more common to use [`ecosystem.getNode`](../classes/Ecosystem.mdx#getnode) to create/retrieve atom instances inside other atoms, especially in [ions](../factories/ion.mdx) where the ecosystem is automatically in scope. + +```ts +const exampleAtom = ion('example', ({ getNode }) => { + // these two lines are exactly equivalent: + const instance = getNode(myAtom) + const instance = injectAtomInstance(myAtom) +}) +``` + +::: + +## Examples + +Simple example: + +```tsx live ecosystemId=injectAtomInstance/example resultVar=App version=2 +const countAtom = atom('count', 0) + +const snapshotAtom = atom('snapshot', () => { + const instance = injectAtomInstance(countAtom) + + // Use `getOnce()` to avoid subscribing to state changes: + return `Static snapshot of ${instance.id}: ${instance.getOnce()}` +}) + +const watcherAtom = atom('watcher', () => { + const instance = injectAtomInstance(countAtom) + + // Calling `instance.get()` subscribes to state changes: + return `Watcher of ${instance.id}: ${instance.get()}` +}) + +function App() { + const [count, setCount] = useAtomState(countAtom) + const snapshot = useAtomValue(snapshotAtom) + const watcher = useAtomValue(watcherAtom) + + return ( +
+
Count: {count}
+
{snapshot}
+
{watcher}
+ +
+ ) +} +``` + +Miscellaneous: + +```ts +const instance = injectAtomInstance(myAtom) +const withParams = injectAtomInstance(myAtom, ['param 1', 'param 2']) +``` + +## Dynamicizing the Dependency + +Sometimes, you need both a reference to the atom instance _and_ you want the current atom or selector to reevaluate when the retrieved atom's state changes. + +There are a few ways to do this: + +1. Call [`instance.get()`](../classes/AtomInstance.mdx#get) after retrieving it from `injectAtomInstance`: + + ```tsx + const exampleAtom = atom('example', () => { + const instance = injectAtomInstance(otherAtom) + + // Calling `get` on any of Zedux's reactive primitives automatically + // subscribes to updates. In this case, it upgrades `injectAtomInstance`'s + // static edge to a dynamic one. This `example` atom will now reevaluate + // when the `otherAtom`'s state changes + const dynamicValue = instance.get() + }) + ``` + +2. Pass the atom instance to a dynamic injector like [`injectAtomValue`](/not-done?path=./injectAtomValue.mdx) or [`injectAtomState`](/not-done?path=./injectAtomState.mdx) or to [`ecosystem.get`](../classes/Ecosystem.mdx#get) after retrieving it from `injectAtomInstance`. + + ```tsx + const exampleAtom = atom('example', () => { + const instance = injectAtomInstance(otherAtom) + + // pass the instance directly to `injectAtomValue` to subscribe to updates: + const dynamicValue = injectAtomValue(instance) + // alternatively, pass the instance to `ecosystem.get`: + // const dynamicValue = injectEcosystem().get(instance) + }) + ``` + +3. Pass `{ subscribe: true }` as the third argument to `injectAtomInstance`. + + ```tsx + const exampleAtom = atom('example', () => { + // pass `[]` if the atom doesn't take params: + const instance = injectAtomInstance(otherAtom, [], { subscribe: true }) + + // Even calling `getOnce()` is okay now. This `example` atom will + // reevaluate when `otherAtom`'s state changes: + const dynamicValue = instance.getOnce() + }) + ``` + +:::tip +[`injectAtomState`](./injectAtomState.mdx) is often good enough when you don't need a reference to the full atom instance. +::: + +## Signature + + + {tab1(`injectAtomInstance = (template, params?, config?) => atomInstance`)} + {tab2(`declare const injectAtomInstance: { +
(template: A, params: ParamsOf, config?: InjectAtomInstanceConfig): NodeOf; + >(template: A): NodeOf; + (template: ParamlessTemplate): NodeOf; + (instance: I, params?: [], config?: InjectAtomInstanceConfig): I; + (template: S, params: ParamsOf, config?: InjectAtomInstanceConfig): NodeOf; + >(template: S): NodeOf; + (template: ParamlessTemplate): NodeOf; +}`)} + + + + + Required. An [atom template](../classes/AtomTemplate.mdx) to find or create an instance of. + + You can also pass an [atom instance](../classes/AtomInstance.mdx) directly. This is useful when receiving an atom instance from other sources (outside React) that you don't want to cause rerenders, but that you also need to prevent from being destroyed until the component unmounts. This is a rare use case. + + + + An array of the atom's [params](../classes/AtomInstance.mdx#params). + + TypeScript users will see that this is required if the atom has required params. + + + + An object with the following optional properties: + + ```ts + { operation, subscribe } + ``` + + + + A string. Default: `injectAtomInstance`. Used for debugging to describe the reason for creating this graph edge. This default is usually fine. + + + A boolean. Default: `false`. Pass `subscribe: true` to make `injectAtomInstance` create a [dynamic graph dependency](../glossary.mdx#dynamic-graph-dependency) instead. + + + + See the [React Hooks walkthrough](/not-done?path=/../../walkthrough/react-hooks.mdx) for more details. + + + + The resolved [atom instance](../classes/AtomInstance.mdx) for the passed template + params combo. + + + + +## See Also + +- [The `AtomTemplate` class](../classes/AtomTemplate.mdx) +- [The `AtomInstance` class](../classes/AtomInstance.mdx) +- [The `injectAtomState` injector](/not-done?path=./injectAtomState.mdx) +- [The `injectAtomValue` injector](/not-done?path=./injectAtomValue.mdx) diff --git a/docs/sidebars.js b/docs/sidebars.js index 29aeab0b..a02f6465 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -209,6 +209,7 @@ module.exports = { type: 'category', label: 'Injectors', items: [ + 'v2/api/injectors/injectAtomInstance', 'v2/api/injectors/injectEcosystem', 'v2/api/injectors/injectSignal', ], From df53ac7943f96817b6375fc9c819d86a43e0528f Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Fri, 25 Jul 2025 14:53:07 -0400 Subject: [PATCH 26/49] docs: document `injectAtomState` injector for v2 --- docs/docs/v2/api/classes/AtomTemplate.mdx | 2 +- docs/docs/v2/api/hooks/useAtomState.mdx | 2 +- .../v2/api/injectors/injectAtomInstance.mdx | 12 +- .../docs/v2/api/injectors/injectAtomState.mdx | 111 ++++++++++++++++++ docs/sidebars.js | 1 + 5 files changed, 119 insertions(+), 9 deletions(-) create mode 100644 docs/docs/v2/api/injectors/injectAtomState.mdx diff --git a/docs/docs/v2/api/classes/AtomTemplate.mdx b/docs/docs/v2/api/classes/AtomTemplate.mdx index a2772aee..b65421eb 100644 --- a/docs/docs/v2/api/classes/AtomTemplate.mdx +++ b/docs/docs/v2/api/classes/AtomTemplate.mdx @@ -158,5 +158,5 @@ When creating your own, custom atom types, you'll usually want to extend this cl - [`useAtomState`](../hooks/useAtomState.mdx) - [`useAtomValue`](../hooks/useAtomValue.mdx) - [`injectAtomInstance`](../injectors/injectAtomInstance.mdx) - - [`injectAtomState`](/not-done?path=../injectors/injectAtomState.mdx) + - [`injectAtomState`](../injectors/injectAtomState.mdx) - [`injectAtomValue`](/not-done?path=../injectors/injectAtomValue.mdx) diff --git a/docs/docs/v2/api/hooks/useAtomState.mdx b/docs/docs/v2/api/hooks/useAtomState.mdx index 63e16561..421f31ba 100644 --- a/docs/docs/v2/api/hooks/useAtomState.mdx +++ b/docs/docs/v2/api/hooks/useAtomState.mdx @@ -136,5 +136,5 @@ const [, { exports, only }] = useAtomState(myAtom) - [The `useAtomValue` hook](./useAtomValue.mdx) - [The `useAtomInstance` hook](./useAtomInstance.mdx) -- [The `injectAtomState` injector](/not-done?path=../injectors/injectAtomState.mdx) +- [The `injectAtomState` injector](../injectors/injectAtomState.mdx) - [The React Hooks walkthrough](/not-done?path=../../../walkthrough/react-hooks.mdx) diff --git a/docs/docs/v2/api/injectors/injectAtomInstance.mdx b/docs/docs/v2/api/injectors/injectAtomInstance.mdx index 07c8880f..2084ec1d 100644 --- a/docs/docs/v2/api/injectors/injectAtomInstance.mdx +++ b/docs/docs/v2/api/injectors/injectAtomInstance.mdx @@ -13,9 +13,9 @@ An [injector](../glossary.mdx#injector) that returns a cached [atom instance](.. `injectAtomInstance` registers a [static graph dependency](../glossary.mdx#static-graph-dependency) on the resolved atom instance, preventing it from being destroyed automatically. -Unlike [`injectAtomValue`](/not-done?path=./injectAtomValue.mdx) or [`injectAtomState`](/not-done?path=./injectAtomState.mdx), `injectAtomInstance` doesn't subscribe to state changes by default. +Unlike [`injectAtomValue`](/not-done?path=./injectAtomValue.mdx) or [`injectAtomState`](./injectAtomState.mdx), `injectAtomInstance` doesn't subscribe to state changes by default. -This injector has an equivalent hook: [`useAtomInstance`](../hooks/useAtomInstance.mdx). +This injector has an equivalent React hook: [`useAtomInstance`](../hooks/useAtomInstance.mdx). :::tip While there's nothing wrong with using this injector if you prefer, it's more common to use [`ecosystem.getNode`](../classes/Ecosystem.mdx#getnode) to create/retrieve atom instances inside other atoms, especially in [ions](../factories/ion.mdx) where the ecosystem is automatically in scope. @@ -94,7 +94,7 @@ There are a few ways to do this: }) ``` -2. Pass the atom instance to a dynamic injector like [`injectAtomValue`](/not-done?path=./injectAtomValue.mdx) or [`injectAtomState`](/not-done?path=./injectAtomState.mdx) or to [`ecosystem.get`](../classes/Ecosystem.mdx#get) after retrieving it from `injectAtomInstance`. +2. Pass the atom instance to a dynamic injector like [`injectAtomValue`](/not-done?path=./injectAtomValue.mdx) or [`injectAtomState`](./injectAtomState.mdx) or to [`ecosystem.get`](../classes/Ecosystem.mdx#get) after retrieving it from `injectAtomInstance`. ```tsx const exampleAtom = atom('example', () => { @@ -143,7 +143,7 @@ There are a few ways to do this: - Required. An [atom template](../classes/AtomTemplate.mdx) to find or create an instance of. + Required. The [atom template](../classes/AtomTemplate.mdx) to find or create a cached [instance](../classes/AtomInstance.mdx) of. You can also pass an [atom instance](../classes/AtomInstance.mdx) directly. This is useful when receiving an atom instance from other sources (outside React) that you don't want to cause rerenders, but that you also need to prevent from being destroyed until the component unmounts. This is a rare use case. @@ -170,8 +170,6 @@ There are a few ways to do this: - See the [React Hooks walkthrough](/not-done?path=/../../walkthrough/react-hooks.mdx) for more details. - The resolved [atom instance](../classes/AtomInstance.mdx) for the passed template + params combo. @@ -183,5 +181,5 @@ There are a few ways to do this: - [The `AtomTemplate` class](../classes/AtomTemplate.mdx) - [The `AtomInstance` class](../classes/AtomInstance.mdx) -- [The `injectAtomState` injector](/not-done?path=./injectAtomState.mdx) +- [The `injectAtomState` injector](./injectAtomState.mdx) - [The `injectAtomValue` injector](/not-done?path=./injectAtomValue.mdx) diff --git a/docs/docs/v2/api/injectors/injectAtomState.mdx b/docs/docs/v2/api/injectors/injectAtomState.mdx new file mode 100644 index 00000000..de1ff12b --- /dev/null +++ b/docs/docs/v2/api/injectors/injectAtomState.mdx @@ -0,0 +1,111 @@ +--- +id: injectAtomState +title: injectAtomState +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +```ts +import { injectAtomState } from '@zedux/react' +``` + +An [injector](../glossary.mdx#injector) that retrieves the value of a cached [atom instance](../classes/AtomInstance.mdx) for a given [atom template](../classes/AtomTemplate.mdx). This is exactly like [`injectAtomValue`](/not-done?path=./injectAtomValue.mdx) except it returns a`[state, setState]` tuple similar to React's [`useState` hook](https://react.dev/reference/react/useState). + +Registers a [dynamic graph dependency](../glossary.mdx#dynamic-graph-dependency) on the retrieved atom instance. The injecting atom will reevaluate when the retrieved atom's state changes. + +Creates a new atom instance for the given template (+ params combo if the atom takes [params](../classes/AtomInstance.mdx#params)) if none exists yet. + +This injector has an equivalent React hook: [`useAtomState`](../hooks/useAtomState.mdx). + +## Example + +```tsx live ecosystemId=injectAtomState/example resultVar=App version=2 +const countAtom = atom('count', 0) + +const doubleCountAtom = atom('doubleCount', () => { + const [count, setCount] = injectAtomState(countAtom) + + const decrement = () => setCount(state => state - 1) + const increment = () => setCount(state => state + 1) + const reset = () => setCount(0) + + return api(count * 2).setExports({ decrement, increment, reset }) +}) + +function App() { + const count = useAtomValue(countAtom) + + const [doubleCount, { decrement, increment, reset }] = + useAtomState(doubleCountAtom) + + return ( +
+
Count: {count}
+
Double Count: {doubleCount}
+ + + +
+ ) +} +``` + + + {tab1(`injectAtomState = (template, params?) => [state, setState]`)} + {tab2(`declare const injectAtomState: { +
( + template: A, + params: ParamsOf + ): StateHookTuple, ExportsOf> + + >( + template: A + ): StateHookTuple, ExportsOf> + + ( + template: ParamlessTemplate + ): StateHookTuple, ExportsOf> +}`)} + + + + + Required. The [atom template](../classes/AtomTemplate.mdx) to find or create a cached [instance](../classes/AtomInstance.mdx) of. + + Also accepts an [atom instance](../classes/AtomInstance.mdx) directly. In this case, any passed [`params`](#params) will be ignored and `injectAtomState` will subscribe to the passed instance. + + + + An array of the atom's [params](../classes/AtomInstance.mdx#params). + + TypeScript users will see that this is required if the atom has required params. + + + + An object with the following optional property: + + ```ts + { operation } + ``` + + + + A string. Default: `'injectAtomState'`. Used for debugging to describe the reason for creating this graph edge. This default is usually fine. + + + + + + A `[state, setState]` tuple: + + - **`state`** - The current state of the atom instance. This will be kept up to date, as the injecting atom will reevaluate if the injected atom's state changes. + - **`setState`** - An "exports-infused" setter function that's a thin wrapper around [`atom.set`](../classes/AtomInstance.mdx#set). This function has all the atom's exports attached to the function object itself. + + + + +## See Also + +- [The `injectAtomValue` injector](/not-done?path=./injectAtomValue.mdx) +- [The `injectAtomInstance` injector](./injectAtomInstance.mdx) +- [The `useAtomState` hook](../hooks/useAtomState.mdx) diff --git a/docs/sidebars.js b/docs/sidebars.js index a02f6465..22bba5f3 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -210,6 +210,7 @@ module.exports = { label: 'Injectors', items: [ 'v2/api/injectors/injectAtomInstance', + 'v2/api/injectors/injectAtomState', 'v2/api/injectors/injectEcosystem', 'v2/api/injectors/injectSignal', ], From 95aaf095db2f405f3c6c8012b89d17830e6a3896 Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Fri, 25 Jul 2025 16:05:09 -0400 Subject: [PATCH 27/49] docs: document `injectAtomValue` injector for v2 --- docs/docs/v2/api/classes/AtomTemplate.mdx | 2 +- docs/docs/v2/api/classes/Ecosystem.mdx | 2 +- docs/docs/v2/api/classes/ZeduxNode.mdx | 2 +- docs/docs/v2/api/glossary.mdx | 2 +- docs/docs/v2/api/hooks/useAtomValue.mdx | 2 +- .../v2/api/injectors/injectAtomInstance.mdx | 10 +- .../docs/v2/api/injectors/injectAtomState.mdx | 31 ++--- .../docs/v2/api/injectors/injectAtomValue.mdx | 121 ++++++++++++++++++ docs/docs/v2/api/types/SelectorTemplate.mdx | 2 +- docs/sidebars.js | 1 + 10 files changed, 150 insertions(+), 25 deletions(-) create mode 100644 docs/docs/v2/api/injectors/injectAtomValue.mdx diff --git a/docs/docs/v2/api/classes/AtomTemplate.mdx b/docs/docs/v2/api/classes/AtomTemplate.mdx index b65421eb..e96604f2 100644 --- a/docs/docs/v2/api/classes/AtomTemplate.mdx +++ b/docs/docs/v2/api/classes/AtomTemplate.mdx @@ -159,4 +159,4 @@ When creating your own, custom atom types, you'll usually want to extend this cl - [`useAtomValue`](../hooks/useAtomValue.mdx) - [`injectAtomInstance`](../injectors/injectAtomInstance.mdx) - [`injectAtomState`](../injectors/injectAtomState.mdx) - - [`injectAtomValue`](/not-done?path=../injectors/injectAtomValue.mdx) + - [`injectAtomValue`](../injectors/injectAtomValue.mdx) diff --git a/docs/docs/v2/api/classes/Ecosystem.mdx b/docs/docs/v2/api/classes/Ecosystem.mdx index b38dfca9..b17c0036 100644 --- a/docs/docs/v2/api/classes/Ecosystem.mdx +++ b/docs/docs/v2/api/classes/Ecosystem.mdx @@ -627,7 +627,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context - When called in a [reactive context](../glossary#reactive-context) (e.g. while an atom or selector is evaluating), `get` registers a [dynamic graph dependency](../glossary#dynamic-graph-dependency) between the currently-evaluating node and the retrieved node. - When called outside evaluation (e.g. in an effect or callback), `get` doesn't register any graph dependencies. - Note that [`node.get()`](./ZeduxNode#get) has this exact same behavior. In fact, `ecosystem.get(myAtom)` is almost a shorthand for `ecosystem.getNode(myAtom).get()`, with one key difference: + Note that [`node.get()`](./ZeduxNode#get) has this exact same behavior. In fact, `ecosystem.get(myAtom)` is almost a shorthand for `ecosystem.getNode(myAtom).get()`, with one key difference. See next section: #### Note on Caching diff --git a/docs/docs/v2/api/classes/ZeduxNode.mdx b/docs/docs/v2/api/classes/ZeduxNode.mdx index 2757350c..713b219b 100644 --- a/docs/docs/v2/api/classes/ZeduxNode.mdx +++ b/docs/docs/v2/api/classes/ZeduxNode.mdx @@ -203,7 +203,7 @@ Every node has the following **readonly** properties: useAtomInstance(myAtom, ['b', 'a']) ``` - The only exception to the serializable rule is other atom instances. That's right! You can pass an atom instance to another atom instance. You can then use [`instance.get()`](#get) or [`ecosystem.get(instance)`](./Ecosystem#get), [`injectAtomValue()`](/not-done?path=../injectors/injectAtomValue), or any other dynamic injector to register a [dynamic graph dependency](../glossary#dynamic-graph-dependency) on the passed atom instance. + The only exception to the serializable rule is other atom instances. That's right! You can pass an atom instance to another atom instance. You can then use [`instance.get()`](#get) or [`ecosystem.get(instance)`](./Ecosystem#get), [`injectAtomValue()`](../injectors/injectAtomValue.mdx), or any other dynamic injector to register a [dynamic graph dependency](../glossary#dynamic-graph-dependency) on the passed atom instance. ```tsx live ecosystemId=AtomInstance-params resultVar=Shout version=2 const normalAtom = atom( diff --git a/docs/docs/v2/api/glossary.mdx b/docs/docs/v2/api/glossary.mdx index 0a7e0345..c452d73d 100644 --- a/docs/docs/v2/api/glossary.mdx +++ b/docs/docs/v2/api/glossary.mdx @@ -40,7 +40,7 @@ Injectors are the "hooks" of Atoms. Zedux exports several injectors. There are 3 basic types of injectors: - React-hook equivalents, like [`injectEffect`](/not-done?path=./injectors/injectEffect), [`injectMemo`](/not-done?path=./injectors/injectMemo), and [`injectRef`](/not-done?path=./injectors/injectRef). -- Dependency injectors, like [`injectAtomValue`](/not-done?path=./injectors/injectAtomValue) and [`injectAtomInstance`](./injectors/injectAtomInstance.mdx). +- Dependency injectors, like [`injectAtomValue`](./injectors/injectAtomValue.mdx) and [`injectAtomInstance`](./injectors/injectAtomInstance.mdx). - Utility or dev X injectors, such as [`injectEcosystem`](./injectors/injectEcosystem.mdx) and [`injectWhy`](/not-done?path=./injectors/injectWhy). Injectors should only be used at the top level of [atom state factories](#state-factory). Don't use them in loops or conditional statements. diff --git a/docs/docs/v2/api/hooks/useAtomValue.mdx b/docs/docs/v2/api/hooks/useAtomValue.mdx index 121e1cf3..75373dcb 100644 --- a/docs/docs/v2/api/hooks/useAtomValue.mdx +++ b/docs/docs/v2/api/hooks/useAtomValue.mdx @@ -173,5 +173,5 @@ const fromInstance = useAtomValue(instance) - [The `useAtomState` hook](./useAtomState.mdx) - [The `useAtomInstance` hook](./useAtomInstance.mdx) -- [The `injectAtomValue` injector](/not-done?path=../injectors/injectAtomValue.mdx) +- [The `injectAtomValue` injector](../injectors/injectAtomValue.mdx) - [The React Hooks walkthrough](/not-done?path=../../../walkthrough/react-hooks.mdx) diff --git a/docs/docs/v2/api/injectors/injectAtomInstance.mdx b/docs/docs/v2/api/injectors/injectAtomInstance.mdx index 2084ec1d..a71a0d41 100644 --- a/docs/docs/v2/api/injectors/injectAtomInstance.mdx +++ b/docs/docs/v2/api/injectors/injectAtomInstance.mdx @@ -13,7 +13,7 @@ An [injector](../glossary.mdx#injector) that returns a cached [atom instance](.. `injectAtomInstance` registers a [static graph dependency](../glossary.mdx#static-graph-dependency) on the resolved atom instance, preventing it from being destroyed automatically. -Unlike [`injectAtomValue`](/not-done?path=./injectAtomValue.mdx) or [`injectAtomState`](./injectAtomState.mdx), `injectAtomInstance` doesn't subscribe to state changes by default. +Unlike [`injectAtomValue`](./injectAtomValue.mdx) or [`injectAtomState`](./injectAtomState.mdx), `injectAtomInstance` doesn't subscribe to state changes by default. This injector has an equivalent React hook: [`useAtomInstance`](../hooks/useAtomInstance.mdx). @@ -22,9 +22,10 @@ While there's nothing wrong with using this injector if you prefer, it's more co ```ts const exampleAtom = ion('example', ({ getNode }) => { - // these two lines are exactly equivalent: + // all three of these are exactly equivalent: const instance = getNode(myAtom) const instance = injectAtomInstance(myAtom) + const instance = injectEcosystem().getNode(myAtom) }) ``` @@ -94,7 +95,7 @@ There are a few ways to do this: }) ``` -2. Pass the atom instance to a dynamic injector like [`injectAtomValue`](/not-done?path=./injectAtomValue.mdx) or [`injectAtomState`](./injectAtomState.mdx) or to [`ecosystem.get`](../classes/Ecosystem.mdx#get) after retrieving it from `injectAtomInstance`. +2. Pass the atom instance to a dynamic injector like [`injectAtomValue`](./injectAtomValue.mdx) or [`injectAtomState`](./injectAtomState.mdx) or to [`ecosystem.get`](../classes/Ecosystem.mdx#get) after retrieving it from `injectAtomInstance`. ```tsx const exampleAtom = atom('example', () => { @@ -179,7 +180,8 @@ There are a few ways to do this: ## See Also +- [`ecosystem.getNode`](../classes/Ecosystem.mdx#getnode) - [The `AtomTemplate` class](../classes/AtomTemplate.mdx) - [The `AtomInstance` class](../classes/AtomInstance.mdx) - [The `injectAtomState` injector](./injectAtomState.mdx) -- [The `injectAtomValue` injector](/not-done?path=./injectAtomValue.mdx) +- [The `injectAtomValue` injector](./injectAtomValue.mdx) diff --git a/docs/docs/v2/api/injectors/injectAtomState.mdx b/docs/docs/v2/api/injectors/injectAtomState.mdx index de1ff12b..1e3cbf5c 100644 --- a/docs/docs/v2/api/injectors/injectAtomState.mdx +++ b/docs/docs/v2/api/injectors/injectAtomState.mdx @@ -9,7 +9,7 @@ import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' import { injectAtomState } from '@zedux/react' ``` -An [injector](../glossary.mdx#injector) that retrieves the value of a cached [atom instance](../classes/AtomInstance.mdx) for a given [atom template](../classes/AtomTemplate.mdx). This is exactly like [`injectAtomValue`](/not-done?path=./injectAtomValue.mdx) except it returns a`[state, setState]` tuple similar to React's [`useState` hook](https://react.dev/reference/react/useState). +An [injector](../glossary.mdx#injector) that retrieves the value of a cached [atom instance](../classes/AtomInstance.mdx) for a given [atom template](../classes/AtomTemplate.mdx). This is exactly like [`injectAtomValue`](./injectAtomValue.mdx) except it returns a`[state, setState]` tuple similar to React's [`useState` hook](https://react.dev/reference/react/useState). Registers a [dynamic graph dependency](../glossary.mdx#dynamic-graph-dependency) on the retrieved atom instance. The injecting atom will reevaluate when the retrieved atom's state changes. @@ -50,21 +50,22 @@ function App() { } ``` +## Signature + - {tab1(`injectAtomState = (template, params?) => [state, setState]`)} + {tab1(`injectAtomState = (template, params?, config?) => [state, setState]`)} {tab2(`declare const injectAtomState: { - ( - template: A, - params: ParamsOf - ): StateHookTuple, ExportsOf> - - >( - template: A - ): StateHookTuple, ExportsOf> - - ( - template: ParamlessTemplate - ): StateHookTuple, ExportsOf> + >(template: A, params: ParamsOf): StateHookTuple, ExportsOf>; + >(template: A): StateHookTuple, ExportsOf>; + >(template: ParamlessTemplate): StateHookTuple, ExportsOf>; + (instance: I): StateHookTuple, ExportsOf>; }`)} @@ -106,6 +107,6 @@ function App() { ## See Also -- [The `injectAtomValue` injector](/not-done?path=./injectAtomValue.mdx) +- [The `injectAtomValue` injector](./injectAtomValue.mdx) - [The `injectAtomInstance` injector](./injectAtomInstance.mdx) - [The `useAtomState` hook](../hooks/useAtomState.mdx) diff --git a/docs/docs/v2/api/injectors/injectAtomValue.mdx b/docs/docs/v2/api/injectors/injectAtomValue.mdx new file mode 100644 index 00000000..dd91f008 --- /dev/null +++ b/docs/docs/v2/api/injectors/injectAtomValue.mdx @@ -0,0 +1,121 @@ +--- +id: injectAtomValue +title: injectAtomValue +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +```ts +import { injectAtomValue } from '@zedux/react' +``` + +An [injector](../glossary.mdx#injector) that retrieves the value of a cached [atom instance](../classes/AtomInstance.mdx) for a given [atom template](../classes/AtomTemplate.mdx). + +Returns the current state of the atom and registers a [dynamic graph dependency](../glossary.mdx#dynamic-graph-dependency) on the retrieved atom instance. The injecting atom will reevaluate when the retrieved atom's state changes. + +Creates a new atom instance for the given template (+ params combo if the atom takes [params](../classes/AtomInstance.mdx#params)) if none exists yet. + +This injector has an equivalent React hook: [`useAtomValue`](../hooks/useAtomValue.mdx). + +:::tip +While there's nothing wrong with using this injector if you prefer, it's more common to use [`ecosystem.get`](../classes/Ecosystem.mdx#get) to create/retrieve atom instances inside other atoms, especially in [ions](../factories/ion.mdx) where the ecosystem is automatically in scope. + +```ts +const exampleAtom = ion('example', ({ get }) => { + // all three of these are exactly equivalent: + const value = get(myAtom) + const value = injectAtomValue(myAtom) + const value = injectEcosystem().get(myAtom) +}) +``` + +::: + +## Example + +```tsx live ecosystemId=injectAtomValue/example resultVar=App version=2 +const nameAtom = atom('name', 'World') +const countAtom = atom('count', 0) + +const greetingAtom = atom('greeting', () => { + const name = injectAtomValue(nameAtom) + const count = injectAtomValue(countAtom) + + return `Hello, ${name}! Count: ${count}` +}) + +function App() { + const [name, setName] = useAtomState(nameAtom) + const [count, setCount] = useAtomState(countAtom) + const greeting = useAtomValue(greetingAtom) + + return ( +
+
{greeting}
+ setName(e.target.value)} + placeholder="Enter name" + /> + +
+ ) +} +``` + +## Signature + + + {tab1(`injectAtomValue = (template, params?, config?) => currentState`)} + {tab2(`declare const injectAtomValue: { +
(template: A, params: ParamsOf): StateOf; + >(template: A): StateOf; + (template: ParamlessTemplate): StateOf; + (instance: I): StateOf; + (template: S, params: ParamsOf): StateOf; + >(template: S): StateOf; + (template: ParamlessTemplate): StateOf; +}`)} + + + + + Required. The [atom template](../classes/AtomTemplate.mdx) to find or create a cached [instance](../classes/AtomInstance.mdx) of. + + Also accepts an [atom instance](../classes/AtomInstance.mdx) directly. In this case, any passed [`params`](#params) will be ignored and `injectAtomValue` will subscribe to the passed instance. + + + + An array of the atom's [params](../classes/AtomInstance.mdx#params). + + TypeScript users will see that this is required if the atom has required params. + + + + An object with the following optional property: + + ```ts + { operation } + ``` + + + + A string. Default: `'injectAtomValue'`. Used for debugging to describe the reason for creating this graph edge. This default is usually fine. + + + + + + The current state of the atom instance. This will be kept up to date, as the injecting atom will reevaluate if the injected atom's state changes. + + + + +## See Also + +- [`ecosystem.get`](../classes/Ecosystem.mdx#get) +- [The `injectAtomState` injector](./injectAtomState.mdx) +- [The `injectAtomInstance` injector](./injectAtomInstance.mdx) +- [The `useAtomValue` hook](../hooks/useAtomValue.mdx) diff --git a/docs/docs/v2/api/types/SelectorTemplate.mdx b/docs/docs/v2/api/types/SelectorTemplate.mdx index 694fb97f..5069eb9b 100644 --- a/docs/docs/v2/api/types/SelectorTemplate.mdx +++ b/docs/docs/v2/api/types/SelectorTemplate.mdx @@ -69,7 +69,7 @@ An object that defines a selector plus some additional configuration: Return false to allow the selector to reevaluate. In this case, if the params create a different [hash](../classes/ZeduxNode.mdx#params), the selector will be reevaluated. - This config option is only respected by Zedux's React hooks and only runs after the selector has already evaluated once and is about to reevaluate. Its use is to prevent excess React rerenders from running the selector unnecessarily. This option is a noop when passing selectors to other Zedux APIs like [`ecosystem.get`](../classes/Ecosystem.mdx#get), [`ecosystem.getNode`](../classes/Ecosystem.mdx#getnode), or [`injectAtomValue`](/not-done?path=../injectors/injectAtomValue.mdx). + This config option is only respected by Zedux's React hooks and only runs after the selector has already evaluated once and is about to reevaluate. Its use is to prevent excess React rerenders from running the selector unnecessarily. This option is a noop when passing selectors to other Zedux APIs like [`ecosystem.get`](../classes/Ecosystem.mdx#get), [`ecosystem.getNode`](../classes/Ecosystem.mdx#getnode), or [`injectAtomValue`](../injectors/injectAtomValue.mdx). diff --git a/docs/sidebars.js b/docs/sidebars.js index 22bba5f3..e2382974 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -211,6 +211,7 @@ module.exports = { items: [ 'v2/api/injectors/injectAtomInstance', 'v2/api/injectors/injectAtomState', + 'v2/api/injectors/injectAtomValue', 'v2/api/injectors/injectEcosystem', 'v2/api/injectors/injectSignal', ], From 97b1ea9855928ba6aab6b405fe6d754e7cead954 Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Fri, 25 Jul 2025 18:02:09 -0400 Subject: [PATCH 28/49] docs: document `injectCallback` injector for v2 --- docs/docs/v2/api/classes/AtomApi.mdx | 4 +- docs/docs/v2/api/injectors/injectCallback.mdx | 148 ++++++++++++++++++ docs/sidebars.js | 1 + 3 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 docs/docs/v2/api/injectors/injectCallback.mdx diff --git a/docs/docs/v2/api/classes/AtomApi.mdx b/docs/docs/v2/api/classes/AtomApi.mdx index 1c0a6628..99a9a8fe 100644 --- a/docs/docs/v2/api/classes/AtomApi.mdx +++ b/docs/docs/v2/api/classes/AtomApi.mdx @@ -179,7 +179,9 @@ AtomApis expose the following **readonly** properties: When an AtomApi is returned from a state factory, these will become the atom instance's [`.exports`](./AtomInstance#exports). - **New in v2:** These exports can change on subsequent evaluations - they no longer have to be stable references. Exports are not part of the atom's state, meaning they don't trigger updates in consumers when changed. However, Zedux works around this by wrapping exported functions. See [`addExports`](#addexports) and [`setExports`](#setexports) for more information. + **New in v2:** These exports can change on subsequent evaluations - they no longer have to be stable references. Exports are not part of the atom's state, meaning they don't trigger updates in consumers when changed. However, Zedux works around this by wrapping exported functions. + + See [`addExports`](#addexports) and [`setExports`](#setexports) for more information on function wrapping. diff --git a/docs/docs/v2/api/injectors/injectCallback.mdx b/docs/docs/v2/api/injectors/injectCallback.mdx new file mode 100644 index 00000000..cebc3e6e --- /dev/null +++ b/docs/docs/v2/api/injectors/injectCallback.mdx @@ -0,0 +1,148 @@ +--- +id: injectCallback +title: injectCallback +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +```ts +import { injectCallback } from '@zedux/react' +``` + +An [injector](../glossary.mdx#injector) for creating memoized callback functions inside atoms. This is the injector equivalent of React's [`useCallback` hook](https://react.dev/reference/react/useCallback). + +Like `useCallback`, `injectCallback` returns a memoized version of the callback that only changes when any passed dependencies change. + +Unlike React's `useCallback`, this is less useful for performance optimizations since it doesn't usually matter if function references created in an atom [state factory](../glossary.mdx#state-factory) change on subsequent evaluations. + +Instead, the main purpose of `injectCallback` is to automatically [batch](../classes/Ecosystem.mdx#batch) and, if the atom is [scoped](../glossary.mdx#scoped-atom), [scope](../classes/Ecosystem.mdx#withscope) the callback. + +`injectCallback` is essentially an alias for this: + +```ts +const ecosystem = injectEcosystem() +const self = injectSelf() + +const callback = injectMemo(() => { + return (myParam: string) => { + return ecosystem.batch(() => { + // `.V` is currently where an atom's used scope is stored. We are planning + // to change this to a much more user-friendly `.scope` property before v2 + // officially releases. + return ecosystem.withScope(self.V, () => { + // ... do stuff here! ... + }) + }) + } +}, [someDep]) + +// equivalent using `injectCallback`: +const callback = injectCallback( + (myParam: string) => { + // ... do stuff here! ... + }, + [someDep] +) +``` + +:::warning These are not the joys you're looking for +While this looks useful, `injectCallback` is rarely needed. + +[Exported functions](../classes/AtomApi.mdx#exports) are already automatically batched and scoped. You typically don't need `injectCallback` unless a function is both used locally, e.g. in an [`injectEffect`](/not-done?path=./injectEffect.mdx), and exported. +::: + +## Example + +```tsx live ecosystemId=injectCallback/example resultVar=App version=2 +const counterAtom = atom('counter', () => { + const signal = injectSignal(0) + + const increment = injectCallback(() => { + signal.set(state => state + 1) + }, []) // `signal` is stable, no need to pass it as a dependency + + const incrementTwice = injectCallback(() => { + // the automatic batching prevents excess reevaluations in this case: + signal.set(state => state + 1) + signal.set(state => state + 1) + }, []) + + // uncomment this log and remove `incrementTwice`'s `injectCallback` to see + // how batching prevents excess reevaluations: + // console.log('reevaluated!', signal.get()) + + const decrement = injectCallback(() => { + signal.set(state => state - 1) + }, []) + + const incrementBy = injectCallback((amount: number) => { + signal.set(state => state + amount) + }, []) + + return api(signal).setExports( + { + decrement, + increment, + incrementBy, + incrementTwice, + }, + // since all these exports are already wrapped by `injectCallback`, there's + // no need to make `setExports` wrap them again. So we pass `wrap: false`. + // This is just a small optimization. + { wrap: false } + ) +}) + +function App() { + const [count, { decrement, increment, incrementBy, incrementTwice }] = + useAtomState(counterAtom) + + return ( +
+
Count: {count}
+ + + + +
+ ) +} +``` + +## Signature + + + {tab1(`injectCallback = (callback, deps) => memoizedCallback`)} + {tab2( + `declare const injectCallback: ( + callback: (...args: Args) => Ret, + deps?: InjectorDeps + ) => (...args: Args) => Ret` + )} + + + + + Required. The function to memoize. Can accept any arguments and return any value. + + + + Optional. An array of values that the callback uses. + + When these dependencies change on subsequent evaluations, Zedux will swap out the cached callback function reference. If no dependencies change, Zedux returns the previously-cached function. + + + + A memoized, auto-[batched](../classes/Ecosystem.mdx#batch), auto-[scoped](../classes/Ecosystem.mdx#withscope) version of the callback function. + + + + +## See Also + +- [The `injectMemo` injector](/not-done?path=./injectMemo.mdx) +- [`ecosystem.batch`](../classes/Ecosystem.mdx#batch) +- [`ecosystem.withScope`](../classes/Ecosystem.mdx#withscope) +- Export wrapping is functionally similar and usually preferable to `injectCallback`. See these methods on `AtomApi`: + - [`api().setExports`](../classes/AtomApi.mdx#setexports) + - [`api().addExports`](../classes/AtomApi.mdx#addexports) diff --git a/docs/sidebars.js b/docs/sidebars.js index e2382974..ee660252 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -212,6 +212,7 @@ module.exports = { 'v2/api/injectors/injectAtomInstance', 'v2/api/injectors/injectAtomState', 'v2/api/injectors/injectAtomValue', + 'v2/api/injectors/injectCallback', 'v2/api/injectors/injectEcosystem', 'v2/api/injectors/injectSignal', ], From 94d8f41b1c2159ea14e7e98b5b164ab4098d3c9d Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Thu, 31 Jul 2025 09:45:40 -0400 Subject: [PATCH 29/49] docs: document `injectEffect` injector for v2 --- docs/docs/v2/api/classes/Ecosystem.mdx | 2 +- docs/docs/v2/api/glossary.mdx | 2 +- docs/docs/v2/api/injectors/injectCallback.mdx | 2 +- docs/docs/v2/api/injectors/injectEffect.mdx | 272 ++++++++++++++++++ docs/sidebars.js | 1 + 5 files changed, 276 insertions(+), 3 deletions(-) create mode 100644 docs/docs/v2/api/injectors/injectEffect.mdx diff --git a/docs/docs/v2/api/classes/Ecosystem.mdx b/docs/docs/v2/api/classes/Ecosystem.mdx index b17c0036..029c2240 100644 --- a/docs/docs/v2/api/classes/Ecosystem.mdx +++ b/docs/docs/v2/api/classes/Ecosystem.mdx @@ -457,7 +457,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context This is set to the `ssr` value you passed via [EcosystemConfig](/not-done?path=../types/EcosystemConfig) when creating this ecosystem. - Currently the only thing this affects is [`injectEffect()`](/not-done?path=../injectors/injectEffect) - SSR mode prevents effects from running at all in this ecosystem. + Currently the only thing this affects is [`injectEffect()`](../injectors/injectEffect.mdx) - SSR mode prevents effects from running at all in this ecosystem.
diff --git a/docs/docs/v2/api/glossary.mdx b/docs/docs/v2/api/glossary.mdx index c452d73d..914d2a8b 100644 --- a/docs/docs/v2/api/glossary.mdx +++ b/docs/docs/v2/api/glossary.mdx @@ -39,7 +39,7 @@ Injectors are the "hooks" of Atoms. Zedux exports several injectors. There are 3 basic types of injectors: -- React-hook equivalents, like [`injectEffect`](/not-done?path=./injectors/injectEffect), [`injectMemo`](/not-done?path=./injectors/injectMemo), and [`injectRef`](/not-done?path=./injectors/injectRef). +- React-hook equivalents, like [`injectEffect`](./injectors/injectEffect.mdx), [`injectMemo`](/not-done?path=./injectors/injectMemo), and [`injectRef`](/not-done?path=./injectors/injectRef). - Dependency injectors, like [`injectAtomValue`](./injectors/injectAtomValue.mdx) and [`injectAtomInstance`](./injectors/injectAtomInstance.mdx). - Utility or dev X injectors, such as [`injectEcosystem`](./injectors/injectEcosystem.mdx) and [`injectWhy`](/not-done?path=./injectors/injectWhy). diff --git a/docs/docs/v2/api/injectors/injectCallback.mdx b/docs/docs/v2/api/injectors/injectCallback.mdx index cebc3e6e..7328d683 100644 --- a/docs/docs/v2/api/injectors/injectCallback.mdx +++ b/docs/docs/v2/api/injectors/injectCallback.mdx @@ -48,7 +48,7 @@ const callback = injectCallback( :::warning These are not the joys you're looking for While this looks useful, `injectCallback` is rarely needed. -[Exported functions](../classes/AtomApi.mdx#exports) are already automatically batched and scoped. You typically don't need `injectCallback` unless a function is both used locally, e.g. in an [`injectEffect`](/not-done?path=./injectEffect.mdx), and exported. +[Exported functions](../classes/AtomApi.mdx#exports) are already automatically batched and scoped. You typically don't need `injectCallback` unless a function is both used locally, e.g. in an [`injectEffect`](./injectEffect.mdx), and exported. ::: ## Example diff --git a/docs/docs/v2/api/injectors/injectEffect.mdx b/docs/docs/v2/api/injectors/injectEffect.mdx new file mode 100644 index 00000000..b6bf125f --- /dev/null +++ b/docs/docs/v2/api/injectors/injectEffect.mdx @@ -0,0 +1,272 @@ +--- +id: injectEffect +title: injectEffect +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +```ts +import { injectEffect } from '@zedux/react' +``` + +An [injector](../glossary.mdx#injector) for running side effects in atoms. Similar to React's [`useEffect` hook](https://react.dev/reference/react/useEffect), it runs a callback function and optionally cleans up after it. + +The effect will run on initial atom creation and re-run when any of the specified dependencies change on subsequent evaluations. + +When a cleanup function is returned, it will be called when the effect is about to run again or when the atom is destroyed. + +## Examples + +Run an effect once on initial atom creation: + +```tsx live ecosystemId=injectEffect/example resultVar=App version=2 +const timerAtom = atom('timer', () => { + const signal = injectSignal(0) + + injectEffect(() => { + const interval = setInterval(() => { + signal.set(count => count + 1) + }, 1000) + + return () => clearInterval(interval) + }, []) + + return signal +}) + +function App() { + const timer = useAtomValue(timerAtom) + + return
Timer: {timer} seconds
+} +``` + +Run an effect every time dependencies change: + +```tsx live ecosystemId=injectEffect/dependencies resultVar=App version=2 +const counterAtom = atom('counter', () => { + const signal = injectSignal(0) + const { status } = injectSelf() + + injectEffect(() => { + if (status !== 'Initializing') { + window.alert(`Value changed to: ${signal.get()}`) + } + }, [signal.get()]) + + return signal +}) + +function App() { + const [count, setCount] = useAtomState(counterAtom) + + return ( +
+
Count: {count}
+ +
+ ) +} +``` + +## Effects Run When It's Safe + +Proper effect timing is tricky to get right. To make effects as intuitive as possible, Zedux runs effects as soon as it detects that it's "safe" to do so: + +- When an atom is created outside React, effects run immediately after the atom (and any other atoms it created) finishes evaluating. +- When an atom is created during a React render, effects run in a microtask after initial atom evaluation. + +This ensures a few things: + +- When using atoms outside React, especially in tests, effects can register event listeners before those events fire. +- When an atom is created during React render, effects don't cause bad React state updates during render. + +The bottom line is that **you typically don't need to worry about effect timing**. However, there's one scenario that Zedux can't fully handle by itself: When a test `render`s a test component that creates an atom that has a side effect. + +In this case, the effect(s) will be scheduled to run in a microtask (since the atom was created during a React render). Either wait for effects to flush or flush them manually before continuing the test: + +- Wait for effects to flush: + + ```tsx + const { findByTestId } = render() + + await Promise.resolve() // wait for microtasks to flush + ``` + +- Flush effects manually with [`ecosystem.asyncScheduler`](../classes/Ecosystem.mdx#asyncscheduler): + + ```tsx + const { findByTestId } = render() + + ecosystem.asyncScheduler.flush() // flush Zedux effects synchronously + ``` + + This is safe to run "just in case" whenever you know React isn't currently rendering. So consider baking this into a custom `render` function. + +## You Might Not Need an Effect + +Many of the principles in React's famous ["You Might Not Need an Effect" doc](https://react.dev/learn/you-might-not-need-an-effect) apply to `injectEffect`. + +### Transformations + +You don't typically need effects to transform data. Prefer selectors or ions that naturally derive data. Or just do the transformation inline during atom evaluation if it's cheap. Also consider [`injectMemo`](/not-done?path=./injectMemo.mdx) if it's less cheap. + +Bad example: + +```tsx +const multiplierAtom = atom('multiplier', 2) + +const computedAtom = atom('computed', () => { + const multiplier = injectAtomValue(multiplierAtom) + const signal = injectSignal(0) + + // This is bad practice! You don't need an effect for this: + injectEffect(() => { + signal.set(10 * multiplier) + }, [multiplier]) + + return signal +}) +``` + +Here are some better approaches: + +- Use an [atom selector](../types/SelectorTemplate.mdx): + + ```tsx + const getComputed = ({ get }: Ecosystem) => 10 * get(multiplierAtom) + ``` + +- Derive the value directly in an [ion](../factories/ion.mdx): + + ```tsx + const computedAtom = ion('computed', ({ get }) => { + return 10 * get(multiplierAtom) + }) + ``` + +- Derive the value directly in an atom: + + ```tsx + const computedAtom = atom('computed', () => { + const multiplier = injectAtomValue(multiplierAtom) + + return 10 * multiplier + }) + ``` + +- Use [`injectMemo`](/not-done?path=./injectMemo.mdx) if needed: + + ```tsx + const computedAtom = atom('computed', () => { + const multiplier = injectAtomValue(multiplierAtom) + + // this simple computation obviously doesn't need `injectMemo` + return injectMemo(() => 10 * multiplier, [multiplier]) + }) + ``` + +- If the `computed` atom uses a signal for its state, set it during evaluation: + + ```tsx + const computedAtom = atom('computed', () => { + const multiplier = injectAtomValue(multiplierAtom) + const signal = injectSignal(0) + + // if state doesn't change, `.set` is a no-op. So this call is safe. But if + // this call could possibly change the state (e.g. if the signal's state is + // an object), wrap this in an `if` statement to prevent an infinite + // evaluation loop: + signal.set(10 * multiplier) + + return signal + ``` + +### Callbacks + +Don't use effects to handle user events. Prefer callbacks that kick off side effects directly, avoiding the indirection of `injectEffect`. + +Bad example: + +```tsx +const usersAtom = atom('users', () => { + const signal = injectSignal({ status: 'idle' }) + + injectEffect(() => { + if (signal.get().status === 'loading') { + // fetch users here... + } + }, [signal.get().status]) + + const fetchUsers = () => { + // This is bad practice! You don't need to reroute execution to an effect + signal.set({ status: 'loading' }) + } + + return api(signal).setExports({ fetchUsers }) +}) +``` + +Instead, kick off the side effect directly in the callback: + +```tsx +const usersAtom = atom('users', () => { + const signal = injectSignal({ status: 'idle' } + + const fetchUsers = async () => { + signal.set({ status: 'loading' }) + // fetch users here... + } + + return api(signal).setExports({ fetchUsers }) +}) +``` + +### Async State + +Additionally, in Zedux, consider using [`injectPromise`](/not-done?path=./injectPromise.mdx) or [query atoms](../../../walkthrough/query-atoms.mdx) instead of `injectEffect` to run async operations and track their state. + +## Signature + + + {tab1(`injectEffect = (callback, deps?, config?) => void`)} + {tab2(`declare const injectEffect: (effect: EffectCallback, deps?: InjectorDeps, config?: { + synchronous?: boolean; +}) => void`)} + + + + + Required. A function that contains the side effect logic. Can optionally return a cleanup function that will be called when the effect is cleaned up. + + + + Optional. An array of dependencies. The effect will re-run when any dependency changes. + + If not provided, the effect runs after every atom evaluation. If an empty array `[]` is provided, the effect only runs once when the atom is first created. + + + + Optional. An object with the following properties: + + ```ts + { synchronous? } + ``` + + - `synchronous`: A boolean. If `true`, the effect runs immediately, during atom evaluation. Defaults to `false`. + + You typically don't need this! Zedux is very intelligent about when it runs effects. See [above](#effects-run-when-its-safe). + + + + Nothing (`void`). + + + + +## See Also + +- [The side effects walkthrough](../../../walkthrough/side-effects.mdx) +- [The `injectPromise` injector](/not-done?path=./injectPromise.mdx) diff --git a/docs/sidebars.js b/docs/sidebars.js index ee660252..86eeb4ab 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -214,6 +214,7 @@ module.exports = { 'v2/api/injectors/injectAtomValue', 'v2/api/injectors/injectCallback', 'v2/api/injectors/injectEcosystem', + 'v2/api/injectors/injectEffect', 'v2/api/injectors/injectSignal', ], }, From e9e1309a772edd22716d77d796399c499dd3e374 Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Fri, 1 Aug 2025 12:10:06 -0400 Subject: [PATCH 30/49] docs: document `injectHydration` injector for v2 --- docs/docs/v2/api/classes/SelectorInstance.mdx | 2 +- docs/docs/v2/api/factories/ion.mdx | 9 +- .../docs/v2/api/injectors/injectHydration.mdx | 166 ++++++++++++++++++ docs/sidebars.js | 1 + .../atoms/src/injectors/injectHydration.ts | 3 +- 5 files changed, 176 insertions(+), 5 deletions(-) create mode 100644 docs/docs/v2/api/injectors/injectHydration.mdx diff --git a/docs/docs/v2/api/classes/SelectorInstance.mdx b/docs/docs/v2/api/classes/SelectorInstance.mdx index b1eff444..126de013 100644 --- a/docs/docs/v2/api/classes/SelectorInstance.mdx +++ b/docs/docs/v2/api/classes/SelectorInstance.mdx @@ -54,7 +54,7 @@ const groupedUsersAtom = ion('groupedUsers', ({ get }) => { ## Destruction -Selector instances are always destroyed when they're no longer in use. This is the same behavior as atoms configured with [`ttl: 0`](./AtomInstance#ttl). +Selector instances are always destroyed when they're no longer in use. This is the same behavior as atoms configured with [`ttl: 0`](./AtomInstance#ttl). It's also the default behavior of [ions](../factories/ion.mdx). ## Inline Selectors diff --git a/docs/docs/v2/api/factories/ion.mdx b/docs/docs/v2/api/factories/ion.mdx index 72f6ed11..a24b68d7 100644 --- a/docs/docs/v2/api/factories/ion.mdx +++ b/docs/docs/v2/api/factories/ion.mdx @@ -137,7 +137,7 @@ function App() { - Required. A function that receives the ecosystem as the first parameter. The remaining parameters are the parameters of the atom. + Required. A function that receives the ecosystem as the first parameter. The remaining parameters are the [params](../classes/AtomInstance.mdx#params) of the atom. See [the `atom()` factory](./atom.mdx#valueorfactory) for details on what the state factory can return and how that determines the atom's behavior. @@ -145,15 +145,18 @@ function App() { Optional. An [AtomConfig](/not-done?path=../types/AtomConfig) object. + Note that the `ttl` config option is set to `0` by default for ions. + - An ion template (selector template) that can be used with atom hooks and ecosystem methods. + An [atom template](../classes/AtomTemplate.mdx) that can be used with Zedux's injectors, React hooks, and ecosystem methods. ## See Also +- [The AtomTemplate class](../classes/AtomTemplate.mdx) - [The SelectorInstance class](../classes/SelectorInstance.mdx) - [The AtomApi class](../classes/AtomApi.mdx) -- [The Selectors walkthrough](../../../walkthrough/selectors.mdx) +- [The selectors walkthrough](../../../walkthrough/selectors.mdx) diff --git a/docs/docs/v2/api/injectors/injectHydration.mdx b/docs/docs/v2/api/injectors/injectHydration.mdx new file mode 100644 index 00000000..413bb2f0 --- /dev/null +++ b/docs/docs/v2/api/injectors/injectHydration.mdx @@ -0,0 +1,166 @@ +--- +id: injectHydration +title: injectHydration +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +```ts +import { injectHydration } from '@zedux/react' +``` + +An [injector](../glossary.mdx#injector) that retrieves data previously passed to [`ecosystem.hydrate`](../classes/Ecosystem.mdx#hydrate) for the currently-evaluating atom. + +If the atom has a [`hydrate`](/not-done?path=../types/AtomConfig.mdx#hydrate) function configured, `injectHydration` will call it with the atom's hydration and return the result. Pass `{ transform: false }` to prevent this. + +`injectHydration` is unique in that it has a side effect: When called, it prevents Zedux from [automatically hydrating](/not-done?path=../../advanced/persistence#automatic-hydration) the atom after its initial evaluation. Pass `{ intercept: false }` to prevent this. + +:::tip +Simple atoms don't need this. Zedux automatically hydrates atoms after their initial evaluation. + +Use `injectHydration` for more fine-grained control, e.g. when persisting and restoring only part of an atom's state or when different instances of an atom need unique hydration logic. +::: + +Hydration data is returned again on subsequent evaluations, though you typically ignore it. + +## Examples + +An example that uses simple React state to simulate persisting and restoring an atom's state. + +```tsx live ecosystemId=injectHydration/example resultVar=App version=2 +const persistedCounterAtom = atom('persistedCounter', () => { + // Try to get hydration data for this atom + const hydration = injectHydration() + + // Initialize with hydrated value or default to 0 + const signal = injectSignal(hydration ?? 0) + + const increment = () => signal.set(val => val + 1) + + return api(signal).setExports({ increment }) +}) + +function App() { + const [persistedState, setPersistedState] = useState({}) + const instance = useAtomInstance(persistedCounterAtom) + const [count, { increment }] = useAtomState(instance) + + const ecosystem = useEcosystem() + + // Simulate dehydrating and rehydrating persisted data + const simulatePersist = () => { + setPersistedState({ [instance.id]: count }) + } + + const simulateHydration = () => { + ecosystem.hydrate({ + [instance.id]: persistedState[instance.id], + }) + } + + return ( +
+
Current Count: {count}
+
Persisted State:
+
{JSON.stringify(persistedState, null, 2)}
+ + + +
+ ) +} +``` + +See [the persistence walkthrough](../../../advanced/persistence.mdx) for more complete examples using `localStorage` for persistence. + +An example using atom configuration to persist and restore only part of an atom's state: + +```tsx live ecosystemId=injectHydration/transform resultVar=App version=2 +const hydratedUserAtom = atom( + 'hydratedUser', + () => { + // Get hydration with transformation applied + const hydration = injectHydration<{ name: string; theme: string }>() + + const signal = injectSignal( + hydration ?? { name: 'Default', theme: 'light' } + ) + + return signal + }, + { + // only persist the user's name + dehydrate: state => state.name, + hydrate: (name: unknown) => ({ + name: typeof name === 'string' ? name : 'Unknown', + theme: 'light', + }), + } +) + +function App() { + const ecosystem = useEcosystem() + + // calling `ecosystem.hydrate` with `{ retroactive: false }` is safe during render: + ecosystem.hydrate( + { + [hydratedUserAtom.getNodeId(ecosystem)]: 'Eagerly-hydrated name', + }, + { retroactive: false } + ) + + const user = useAtomValue(hydratedUserAtom) + + const hydrateUser = () => { + ecosystem.hydrate({ + [hydratedUserAtom.getNodeId(ecosystem)]: 'Lazily-hydrated name', + }) + } + + return ( +
+
User: {user.name}
+
Theme: {user.theme}
+ +
+ ) +} +``` + +## Signature + + + {tab1(`injectHydration = (config?) => hydrationData`)} + {tab2(`declare const injectHydration: (config?: { + intercept?: boolean + transform?: boolean +}) => T`)} + + + + + Optional. An object with the following, optional properties: + + ```ts + { intercept?, transform? } + ``` + + - **`intercept`** - A boolean. Default: `true`. Whether to prevent Zedux from auto-hydrating this atom after evaluation. + - **`transform`** - A boolean. Default: `true`. Whether to apply the atom's configured `hydrate` function to transform the raw hydration data. + + + + The hydration data for this atom, or `undefined` if no hydration was provided. + + If the atom has a `hydrate` function configured and `transform` is not `false`, the transformed value is returned. + + + + +## See Also + +- [`ecosystem.hydrate`](../classes/Ecosystem.mdx#hydrate) +- [`ecosystem.dehydrate`](../classes/Ecosystem.mdx#dehydrate) +- [The `AtomConfig` type](/not-done?path=../types/AtomConfig.mdx) +- [The persistence walkthrough](../../../advanced/persistence.mdx) +- [The SSR walkthrough](../../../advanced/ssr.mdx) diff --git a/docs/sidebars.js b/docs/sidebars.js index 86eeb4ab..f13b87a5 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -215,6 +215,7 @@ module.exports = { 'v2/api/injectors/injectCallback', 'v2/api/injectors/injectEcosystem', 'v2/api/injectors/injectEffect', + 'v2/api/injectors/injectHydration', 'v2/api/injectors/injectSignal', ], }, diff --git a/packages/atoms/src/injectors/injectHydration.ts b/packages/atoms/src/injectors/injectHydration.ts index f57ca417..8da0bab0 100644 --- a/packages/atoms/src/injectors/injectHydration.ts +++ b/packages/atoms/src/injectors/injectHydration.ts @@ -25,7 +25,8 @@ import { injectSelf } from './injectSelf' * * When `injectHydration` is called in any atom, it prevents Zedux from trying * to auto-hydrate the atom after initial evaluation. Pass `{ intercept: false - * }` to prevent this. + * }` to prevent this - e.g. if you just want to read the hydration, not use it + * to initialize state yourself. */ export const injectHydration = (config?: { intercept?: boolean From 9b64c917de12049b89727f1b236ee2e64f402bae Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Mon, 4 Aug 2025 12:04:36 -0400 Subject: [PATCH 31/49] docs: document `injectMappedSignal` for v2 --- docs/docs/v2/api/classes/Ecosystem.mdx | 2 +- docs/docs/v2/api/classes/MappedSignal.mdx | 8 +- .../v2/api/injectors/injectMappedSignal.mdx | 117 ++++++++++++++++++ docs/docs/v2/api/injectors/injectSignal.mdx | 12 +- 4 files changed, 128 insertions(+), 11 deletions(-) create mode 100644 docs/docs/v2/api/injectors/injectMappedSignal.mdx diff --git a/docs/docs/v2/api/classes/Ecosystem.mdx b/docs/docs/v2/api/classes/Ecosystem.mdx index 029c2240..d552b6b2 100644 --- a/docs/docs/v2/api/classes/Ecosystem.mdx +++ b/docs/docs/v2/api/classes/Ecosystem.mdx @@ -857,7 +857,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context - `@memo()-` An atom selector created via an `injectMemo` call with no deps. Wraps the containing atom's template key inside the `()`. - `@ref()-` A function or class instance reference tracked when the ecosystem is configured with `complexParams: true`. Wraps the function or class name inside the `()`. - `@selector()-` An atom selector. Wraps the selector's name inside the `()`. - - `@signal()-` A signal created via [`ecosystem.signal`](#signal) or [`injectSignal`](../injectors/injectSignal) or a mapped signal created via [`injectMappedSignal`](/not-done?path=../injectors/injectMappedSignal). Wraps the containing atom's template key inside the `()` (empty if created via `ecosystem.signal`). + - `@signal()-` A signal created via [`ecosystem.signal`](#signal) or [`injectSignal`](../injectors/injectSignal.mdx) or a mapped signal created via [`injectMappedSignal`](../injectors/injectMappedSignal.mdx). Wraps the containing atom's template key inside the `()` (empty if created via `ecosystem.signal`).
diff --git a/docs/docs/v2/api/classes/MappedSignal.mdx b/docs/docs/v2/api/classes/MappedSignal.mdx index 9c7dd9bf..f63a1e25 100644 --- a/docs/docs/v2/api/classes/MappedSignal.mdx +++ b/docs/docs/v2/api/classes/MappedSignal.mdx @@ -5,11 +5,11 @@ title: MappedSignal import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' -The object returned by [`injectMappedSignal`](/not-done?path=../injectors/injectMappedSignal). Mapped signals _are_ signals, meaning this class extends [the `Signal` class](./Signal.mdx) (which in turn extends [the `ZeduxNode` class](./ZeduxNode.mdx)). All signal operations work on mapped signals. +The object returned by [`injectMappedSignal`](../injectors/injectMappedSignal.mdx). Mapped signals _are_ signals, meaning this class extends [the `Signal` class](./Signal.mdx) (which in turn extends [the `ZeduxNode` class](./ZeduxNode.mdx)). All signal operations work on mapped signals. Mapped signals are composed of any number of "inner signals" plus any number of other state fields. They're aware of which inner signal controls each piece of state and forward all state changes to the relevant inner signal(s). They also forward events to and from inner signals. -The state of a mapped signal is always an object. Mapped signals don't support nested objects. To nest signals, create multiple mapped signals. See [`injectMappedSignal`](/not-done?path=../injectors/injectMappedSignal) for an example. +The state of a mapped signal is always an object. Mapped signals don't support nested objects. To nest signals, create multiple mapped signals. See [`injectMappedSignal`](../injectors/injectMappedSignal.mdx) for an example. The keys of a mapped signal's object never change. They're defined when `injectMappedSignal` is first called in an atom and stay for the lifetime of the mapped signal. @@ -21,7 +21,7 @@ Mapped signals are primarily for composing multiple signals inside an atom into ## Creation -You never instantiate this class yourself. Mapped signals can currently only be created via [the `injectMappedSignal` injector](/not-done?path=../injectors/injectMappedSignal): +You never instantiate this class yourself. Mapped signals can currently only be created via [the `injectMappedSignal` injector](../injectors/injectMappedSignal.mdx): ```ts const exampleAtom = atom('example', () => { @@ -53,7 +53,7 @@ For TypeScript users, mapped signals have the following generics inherited from ## Events -Mapped signals inherit all [custom events](./Signal.mdx#custom-events) from their inner signals. They can also define their own. See [`injectMappedSignal`'s `events`](/not-done?path=../injectors/injectMappedSignal#events) config option. +Mapped signals inherit all [custom events](./Signal.mdx#custom-events) from their inner signals. They can also define their own. See [`injectMappedSignal`'s `events`](../injectors/injectMappedSignal.mdx#events) config option. Mapped signals also inherit the following built-in events from the [`Signal` class](./Signal.mdx#events): diff --git a/docs/docs/v2/api/injectors/injectMappedSignal.mdx b/docs/docs/v2/api/injectors/injectMappedSignal.mdx new file mode 100644 index 00000000..cd0d0e8d --- /dev/null +++ b/docs/docs/v2/api/injectors/injectMappedSignal.mdx @@ -0,0 +1,117 @@ +--- +id: injectMappedSignal +title: injectMappedSignal +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +An [injector](../glossary.mdx#injector) that creates and returns a stable [`MappedSignal`](../classes/MappedSignal.mdx) instance. The reference will never change for the lifetime of the injecting atom. + +Registers a [dynamic graph dependency](../glossary.mdx#dynamic-graph-dependency) on the injected signal. By default, every state change in the injected mapped signal (or, by extension, any of its inner signals) will trigger a reevaluation of the current atom. + +Since mapped signals don't accept nested objects, use this injector multiple times to create deeply-nested signals. + +## Examples + +Deeply nesting mixed signal types: + +```ts +const exampleAtom = atom('example', () => { + const innerSignal = injectSignal(0) + + const innerAtomInstance = injectAtomInstance(someOtherAtom) + + const mappedSignal = injectMappedSignal({ + inner: innerSignal, + // atoms are signals. Using another atom instance is fine: + innerAtom: innerAtomInstance, + }) + + // mapped signals are signals, so they can be nested in other mapped signals: + const outermostSignal = injectMappedSignal({ nested: mappedSignal }) + + outermostSignal.get() // { nested: { inner: 0, innerAtom: } } + + return outermostSignal +}) +``` + +Miscellaneous examples: + +```ts +const signalA = injectSignal('a') +const signalB = injectSignal('b', { events: { exampleEvent: As } }) + +const signal = injectMappedSignal({ a: signalA, b: signalB }) +const withEvents = injectMappedSignal( + { a: signalA }, + { + events: { + exampleEvent: As, + }, + } +) +const nonReactive = injectMappedSignal({ b: signalB }, { reactive: false }) +const mixedValues = injectMappedSignal({ a: signalA, b: 'anything here' }) +``` + +## Signature + + + {tab1(`injectMappedSignal = (signalMap, config?) => MappedSignal`)} + {tab2(`declare const injectMappedSignal: < + M extends SignalMap, + EventMap extends Record = {} + >( + map: M, + config?: InjectSignalConfig + ) => MappedSignal<{ + Events: Prettify & EventMap> + State: { [K in keyof M]: M[K] extends Signal ? StateOf : M[K] } + }>`)} + + + + + Required. A JS object containing signals or any other value. + + The keys of this object will become keys in the state of the returned signal. + + When a value is a signal, that signal will control the state of that key in the mapped signal's state. These don't have to be stable references; on reevaluation, each passed signal reference will be swapped in for its previous reference if they don't match. + + When a value is not a signal, it will be returned as-is in the mapped signal's initial state. On reevaluation, this behaves exactly like [`injectSignal`](./injectSignal.mdx) - the value will be ignored. + + + + Optional. An object containing the following properties: + + + + Optional. A boolean. Default: `true`. + + If `false`, this injector will not register a dynamic graph dependency on the injected mapped signal. Note that if any inner signals are also injected via [`injectSignal`](./injectSignal.mdx), those may still register their own dependencies. + + + + Optional. An object mapping custom event names to `As`. See [the `As` util](/not-done?path=../utils/As.mdx). + + This object is unused. It's just for type inference. As such, passing the second `EventMap` generic to `injectMappedSignal` has the same effect as passing this config option. + + Note that mapped signals always inherit events from their inner signals. You don't need to also specify those here. The mapped signal's [`Events` generic](../classes/MappedSignal.mdx#gevents) will merge any events on this object with those of inner signals. Mapped signals can't remove inner signal events from their own type. + + + + + + + A stable [`MappedSignal`](../classes/MappedSignal.mdx) instance. + + + + +## See Also + +- [The `MappedSignal` class](../classes/MappedSignal.mdx) +- [The `Signal` class](../classes/Signal.mdx) +- [The `injectSignal` injector](./injectSignal.mdx) +- [The `As` util](/not-done?path=../utils/As.mdx) diff --git a/docs/docs/v2/api/injectors/injectSignal.mdx b/docs/docs/v2/api/injectors/injectSignal.mdx index 17135dec..8769605f 100644 --- a/docs/docs/v2/api/injectors/injectSignal.mdx +++ b/docs/docs/v2/api/injectors/injectSignal.mdx @@ -5,9 +5,9 @@ title: injectSignal import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' -An [injector](../glossary#injector) that creates and returns a stable [`Signal`](../classes/Signal) instance. The reference will never change for the lifetime of the injecting atom. +An [injector](../glossary.mdx#injector) that creates and returns a stable [`Signal`](../classes/Signal.mdx) instance. The reference will never change for the lifetime of the injecting atom. -Registers a [dynamic graph dependency](../glossary#dynamic-graph-dependency) on the injected signal. By default, every state change in the injected signal will trigger a reevaluation of the current atom. +Registers a [dynamic graph dependency](../glossary.mdx#dynamic-graph-dependency) on the injected signal. By default, every state change in the injected signal will trigger a reevaluation of the current atom. Unless you register other dependencies, the injecting atom will typically be the only observer of the injected signal. That means the signal will be destroyed when the injecting atom is destroyed. @@ -69,17 +69,17 @@ const passingGenerics = injectSignal([]) A boolean. Default: `true`. - Pass `false` to prevent the injecting atom from registering a [dynamic graph dependency](../glossary#dynamic-graph-dependency) on the injected signal. The atom will still register a [static graph dependency](../glossary#static-graph-dependency) on the injected signal. + Pass `false` to prevent the injecting atom from registering a [dynamic graph dependency](../glossary.mdx#dynamic-graph-dependency) on the injected signal. The atom will still register a [static graph dependency](../glossary.mdx#static-graph-dependency) on the injected signal. - A stable [`Signal`](../classes/Signal) instance. + A stable [`Signal`](../classes/Signal.mdx) instance. ## See Also -- [The `Signal` class](../classes/Signal) -- [`injectMappedSignal`](/not-done?path=./injectMappedSignal) +- [The `Signal` class](../classes/Signal.mdx) +- [The `injectMappedSignal` injector](./injectMappedSignal.mdx) From e261b1492e1270b748e14784cadaebb8df045c63 Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Tue, 5 Aug 2025 13:54:00 -0400 Subject: [PATCH 32/49] docs: document `injectMemo` for v2 --- docs/docs/v2/api/glossary.mdx | 22 ++-- docs/docs/v2/api/injectors/injectCallback.mdx | 6 +- docs/docs/v2/api/injectors/injectEffect.mdx | 4 +- docs/docs/v2/api/injectors/injectMemo.mdx | 121 ++++++++++++++++++ docs/docs/v2/api/types/SelectorTemplate.mdx | 2 +- docs/sidebars.js | 1 + 6 files changed, 139 insertions(+), 17 deletions(-) create mode 100644 docs/docs/v2/api/injectors/injectMemo.mdx diff --git a/docs/docs/v2/api/glossary.mdx b/docs/docs/v2/api/glossary.mdx index 914d2a8b..126d78ff 100644 --- a/docs/docs/v2/api/glossary.mdx +++ b/docs/docs/v2/api/glossary.mdx @@ -25,13 +25,13 @@ These can be created manually with [manual graphing](../../walkthrough/destructi Zedux builds an internal graph to manage atom dependencies and propagate updates in an optimal way. There are many types of nodes in this graph: -- [Atom instances](./classes/AtomInstance) +- [Atom instances](./classes/AtomInstance.mdx) - [Selector instances](./classes/SelectorInstance.mdx) -- [Signals](./classes/Signal) -- [`injectMemo`](/not-done?path=./injectors/injectMemo) calls with no deps array (enabling automatic dependency tracking) +- [Signals](./classes/Signal.mdx) +- [`injectMemo`](./injectors/injectMemo.mdx) calls with no deps array (enabling automatic dependency tracking) - "External" nodes created for React components and event listeners. -Every node extends [the `ZeduxNode` class](./classes/ZeduxNode). +Every node extends [the `ZeduxNode` class](./classes/ZeduxNode.mdx). ### Injector @@ -39,21 +39,21 @@ Injectors are the "hooks" of Atoms. Zedux exports several injectors. There are 3 basic types of injectors: -- React-hook equivalents, like [`injectEffect`](./injectors/injectEffect.mdx), [`injectMemo`](/not-done?path=./injectors/injectMemo), and [`injectRef`](/not-done?path=./injectors/injectRef). +- React-hook equivalents, like [`injectEffect`](./injectors/injectEffect.mdx), [`injectMemo`](./injectors/injectMemo.mdx), and [`injectRef`](/not-done?path=./injectors/injectRef). - Dependency injectors, like [`injectAtomValue`](./injectors/injectAtomValue.mdx) and [`injectAtomInstance`](./injectors/injectAtomInstance.mdx). - Utility or dev X injectors, such as [`injectEcosystem`](./injectors/injectEcosystem.mdx) and [`injectWhy`](/not-done?path=./injectors/injectWhy). Injectors should only be used at the top level of [atom state factories](#state-factory). Don't use them in loops or conditional statements. -Injectors can be used any number of times throughout an atom state factory. For certain one-off operations like setting an atom instance's exports or setting a suspense promise, use an [AtomApi](./classes/AtomApi). +Injectors can be used any number of times throughout an atom state factory. For certain one-off operations like setting an atom instance's exports or setting a suspense promise, use an [AtomApi](./classes/AtomApi.mdx). Like hooks, you can create custom injectors that compose other injectors. The convention is to start all injectors with the word "inject", similar to the word "use" with React hooks. ### Reactive Context -A function execution context in which Zedux automatically tracks dependencies. Atom state factories, selectors, and auto-tracked [`injectMemo`](/not-done?path=./injectors/injectMemo#auto-tracking) calls create reactive contexts. +A function execution context in which Zedux automatically tracks dependencies. Atom state factories, selectors, and auto-tracked [`injectMemo`](./injectors/injectMemo.mdx#auto-tracking) calls create reactive contexts. -In a reactive context, any [`node.get()`](./classes/ZeduxNode#get) or [`ecosystem.get()`](./classes/Ecosystem#get) calls will register [dynamic graph dependencies](#dynamic-graph-dependency) on the retrieved node, and any [`ecosystem.getNode()`](./classes/Ecosystem#getnode) calls will register [static graph dependencies](#static-graph-dependency). This automatic dependency tracking is a staple in reactive libraries like Zedux. +In a reactive context, any [`node.get()`](./classes/ZeduxNode.mdx#get) or [`ecosystem.get()`](./classes/Ecosystem.mdx#get) calls will register [dynamic graph dependencies](#dynamic-graph-dependency) on the retrieved node, and any [`ecosystem.getNode()`](./classes/Ecosystem.mdx#getnode) calls will register [static graph dependencies](#static-graph-dependency). This automatic dependency tracking is a staple in reactive libraries like Zedux. ### Scope @@ -82,13 +82,13 @@ const val = ecosystem.withScope(exampleScope, () => ecosystem.get(scopedAtom)) ### Scoped Atom -An atom that contains one or more [`inject`](/not-done?path=./injectors/inject) calls. Such atoms must be called with ["scope"](#scope) - e.g. by providing contextual values via Provider components in React or by calling [`ecosystem.withScope`](/not-done?path=./classes/Ecoysstem#withscope) +An atom that contains one or more [`inject`](/not-done?path=./injectors/inject) calls. Such atoms must be called with ["scope"](#scope) - e.g. by providing contextual values via Provider components in React or by calling [`ecosystem.withScope`](./classes/Ecosystem.mdx#withscope) ### State Factory -A function passed to [`atom()`](./factories/atom) (or other atom factory functions like [`ion()`](./factories/ion)). This function is called to produce the initial value of the atom instance. It also runs every time an atom instance reevaluates. +A function passed to [`atom()`](./factories/atom.mdx) (or other atom factory functions like [`ion()`](./factories/ion.mdx)). This function is called to produce the initial value of the atom instance. It also runs every time an atom instance reevaluates. -You can add as many parameters as you want to this function. TS users can specify the types of these params to guarantee that all consumers pass the correct values when accessing the atom via Zedux's [injectors](#injector), React hooks, or [ecosystem](./classes/Ecosystem) methods. Each unique set of params will create a new atom instance. See [`ZeduxNode#params`](./classes/ZeduxNode.mdx#params) for details on how params are stringified. +You can add as many parameters as you want to this function. TS users can specify the types of these params to guarantee that all consumers pass the correct values when accessing the atom via Zedux's [injectors](#injector), React hooks, or [ecosystem](./classes/Ecosystem.mdx) methods. Each unique set of params will create a new atom instance. See [`ZeduxNode#params`](./classes/ZeduxNode.mdx#params) for details on how params are stringified. State factories are similar to render functions in React. Except of course they return state instead of UI. diff --git a/docs/docs/v2/api/injectors/injectCallback.mdx b/docs/docs/v2/api/injectors/injectCallback.mdx index 7328d683..e5883ec3 100644 --- a/docs/docs/v2/api/injectors/injectCallback.mdx +++ b/docs/docs/v2/api/injectors/injectCallback.mdx @@ -9,7 +9,7 @@ import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' import { injectCallback } from '@zedux/react' ``` -An [injector](../glossary.mdx#injector) for creating memoized callback functions inside atoms. This is the injector equivalent of React's [`useCallback` hook](https://react.dev/reference/react/useCallback). +An [injector](../glossary.mdx#injector) for creating memoized, batched, and scoped callback functions inside atoms. This is the injector equivalent of React's [`useCallback` hook](https://react.dev/reference/react/useCallback). Like `useCallback`, `injectCallback` returns a memoized version of the callback that only changes when any passed dependencies change. @@ -45,7 +45,7 @@ const callback = injectCallback( ) ``` -:::warning These are not the joys you're looking for +:::warning You may not need `injectCallback` While this looks useful, `injectCallback` is rarely needed. [Exported functions](../classes/AtomApi.mdx#exports) are already automatically batched and scoped. You typically don't need `injectCallback` unless a function is both used locally, e.g. in an [`injectEffect`](./injectEffect.mdx), and exported. @@ -140,7 +140,7 @@ function App() { ## See Also -- [The `injectMemo` injector](/not-done?path=./injectMemo.mdx) +- [The `injectMemo` injector](./injectMemo.mdx) - [`ecosystem.batch`](../classes/Ecosystem.mdx#batch) - [`ecosystem.withScope`](../classes/Ecosystem.mdx#withscope) - Export wrapping is functionally similar and usually preferable to `injectCallback`. See these methods on `AtomApi`: diff --git a/docs/docs/v2/api/injectors/injectEffect.mdx b/docs/docs/v2/api/injectors/injectEffect.mdx index b6bf125f..fbcae14c 100644 --- a/docs/docs/v2/api/injectors/injectEffect.mdx +++ b/docs/docs/v2/api/injectors/injectEffect.mdx @@ -111,7 +111,7 @@ Many of the principles in React's famous ["You Might Not Need an Effect" doc](ht ### Transformations -You don't typically need effects to transform data. Prefer selectors or ions that naturally derive data. Or just do the transformation inline during atom evaluation if it's cheap. Also consider [`injectMemo`](/not-done?path=./injectMemo.mdx) if it's less cheap. +You don't typically need effects to transform data. Prefer selectors or ions that naturally derive data. Or just do the transformation inline during atom evaluation if it's cheap. Also consider [`injectMemo`](./injectMemo.mdx) if it's less cheap. Bad example: @@ -157,7 +157,7 @@ Here are some better approaches: }) ``` -- Use [`injectMemo`](/not-done?path=./injectMemo.mdx) if needed: +- Use [`injectMemo`](./injectMemo.mdx) if needed: ```tsx const computedAtom = atom('computed', () => { diff --git a/docs/docs/v2/api/injectors/injectMemo.mdx b/docs/docs/v2/api/injectors/injectMemo.mdx new file mode 100644 index 00000000..25cfa16d --- /dev/null +++ b/docs/docs/v2/api/injectors/injectMemo.mdx @@ -0,0 +1,121 @@ +--- +id: injectMemo +title: injectMemo +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +```ts +import { injectMemo } from '@zedux/react' +``` + +An [injector](../glossary.mdx#injector) that memoizes a value and returns it. This is the injector equivalent of React's [`useMemo()`](https://react.dev/reference/react/useMemo) hook. Like `useMemo()`, `injectMemo()` will return the same value on subsequent evaluations of the atom. + +When any of the passed dependencies change on a subsequent evaluation, the value will be recomputed and will replace the previously cached value. + +## Example + +```tsx live ecosystemId=injectMemo/example resultVar=Seconds version=2 +const multiplierAtom = atom('multiplier', () => 2) + +const secondsAtom = atom('seconds', () => { + const ecosystem = injectEcosystem() + const signal = injectSignal(10) + + // empty deps = only run once: + const initialValue = injectMemo(() => signal.get(), []) + + // no deps = auto-track the `signal` and `multiplierAtom` deps + const multipliedValue = injectMemo( + () => signal.get() * ecosystem.get(multiplierAtom) + ) + + injectEffect(() => { + const intervalId = setInterval(() => signal.set(val => val + 1), 1000) + + return () => clearInterval(intervalId) + }, []) + + const mappedSignal = injectMappedSignal({ + initialValue, + liveValue: signal, + multipliedValue, + }) + + // update this every time in case `multipliedValue` changed: + mappedSignal.mutate({ initialValue, multipliedValue }) + + return mappedSignal +}) + +function Seconds() { + const { initialValue, liveValue, multipliedValue } = useAtomValue(secondsAtom) + const multiplierInstance = useAtomInstance(multiplierAtom) + + return ( + <> +
Seconds: {liveValue}
+
Initial Value: {initialValue}
+
Multiplied Value: {multipliedValue}
+ + + ) +} +``` + +Miscellaneous: + +```ts +const constantVal = injectMemo(() => getExpensiveVal(), []) + +const changesWhenDepsChange = injectMemo(getExpensiveVal, [depA, depB]) + +// automatically reevaluate this factory when `mySignal` or `myFilter` change +// and make the injecting atom reevaluate. +const autoTrackedDeps = injectMemo(() => + mySignal.get().sort().filter(myFilter.get()) +) +``` + +## Signature + + + {tab1(`injectMemo = (valueFactory, deps?)`)} + {tab2(`declare const injectMemo: ( + valueFactory: () => Value, + deps?: InjectorDeps +) => Value`)} + + + + + Required. A function that returns the value to memoize. + + This function is called on initial evaluation and again every time any dependencies change on subsequent evaluations. + + If no deps are passed, this function's execution will create a [reactive context](../glossary.mdx#reactive-context) that automatically tracks [`node.get()`](../classes/ZeduxNode.mdx#get), [`ecosystem.get()`](../classes/Ecosystem.mdx#get), and [`ecosystem.getNode()`](../classes/Signal.mdx#getmany) calls. When any [dynamic dependencies](../glossary.mdx#dynamic-graph-dependency) change, the `valueFactory` will reevaluate and, if the value changes, trigger a reevaluation of the injecting atom. Internally, `injectMemo` is just creating a [selector instance](../classes/SelectorInstance.mdx) and drawing a dynamic graph edge between it and the injecting atom. + + + + Optional (though you'll always want to pass it). An array containing absolutely anything. + + If any items in this array change on a subsequent evaluation, the previously memoized value will be discarded and the `valueFactory` will be called again to produce a new value. + + Pass an empty array to prevent the value from ever changing, as long as this atom instance is alive. + + If not passed, the `valueFactory` will become a [reactive context](../glossary.mdx#reactive-context) (see [above](#valuefactory)) + + + + The memoized value returned from the `valueFactory`. + + + + +## See Also + +- [React's `useMemo()` reference](https://react.dev/reference/react/useMemo) +- [`injectCallback()`](./injectCallback.mdx) +- [`injectPromise()`](/not-done?path=./injectPromise.mdx) diff --git a/docs/docs/v2/api/types/SelectorTemplate.mdx b/docs/docs/v2/api/types/SelectorTemplate.mdx index 5069eb9b..81c29210 100644 --- a/docs/docs/v2/api/types/SelectorTemplate.mdx +++ b/docs/docs/v2/api/types/SelectorTemplate.mdx @@ -129,7 +129,7 @@ This is also useful for debugging, since Zedux uses the selector's function name Selectors are designed for simple, pure calculations. They always reevaluate when any of their dependencies update. Sometimes you need more control over when/how often a selector evaluates. -It's common to refactor a selector to an ion for better control over memoization details via [`injectMemo`](/not-done?path=../injectors/injectMemo.mdx) or even combining [`injectEffect`](/not-done?path=../injectors/injectEffect.mdx) with async tools like RxJS to throttle or buffer updates. +It's common to refactor a selector to an ion for better control over memoization details via [`injectMemo`](../injectors/injectMemo.mdx) or even combining [`injectEffect`](../injectors/injectEffect.mdx) with async tools like RxJS to throttle or buffer updates. ```tsx // Before (using an atom selector): diff --git a/docs/sidebars.js b/docs/sidebars.js index f13b87a5..05afa25f 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -216,6 +216,7 @@ module.exports = { 'v2/api/injectors/injectEcosystem', 'v2/api/injectors/injectEffect', 'v2/api/injectors/injectHydration', + 'v2/api/injectors/injectMemo', 'v2/api/injectors/injectSignal', ], }, From 71368db0f06fb67547d3853413d60f5aa6de610c Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Fri, 7 Nov 2025 12:43:04 -0500 Subject: [PATCH 33/49] docs: document `injectPromise` for v2 --- docs/docs/v2/api/injectors/injectPromise.mdx | 240 +++++++++++++++++++ docs/package.json | 10 +- docs/pnpm-lock.yaml | 90 +++---- docs/sidebars.js | 1 + 4 files changed, 291 insertions(+), 50 deletions(-) create mode 100644 docs/docs/v2/api/injectors/injectPromise.mdx diff --git a/docs/docs/v2/api/injectors/injectPromise.mdx b/docs/docs/v2/api/injectors/injectPromise.mdx new file mode 100644 index 00000000..3be63d13 --- /dev/null +++ b/docs/docs/v2/api/injectors/injectPromise.mdx @@ -0,0 +1,240 @@ +--- +id: injectPromise +title: injectPromise +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +```ts +import { injectPromise } from '@zedux/react' +``` + +An [injector](../glossary.mdx#injector) for controlling the creation of a promise and handling its state. It tracks the promise's status (loading, success, error), resolved value, and rejection error. + +`injectPromise` returns an [atom API](../classes/AtomApi.mdx) object with three attached properties: `.signal`, `.dataSignal`, and `.promise`. You can return this API directly from the atom's state factory or pass it to [`api()`](../factories/api.mdx) to clone/extend it. You can also use the `.signal`, `.dataSignal`, and `.promise` properties directly. + +The `.signal`'s state is patterned after [React Query's query state](https://tanstack.com/query/latest/docs/framework/react/guides/queries). + +## Example + +No suspense: + +```tsx live ecosystemId=injectPromise/example resultVar=App version=2 +const userAtom = atom('user', (userId: number) => { + const fetchUser = async () => { + await new Promise(resolve => setTimeout(resolve, 1000)) // Simulate delay + if (userId === 999) throw new Error('User not found') + return { + id: userId, + name: `User ${userId}`, + email: `user${userId}@example.com`, + } + } + + const promiseState = injectPromise(fetchUser) + + return promiseState +}) + +function UserProfile({ userId }: { userId: number }) { + const userState = useAtomValue(userAtom, [userId], { suspend: false }) + + if (userState.isLoading) return
Loading user...
+ if (userState.isError) return
Error: {userState.error?.message}
+ if (userState.isSuccess && userState.data) { + return ( +
+

{userState.data.name}

+

{userState.data.email}

+
+ ) + } + + return null +} + +function App() { + const [userId, setUserId] = useState(1) + + return ( +
+ + + +
+ ) +} +``` + +With suspense: + +```tsx live ecosystemId=injectPromise/example-suspense resultVar=App version=2 +const userAtom = atom('user', (userId: number) => { + const fetchUser = async () => { + await new Promise(resolve => setTimeout(resolve, 1000)) // Simulate delay + if (userId === 999) throw new Error('User not found') + return { + id: userId, + name: `User ${userId}`, + email: `user${userId}@example.com`, + } + } + + const promiseState = injectPromise(fetchUser) + + return promiseState +}) + +function UserProfile({ userId }: { userId: number }) { + const userState = useAtomValue(userAtom, [userId]) + + return ( +
+

{userState.data.name}

+

{userState.data.email}

+
+ ) +} + +class ErrorBoundary extends Component { + state = { error: null } + + componentDidCatch(error: Error) { + this.setState({ error }) + } + + render() { + if (this.state.error) { + return
Error: {this.state.error.message}
+ } + + return this.props.children + } +} + +function App() { + const [userId, setUserId] = useState(1) + + return ( +
+ + Loading user...
}> + + + + + + + ) +} +``` + +## Signature + + + {tab1(`injectPromise = (promiseFactory, deps?, config?) => promiseState`)} + {tab2(`declare const injectPromise: { + = None>( + promiseFactory: (params: { + controller?: AbortController + prevData?: NoInfer + }) => Promise, + deps: InjectorDeps, + config: Omit, 'initialData'> & { + initialData: Data + } & InjectSignalConfig + ): InjectPromiseAtomApi< + { + Exports: Record + Promise: Promise + Signal: MappedSignal<{ + Events: EventMap + State: Omit, 'data'> & { + data: Data + } + }> + State: Omit, 'data'> & { + data: Data + } + }, + EventMap, + Data + > + = None>( + promiseFactory: (params: { + controller?: AbortController + prevData?: NoInfer + }) => Promise, + deps: InjectorDeps, + config?: InjectPromiseConfig & InjectSignalConfig + ): InjectPromiseAtomApi< + { + Exports: Record + Promise: Promise + Signal: MappedSignal<{ + Events: EventMap + ResolvedState: Omit, 'data'> & { + data: Data + } + State: PromiseState + }> + State: PromiseState + }, + EventMap, + Data + > + }`)} + + + + + Required. A function that returns a promise. This function will be called immediately on initial evaluation of the atom, and again when [dependencies](#deps) change. + + + + Optional, but you'll always want to pass it. An array of dependencies. When any of these dependencies change, the promise factory will be re-invoked and the promise state cycle will reset. + + This is just like the dependency arrays passed to React's `useMemo()`, `useEffect()`, etc. + + + + Optional. Configuration object with: + + - **`initialData`** - Initial data to use before the promise resolves + - **`runOnInvalidate`** - Whether to re-run the promise factory when the atom is invalidated + + + + An [atom API](../classes/AtomApi.mdx) object with three attached properties: `.signal`, `.dataSignal`, and `.promise`. This atom API can be returned directly from the atom's state factory or passed to [`api()`](../factories/api.mdx) to clone/extend it. You can also use the `.signal`, `.dataSignal`, and `.promise` properties directly to compose them with other promises/signals or listen to events from them. + + ```ts + const { signal, dataSignal, promise } = injectPromise(...) + ``` + + The extra promise-related state is tracked in the `.signal` property. + + - **`data`** - The resolved value (if success). This state is controlled by the `dataSignal`. See below for more information. + - **`error`** - The rejection reason (if error) + - **`isLoading`** - Whether the promise is pending + - **`isSuccess`** - Whether the promise resolved successfully + - **`isError`** - Whether the promise was rejected + - **`status`** - Current status: 'loading', 'success', or 'error' + + The `.dataSignal` property is unique to the Atom APIs returned from this injector. You can use it directly to access the promise's resolved value, ignoring the extra promise-related state that `injectPromise` tracks. For example: + + ```tsx + const { dataSignal } = injectPromise(...) + + // make the `dataSignal` control this atom's state: + return api(dataSignal).setExports({ getUser: () => dataSignal.get() }) + ``` + + The returned atom API's `.signal` is composed of this `dataSignal`, using it to control its `.data` property. Updates to the `dataSignal` will propagate to the `.signal`'s `.data` property and vice versa. See [MappedSignal](../classes/MappedSignal.mdx) for more information on how this works. + + + + +## See Also + +- [The Query Atoms walkthrough](../../../walkthrough/query-atoms) +- [The `api` factory](../factories/api) diff --git a/docs/package.json b/docs/package.json index b47c09d7..0001689f 100644 --- a/docs/package.json +++ b/docs/package.json @@ -20,11 +20,11 @@ "@zedux.v1/immer": "npm:@zedux/immer@1.3.4", "@zedux.v1/machines": "npm:@zedux/machines@1.3.4", "@zedux.v1/react": "npm:@zedux/react@1.3.4", - "@zedux.v2/atoms": "npm:@zedux/atoms@2.0.0-rc.8", - "@zedux.v2/core": "npm:@zedux/core@2.0.0-rc.8", - "@zedux.v2/immer": "npm:@zedux/immer@2.0.0-rc.8", - "@zedux.v2/machines": "npm:@zedux/machines@2.0.0-rc.8", - "@zedux.v2/react": "npm:@zedux/react@2.0.0-rc.8", + "@zedux.v2/atoms": "npm:@zedux/atoms@2.0.0-rc.10", + "@zedux.v2/core": "npm:@zedux/core@2.0.0-rc.10", + "@zedux.v2/immer": "npm:@zedux/immer@2.0.0-rc.10", + "@zedux.v2/machines": "npm:@zedux/machines@2.0.0-rc.10", + "@zedux.v2/react": "npm:@zedux/react@2.0.0-rc.10", "framer-motion": "^7.10.3", "prism-react-renderer": "^1.3.5", "prismjs": "^1.29.0", diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml index 0ba5b884..f3bd6af3 100644 --- a/docs/pnpm-lock.yaml +++ b/docs/pnpm-lock.yaml @@ -31,28 +31,28 @@ importers: version: '@zedux/core@1.3.4' '@zedux.v1/immer': specifier: npm:@zedux/immer@1.3.4 - version: '@zedux/immer@1.3.4(@zedux/atoms@1.3.4)(immer@9.0.21)' + version: '@zedux/immer@1.3.4(@zedux/atoms@2.0.0-rc.10)(immer@9.0.21)' '@zedux.v1/machines': specifier: npm:@zedux/machines@1.3.4 - version: '@zedux/machines@1.3.4(@zedux/atoms@1.3.4)' + version: '@zedux/machines@1.3.4(@zedux/atoms@2.0.0-rc.10)' '@zedux.v1/react': specifier: npm:@zedux/react@1.3.4 version: '@zedux/react@1.3.4(react@19.0.0)' '@zedux.v2/atoms': - specifier: npm:@zedux/atoms@2.0.0-rc.8 - version: '@zedux/atoms@2.0.0-rc.8' + specifier: npm:@zedux/atoms@2.0.0-rc.10 + version: '@zedux/atoms@2.0.0-rc.10' '@zedux.v2/core': - specifier: npm:@zedux/core@2.0.0-rc.8 - version: '@zedux/core@2.0.0-rc.8' + specifier: npm:@zedux/core@2.0.0-rc.10 + version: '@zedux/core@2.0.0-rc.10' '@zedux.v2/immer': - specifier: npm:@zedux/immer@2.0.0-rc.8 - version: '@zedux/immer@2.0.0-rc.8(@zedux/atoms@2.0.0-rc.8)(immer@9.0.21)' + specifier: npm:@zedux/immer@2.0.0-rc.10 + version: '@zedux/immer@2.0.0-rc.10(@zedux/atoms@2.0.0-rc.10)(immer@9.0.21)' '@zedux.v2/machines': - specifier: npm:@zedux/machines@2.0.0-rc.8 - version: '@zedux/machines@2.0.0-rc.8(@zedux/atoms@2.0.0-rc.8)(@zedux/core@2.0.0-rc.8)' + specifier: npm:@zedux/machines@2.0.0-rc.10 + version: '@zedux/machines@2.0.0-rc.10(@zedux/atoms@2.0.0-rc.10)(@zedux/core@2.0.0-rc.10)' '@zedux.v2/react': - specifier: npm:@zedux/react@2.0.0-rc.8 - version: '@zedux/react@2.0.0-rc.8(react@19.0.0)' + specifier: npm:@zedux/react@2.0.0-rc.10 + version: '@zedux/react@2.0.0-rc.10(react@19.0.0)' framer-motion: specifier: ^7.10.3 version: 7.10.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -1662,14 +1662,14 @@ packages: '@zedux/atoms@1.3.4': resolution: {integrity: sha512-RdszSW4Va3JtGQFOWJgnwswj+sukNmDiHQJuaHfGR00yNJUeMQxxwLT9285M8tAEmev7l9JnRGtWyODLuFv1kA==} - '@zedux/atoms@2.0.0-rc.8': - resolution: {integrity: sha512-vVTdozqrozF3SffTDJFJqifHZm3KmBxw8mIw8qO1UqiVnkMNe1nE5UaDJF0U2VIcJGiF/qJEK73GGxb8XE2a7A==} + '@zedux/atoms@2.0.0-rc.10': + resolution: {integrity: sha512-oyTJeN83BINAHVKln+oZD0AJbp2NUJduCoJb17rZNMxp6s2UM+vK2i6CYnxgLtwzdTkgTcO5tPDYgpF+aWscVw==} '@zedux/core@1.3.4': resolution: {integrity: sha512-J1wcgEbZUMV0/2V0ivfazb4at53kunHKOqreJWiq7m6lH9gcufCBY8dN/86OXxu7M8H9pzRkOJgihLbRA9iIfg==} - '@zedux/core@2.0.0-rc.8': - resolution: {integrity: sha512-zMp/f9g7relQ/qHsyiQauR51i8ZA1LLNrSuoz2UeQcYdbkZOEZZOwBf0EK8244wbPNmEIeJi1grF+SHbIAAF4w==} + '@zedux/core@2.0.0-rc.10': + resolution: {integrity: sha512-9i/XfV6GZnPk5FXUacJaxPjkJxhWPqIusJrZDPeICsb9hRSGE7gD4+jPl7FBzo2WgcEXX7BqcyaU0sTxatEjFA==} '@zedux/immer@1.3.4': resolution: {integrity: sha512-nIWQdnCt6r0rRGjocuoPlX9p8RGCJtAaxxTgOSyRoqmD4Ttnz/rs3A8roprRGfLTfrRIOh73pxU/A9pA1Xc06g==} @@ -1677,10 +1677,10 @@ packages: '@zedux/atoms': 1.3.4 immer: '>=9.0.19' - '@zedux/immer@2.0.0-rc.8': - resolution: {integrity: sha512-MeSsNYRtT+BW7RK53kieyULXTFADEssgM4iYK7XfsWnG4tcGrJk3cIMi3DrdGvC/A0yq2vzT9u9I/lUA6GH2IQ==} + '@zedux/immer@2.0.0-rc.10': + resolution: {integrity: sha512-ZyAIwD0YLdiWIbTiI1jYuKzh5OfZJCcBydf2McjOQeb8tkdusFhZH0avJ1Alf5gepCV8zb4p78FO5TFk5GdJWw==} peerDependencies: - '@zedux/atoms': 2.0.0-rc.8 + '@zedux/atoms': 2.0.0-rc.10 immer: '>=9.0.19' '@zedux/machines@1.3.4': @@ -1688,24 +1688,24 @@ packages: peerDependencies: '@zedux/atoms': 1.3.4 - '@zedux/machines@2.0.0-rc.8': - resolution: {integrity: sha512-AnS4ZHEbKgU9pTr4c1Ymv8VJq/ITrzLeRL+FdYwAoj8j7BpcKf7ip+1h/nSfpU0+thYaPMzumvEwqiYqRtt7QQ==} + '@zedux/machines@2.0.0-rc.10': + resolution: {integrity: sha512-f4J3ajxDBcm3g4X8cFPIIa+5Zq1CGoXy4q3Bcwpl+xTZphIPfH1tN/WGPBHCXvV5rPTYA4XVJrNVY+ElUg2uOA==} peerDependencies: - '@zedux/atoms': 2.0.0-rc.8 - '@zedux/core': 2.0.0-rc.8 + '@zedux/atoms': 2.0.0-rc.10 + '@zedux/core': 2.0.0-rc.10 '@zedux/react@1.3.4': resolution: {integrity: sha512-0uKO5Rv7o3a0+DdzLbSmW3fLME+D4xzznNxDWBr/5zIMgM9+ETNZuZRI2B3iA+Y8EtRgijPWqPZTKou8hC2HLA==} peerDependencies: react: '>=18.0.0' - '@zedux/react@2.0.0-rc.8': - resolution: {integrity: sha512-AUTh/yRdGBeILBEoIhb8v9fzp9CgWk7Wokcqq3oRAbnNalnC4LIobrRAHFul+6vPpaNGoKj1FxeN9SFC29+4tg==} + '@zedux/react@2.0.0-rc.10': + resolution: {integrity: sha512-Grrame+MEUQqGRxtlHK8AyIoguGiiYL6uAIA4rCp9yQfFka1inb9yEpSvIcp1YWaSZ3xt1ryqy7rRGy6ZNJ8vA==} peerDependencies: react: '>=18.0.0' - '@zedux/stores@2.0.0-rc.8': - resolution: {integrity: sha512-oOcem/s1zS9V50YVcXtMj19KOQjbWv/G0wFP24e8B3D+bvDpY92G5xQmXmWGFDCXqR7iNAjdwauUthEI+rWuyw==} + '@zedux/stores@2.0.0-rc.10': + resolution: {integrity: sha512-FvFcr/9qlipw552zcPmdnDnjTD7LEgsSkSSEUyKzlww/WuN4vh8el/J3hOkjcMwJFvXevWTEkMcfWRV/k+hakA==} accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} @@ -7880,47 +7880,47 @@ snapshots: dependencies: '@zedux/core': 1.3.4 - '@zedux/atoms@2.0.0-rc.8': {} + '@zedux/atoms@2.0.0-rc.10': {} '@zedux/core@1.3.4': {} - '@zedux/core@2.0.0-rc.8': {} + '@zedux/core@2.0.0-rc.10': {} - '@zedux/immer@1.3.4(@zedux/atoms@1.3.4)(immer@9.0.21)': + '@zedux/immer@1.3.4(@zedux/atoms@2.0.0-rc.10)(immer@9.0.21)': dependencies: - '@zedux/atoms': 1.3.4 + '@zedux/atoms': 2.0.0-rc.10 immer: 9.0.21 - '@zedux/immer@2.0.0-rc.8(@zedux/atoms@2.0.0-rc.8)(immer@9.0.21)': + '@zedux/immer@2.0.0-rc.10(@zedux/atoms@2.0.0-rc.10)(immer@9.0.21)': dependencies: - '@zedux/atoms': 2.0.0-rc.8 - '@zedux/stores': 2.0.0-rc.8 + '@zedux/atoms': 2.0.0-rc.10 + '@zedux/stores': 2.0.0-rc.10 immer: 9.0.21 - '@zedux/machines@1.3.4(@zedux/atoms@1.3.4)': + '@zedux/machines@1.3.4(@zedux/atoms@2.0.0-rc.10)': dependencies: - '@zedux/atoms': 1.3.4 + '@zedux/atoms': 2.0.0-rc.10 - '@zedux/machines@2.0.0-rc.8(@zedux/atoms@2.0.0-rc.8)(@zedux/core@2.0.0-rc.8)': + '@zedux/machines@2.0.0-rc.10(@zedux/atoms@2.0.0-rc.10)(@zedux/core@2.0.0-rc.10)': dependencies: - '@zedux/atoms': 2.0.0-rc.8 - '@zedux/core': 2.0.0-rc.8 - '@zedux/stores': 2.0.0-rc.8 + '@zedux/atoms': 2.0.0-rc.10 + '@zedux/core': 2.0.0-rc.10 + '@zedux/stores': 2.0.0-rc.10 '@zedux/react@1.3.4(react@19.0.0)': dependencies: '@zedux/atoms': 1.3.4 react: 19.0.0 - '@zedux/react@2.0.0-rc.8(react@19.0.0)': + '@zedux/react@2.0.0-rc.10(react@19.0.0)': dependencies: - '@zedux/atoms': 2.0.0-rc.8 + '@zedux/atoms': 2.0.0-rc.10 react: 19.0.0 - '@zedux/stores@2.0.0-rc.8': + '@zedux/stores@2.0.0-rc.10': dependencies: - '@zedux/atoms': 2.0.0-rc.8 - '@zedux/core': 2.0.0-rc.8 + '@zedux/atoms': 2.0.0-rc.10 + '@zedux/core': 2.0.0-rc.10 accepts@1.3.8: dependencies: diff --git a/docs/sidebars.js b/docs/sidebars.js index 05afa25f..744e4cc3 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -217,6 +217,7 @@ module.exports = { 'v2/api/injectors/injectEffect', 'v2/api/injectors/injectHydration', 'v2/api/injectors/injectMemo', + 'v2/api/injectors/injectPromise', 'v2/api/injectors/injectSignal', ], }, From 6dbc2af0c7fd75f5f9fc8ab9768312cc064dc2ad Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Fri, 7 Nov 2025 12:48:32 -0500 Subject: [PATCH 34/49] update /not-done injectPromise links --- docs/docs/v2/api/classes/AtomInstance.mdx | 4 ++-- docs/docs/v2/api/classes/Ecosystem.mdx | 2 +- docs/docs/v2/api/injectors/injectPromise.mdx | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/docs/v2/api/classes/AtomInstance.mdx b/docs/docs/v2/api/classes/AtomInstance.mdx index f5342a0f..5134901c 100644 --- a/docs/docs/v2/api/classes/AtomInstance.mdx +++ b/docs/docs/v2/api/classes/AtomInstance.mdx @@ -178,8 +178,8 @@ greetingNode.send('greetedPerson', true) // TS Error! Expected a string. Zedux sends this event whenever `atomInstance.invalidate()` is called. Some Zedux APIs hook into this event like - [`injectPromise`](/not-done?path=../injectors/injectPromise)'s - `runOnInvalidate` option. + [`injectPromise`](../injectors/injectPromise.mdx)'s `runOnInvalidate` + option. Zedux sends this event when an atom instance's [`.promise`](#promise) diff --git a/docs/docs/v2/api/classes/Ecosystem.mdx b/docs/docs/v2/api/classes/Ecosystem.mdx index d552b6b2..a0271fbe 100644 --- a/docs/docs/v2/api/classes/Ecosystem.mdx +++ b/docs/docs/v2/api/classes/Ecosystem.mdx @@ -199,7 +199,7 @@ function App() { - Sent whenever `atomInstance.invalidate()` is called. Some Zedux APIs hook into this event like [`injectPromise`](/not-done?path=../injectors/injectPromise)'s `runOnInvalidate` option. + Sent whenever `atomInstance.invalidate()` is called. Some Zedux APIs hook into this event like [`injectPromise`](../injectors/injectPromise.mdx)'s `runOnInvalidate` option. Event shape: diff --git a/docs/docs/v2/api/injectors/injectPromise.mdx b/docs/docs/v2/api/injectors/injectPromise.mdx index 3be63d13..d749c082 100644 --- a/docs/docs/v2/api/injectors/injectPromise.mdx +++ b/docs/docs/v2/api/injectors/injectPromise.mdx @@ -236,5 +236,5 @@ function App() { ## See Also -- [The Query Atoms walkthrough](../../../walkthrough/query-atoms) -- [The `api` factory](../factories/api) +- [The Query Atoms walkthrough](../../../walkthrough/query-atoms.mdx) +- [The `api` factory](../factories/api.mdx) From 5b5d326c18f41ddba26793b3d492bce084ce0910 Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Sat, 15 Nov 2025 12:44:20 -0500 Subject: [PATCH 35/49] fix pnpm-lock zedux v1 peer deps --- docs/pnpm-lock.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml index f3bd6af3..17f13a35 100644 --- a/docs/pnpm-lock.yaml +++ b/docs/pnpm-lock.yaml @@ -31,10 +31,10 @@ importers: version: '@zedux/core@1.3.4' '@zedux.v1/immer': specifier: npm:@zedux/immer@1.3.4 - version: '@zedux/immer@1.3.4(@zedux/atoms@2.0.0-rc.10)(immer@9.0.21)' + version: '@zedux/immer@1.3.4(@zedux/atoms@1.3.4)(immer@9.0.21)' '@zedux.v1/machines': specifier: npm:@zedux/machines@1.3.4 - version: '@zedux/machines@1.3.4(@zedux/atoms@2.0.0-rc.10)' + version: '@zedux/machines@1.3.4(@zedux/atoms@1.3.4)' '@zedux.v1/react': specifier: npm:@zedux/react@1.3.4 version: '@zedux/react@1.3.4(react@19.0.0)' @@ -7886,9 +7886,9 @@ snapshots: '@zedux/core@2.0.0-rc.10': {} - '@zedux/immer@1.3.4(@zedux/atoms@2.0.0-rc.10)(immer@9.0.21)': + '@zedux/immer@1.3.4(@zedux/atoms@1.3.4)(immer@9.0.21)': dependencies: - '@zedux/atoms': 2.0.0-rc.10 + '@zedux/atoms': 1.3.4 immer: 9.0.21 '@zedux/immer@2.0.0-rc.10(@zedux/atoms@2.0.0-rc.10)(immer@9.0.21)': @@ -7897,9 +7897,9 @@ snapshots: '@zedux/stores': 2.0.0-rc.10 immer: 9.0.21 - '@zedux/machines@1.3.4(@zedux/atoms@2.0.0-rc.10)': + '@zedux/machines@1.3.4(@zedux/atoms@1.3.4)': dependencies: - '@zedux/atoms': 2.0.0-rc.10 + '@zedux/atoms': 1.3.4 '@zedux/machines@2.0.0-rc.10(@zedux/atoms@2.0.0-rc.10)(@zedux/core@2.0.0-rc.10)': dependencies: From 4ad17cc6706779d5faf00625674042b404e0e3e3 Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Sat, 15 Nov 2025 12:54:34 -0500 Subject: [PATCH 36/49] docs: document `injectRef` for v2 --- docs/docs/v2/api/injectors/injectRef.mdx | 80 ++++++++++++++++++++++++ docs/sidebars.js | 1 + 2 files changed, 81 insertions(+) create mode 100644 docs/docs/v2/api/injectors/injectRef.mdx diff --git a/docs/docs/v2/api/injectors/injectRef.mdx b/docs/docs/v2/api/injectors/injectRef.mdx new file mode 100644 index 00000000..120c7220 --- /dev/null +++ b/docs/docs/v2/api/injectors/injectRef.mdx @@ -0,0 +1,80 @@ +--- +id: injectRef +title: injectRef +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +```ts +import { injectRef } from '@zedux/react' +``` + +An [injector](../glossary.mdx#injector) for creating mutable reference objects inside atoms. Similar to React's `useRef`, it returns a mutable ref object whose `.current` property can be used to store any value that persists across atom evaluations. + +Changing a ref's `.current` property does not trigger re-evaluations of the atom or its dependents. + +## Example + +```tsx live ecosystemId=injectRef/example resultVar=App version=2 +const timerAtom = atom('timer', () => { + const signal = injectSignal(0) + const intervalRef = injectRef(null) + + const start = injectCallback(() => { + if (intervalRef.current) return // Already running + + intervalRef.current = setInterval(() => { + signal.set(count => count + 1) + }, 100) + }, []) + + const stop = injectCallback(() => { + if (intervalRef.current) { + clearInterval(intervalRef.current) + intervalRef.current = null + } + }, []) + + const reset = injectCallback(() => { + stop() + signal.set(0) + }, [stop]) + + return api(signal).setExports({ start, stop, reset }) +}) + +function App() { + const [time, { start, stop, reset }] = useAtomState(timerAtom) + + return ( +
+
Time: {(time / 10).toFixed(1)}s
+ + + +
+ ) +} +``` + + + {tab1(`injectRef = (initialValue?) => refObject`)} + {tab2(`declare const injectRef: ( + initialValue?: T +) => MutableRefObject`)} + + + + + Optional. The initial value to set on the ref's `.current` property. + + + A mutable ref object with a `.current` property that can be read or modified + at any time. + + + +## See Also + +- [`injectCallback`](./injectCallback.mdx) +- [`injectMemo`](./injectMemo.mdx) diff --git a/docs/sidebars.js b/docs/sidebars.js index 744e4cc3..c989c647 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -219,6 +219,7 @@ module.exports = { 'v2/api/injectors/injectMemo', 'v2/api/injectors/injectPromise', 'v2/api/injectors/injectSignal', + 'v2/api/injectors/injectRef', ], }, { From 0dd27665077a8f6c236c23aa736b84cf74f9dab4 Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Sat, 15 Nov 2025 13:00:27 -0500 Subject: [PATCH 37/49] update /not-done paths for `injectRef` --- docs/docs/v2/api/glossary.mdx | 2 +- docs/docs/v2/api/injectors/injectEffect.mdx | 4 ++-- docs/docs/v2/api/injectors/injectMemo.mdx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/docs/v2/api/glossary.mdx b/docs/docs/v2/api/glossary.mdx index 126d78ff..68be4698 100644 --- a/docs/docs/v2/api/glossary.mdx +++ b/docs/docs/v2/api/glossary.mdx @@ -39,7 +39,7 @@ Injectors are the "hooks" of Atoms. Zedux exports several injectors. There are 3 basic types of injectors: -- React-hook equivalents, like [`injectEffect`](./injectors/injectEffect.mdx), [`injectMemo`](./injectors/injectMemo.mdx), and [`injectRef`](/not-done?path=./injectors/injectRef). +- React-hook equivalents, like [`injectEffect`](./injectors/injectEffect.mdx), [`injectMemo`](./injectors/injectMemo.mdx), and [`injectRef`](./injectors/injectRef.mdx). - Dependency injectors, like [`injectAtomValue`](./injectors/injectAtomValue.mdx) and [`injectAtomInstance`](./injectors/injectAtomInstance.mdx). - Utility or dev X injectors, such as [`injectEcosystem`](./injectors/injectEcosystem.mdx) and [`injectWhy`](/not-done?path=./injectors/injectWhy). diff --git a/docs/docs/v2/api/injectors/injectEffect.mdx b/docs/docs/v2/api/injectors/injectEffect.mdx index fbcae14c..904755ca 100644 --- a/docs/docs/v2/api/injectors/injectEffect.mdx +++ b/docs/docs/v2/api/injectors/injectEffect.mdx @@ -226,7 +226,7 @@ const usersAtom = atom('users', () => { ### Async State -Additionally, in Zedux, consider using [`injectPromise`](/not-done?path=./injectPromise.mdx) or [query atoms](../../../walkthrough/query-atoms.mdx) instead of `injectEffect` to run async operations and track their state. +Additionally, in Zedux, consider using [`injectPromise`](./injectPromise.mdx) or [query atoms](../../../walkthrough/query-atoms.mdx) instead of `injectEffect` to run async operations and track their state. ## Signature @@ -269,4 +269,4 @@ Additionally, in Zedux, consider using [`injectPromise`](/not-done?path=./inject ## See Also - [The side effects walkthrough](../../../walkthrough/side-effects.mdx) -- [The `injectPromise` injector](/not-done?path=./injectPromise.mdx) +- [The `injectPromise` injector](./injectPromise.mdx) diff --git a/docs/docs/v2/api/injectors/injectMemo.mdx b/docs/docs/v2/api/injectors/injectMemo.mdx index 25cfa16d..59c66165 100644 --- a/docs/docs/v2/api/injectors/injectMemo.mdx +++ b/docs/docs/v2/api/injectors/injectMemo.mdx @@ -118,4 +118,4 @@ const autoTrackedDeps = injectMemo(() => - [React's `useMemo()` reference](https://react.dev/reference/react/useMemo) - [`injectCallback()`](./injectCallback.mdx) -- [`injectPromise()`](/not-done?path=./injectPromise.mdx) +- [`injectPromise()`](./injectPromise.mdx) From 9bdb2c37ce008576dc8fe863159d722c07f8144c Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Sat, 15 Nov 2025 13:17:45 -0500 Subject: [PATCH 38/49] docs: document `injectSelf` for v2 --- docs/docs/v2/api/classes/AtomInstance.mdx | 8 +-- docs/docs/v2/api/classes/Ecosystem.mdx | 2 +- docs/docs/v2/api/glossary.mdx | 2 +- docs/docs/v2/api/injectors/injectSelf.mdx | 77 +++++++++++++++++++++++ docs/sidebars.js | 1 + 5 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 docs/docs/v2/api/injectors/injectSelf.mdx diff --git a/docs/docs/v2/api/classes/AtomInstance.mdx b/docs/docs/v2/api/classes/AtomInstance.mdx index 5134901c..0855db2d 100644 --- a/docs/docs/v2/api/classes/AtomInstance.mdx +++ b/docs/docs/v2/api/classes/AtomInstance.mdx @@ -285,8 +285,8 @@ Atom instances also inherit the following properties from [`ZeduxNode`](./ZeduxN See [`ZeduxNode#status`](./ZeduxNode.mdx#status). See [`ZeduxNode#template`](./ZeduxNode.mdx#template). Will always be a - reference to the [atom template](./AtomTemplate) this instance was created - from. + reference to the [atom template](./AtomTemplate.mdx) this instance was + created from. @@ -307,12 +307,12 @@ Atom instances also inherit the following properties from [`ZeduxNode`](./ZeduxN } ``` - To access this method inside the atom's state factory, use [`injectSelf()`](/not-done?path=../injectors/injectSelf). + To access this method inside the atom's state factory, use [`injectSelf()`](../injectors/injectSelf.mdx).
-Atom instances also inherit the following methods from [`Signal`](./Signal#methods): +Atom instances also inherit the following methods from [`Signal`](./Signal.mdx#methods): diff --git a/docs/docs/v2/api/classes/Ecosystem.mdx b/docs/docs/v2/api/classes/Ecosystem.mdx index a0271fbe..b57ca173 100644 --- a/docs/docs/v2/api/classes/Ecosystem.mdx +++ b/docs/docs/v2/api/classes/Ecosystem.mdx @@ -315,7 +315,7 @@ function App() { - `type` - The string `"runStart"`. :::important - When this event fires on initial evaluation, the `source` node will not be fully defined yet. For atoms, this means its [`.exports`](./AtomInstance#exports), [`.promise`](./AtomInstance#promise), and some internal properties will be undefined. See [`injectSelf`](/not-done?path=../injectors/injectSelf) for more details on uninitialized atoms. + When this event fires on initial evaluation, the `source` node will not be fully defined yet. For atoms, this means its [`.exports`](./AtomInstance#exports), [`.promise`](./AtomInstance#promise), and some internal properties will be undefined. See [`injectSelf`](../injectors/injectSelf.mdx) for more details on uninitialized atoms. ::: diff --git a/docs/docs/v2/api/glossary.mdx b/docs/docs/v2/api/glossary.mdx index 68be4698..171afee0 100644 --- a/docs/docs/v2/api/glossary.mdx +++ b/docs/docs/v2/api/glossary.mdx @@ -106,4 +106,4 @@ An [injector](#injector) whose use isn't restricted like normal injectors. An un You usually won't need to worry about this distinction. Just use them like normal injectors and you'll be fine. -Examples of unrestricted injectors include [`injectEcosystem()`](./injectors/injectEcosystem.mdx), [`injectSelf()`](/not-done?path=./injectors/injectSelf), and [`injectWhy()`](/not-done?path=./injectors/injectWhy). +Examples of unrestricted injectors include [`injectEcosystem()`](./injectors/injectEcosystem.mdx), [`injectSelf()`](./injectors/injectSelf.mdx), and [`injectWhy()`](/not-done?path=./injectors/injectWhy.mdx). diff --git a/docs/docs/v2/api/injectors/injectSelf.mdx b/docs/docs/v2/api/injectors/injectSelf.mdx new file mode 100644 index 00000000..16f301b4 --- /dev/null +++ b/docs/docs/v2/api/injectors/injectSelf.mdx @@ -0,0 +1,77 @@ +--- +id: injectSelf +title: injectSelf +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +```ts +import { injectSelf } from '@zedux/react' +``` + +An [injector](../glossary.mdx#injector) that returns the atom instance that's currently evaluating. + +This gives you access to the atom instance object from within the atom itself, allowing you to read properties like the atom's [`.id`](../classes/AtomInstance.mdx#id) or even update, invalidate, or destroy the atom instance. Make sure you only call these methods in a callback, after the atom has been fully initialized. + +:::warning +The atom instance returned by `injectSelf` is not fully initialized on initial evaluation. It may not have a `.exports` or `.promise` set yet - that's the purpose of the atom evaluation! [Scoped atoms](/not-done?path=../../walkthrough/scoped-atoms.mdx) may not even have their real id determined yet. + +The types reflect this by returning a `PartialAtomInstance`. On subsequent evaluations, or when used later in a closure, you may check if the atom has been fully initialized and cast it to an `AtomInstance` if so. +::: + +## Example + +```tsx live ecosystemId=injectSelf/example resultVar=App version=2 +const recursiveAtom = atom('recursive', () => { + const self = injectSelf() + const signal = injectSignal(0) + + const increment = () => signal.set(val => val + 1) + + const logSelf = () => { + console.log('Atom ID:', self.id) + console.log('Current state:', self.getOnce()) + console.log('Atom template key:', self.template.key) + } + + const destroySelf = () => { + console.log('Force-destroying self...') + self.destroy(true) + } + + return api(signal).setExports({ increment, logSelf, destroySelf }) +}) + +function App() { + const [count, { increment, logSelf, destroySelf }] = + useAtomState(recursiveAtom) + + return ( +
+
Count: {count}
+ + + +
+ ) +} +``` + + + {tab1(`injectSelf = () => atomInstance`)} + {tab2(`declare const injectSelf: () => PartialAtomInstance`)} + + + + + The current [atom instance](../classes/AtomInstance.mdx) reference. Note + that some properties may not be fully initialized during the first + evaluation. + + + +## See Also + +- [The `AtomInstance` class](../classes/AtomInstance.mdx) +- [The `injectAtomInstance` injector](./injectAtomInstance.mdx) +- [The Atom Lifecycle walkthrough](/not-done?path=../../../walkthrough/atom-lifecycle.mdx) diff --git a/docs/sidebars.js b/docs/sidebars.js index c989c647..dc10a653 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -220,6 +220,7 @@ module.exports = { 'v2/api/injectors/injectPromise', 'v2/api/injectors/injectSignal', 'v2/api/injectors/injectRef', + 'v2/api/injectors/injectSelf', ], }, { From efbe0606ac1de8f3d0449861ed039d3aac0fc298 Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Sat, 15 Nov 2025 13:40:52 -0500 Subject: [PATCH 39/49] some tweaks --- docs/docs/v2/api/injectors/injectSelf.mdx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/docs/v2/api/injectors/injectSelf.mdx b/docs/docs/v2/api/injectors/injectSelf.mdx index 16f301b4..f8e5e989 100644 --- a/docs/docs/v2/api/injectors/injectSelf.mdx +++ b/docs/docs/v2/api/injectors/injectSelf.mdx @@ -9,7 +9,7 @@ import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' import { injectSelf } from '@zedux/react' ``` -An [injector](../glossary.mdx#injector) that returns the atom instance that's currently evaluating. +An [unrestricted injector](../glossary.mdx#unrestricted-injector) that returns the atom instance that's currently evaluating. This gives you access to the atom instance object from within the atom itself, allowing you to read properties like the atom's [`.id`](../classes/AtomInstance.mdx#id) or even update, invalidate, or destroy the atom instance. Make sure you only call these methods in a callback, after the atom has been fully initialized. @@ -22,10 +22,14 @@ The types reflect this by returning a `PartialAtomInstance`. On subsequent evalu ## Example ```tsx live ecosystemId=injectSelf/example resultVar=App version=2 -const recursiveAtom = atom('recursive', () => { +const selfAwareAtom = atom('selfAware', () => { const self = injectSelf() const signal = injectSignal(0) + if (self.status === 'Initializing') { + console.log('Initializing selfAware atom') + } + const increment = () => signal.set(val => val + 1) const logSelf = () => { @@ -44,7 +48,7 @@ const recursiveAtom = atom('recursive', () => { function App() { const [count, { increment, logSelf, destroySelf }] = - useAtomState(recursiveAtom) + useAtomState(selfAwareAtom) return (
@@ -57,6 +61,8 @@ function App() { } ``` +## Signature + {tab1(`injectSelf = () => atomInstance`)} {tab2(`declare const injectSelf: () => PartialAtomInstance`)} From ec91a3ac8dbc404840f671b73f9f3198994ae186 Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Sat, 15 Nov 2025 13:43:25 -0500 Subject: [PATCH 40/49] docs: document `injectWhy` for v2 --- docs/docs/v2/api/glossary.mdx | 4 +- docs/docs/v2/api/injectors/injectSignal.mdx | 4 + docs/docs/v2/api/injectors/injectWhy.mdx | 94 +++++++++++++++++++++ docs/sidebars.js | 1 + 4 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 docs/docs/v2/api/injectors/injectWhy.mdx diff --git a/docs/docs/v2/api/glossary.mdx b/docs/docs/v2/api/glossary.mdx index 171afee0..06eb287e 100644 --- a/docs/docs/v2/api/glossary.mdx +++ b/docs/docs/v2/api/glossary.mdx @@ -41,7 +41,7 @@ There are 3 basic types of injectors: - React-hook equivalents, like [`injectEffect`](./injectors/injectEffect.mdx), [`injectMemo`](./injectors/injectMemo.mdx), and [`injectRef`](./injectors/injectRef.mdx). - Dependency injectors, like [`injectAtomValue`](./injectors/injectAtomValue.mdx) and [`injectAtomInstance`](./injectors/injectAtomInstance.mdx). -- Utility or dev X injectors, such as [`injectEcosystem`](./injectors/injectEcosystem.mdx) and [`injectWhy`](/not-done?path=./injectors/injectWhy). +- Utility or dev X injectors, such as [`injectEcosystem`](./injectors/injectEcosystem.mdx) and [`injectWhy`](./injectors/injectWhy.mdx). Injectors should only be used at the top level of [atom state factories](#state-factory). Don't use them in loops or conditional statements. @@ -106,4 +106,4 @@ An [injector](#injector) whose use isn't restricted like normal injectors. An un You usually won't need to worry about this distinction. Just use them like normal injectors and you'll be fine. -Examples of unrestricted injectors include [`injectEcosystem()`](./injectors/injectEcosystem.mdx), [`injectSelf()`](./injectors/injectSelf.mdx), and [`injectWhy()`](/not-done?path=./injectors/injectWhy.mdx). +Examples of unrestricted injectors include [`injectEcosystem()`](./injectors/injectEcosystem.mdx), [`injectSelf()`](./injectors/injectSelf.mdx), and [`injectWhy()`](./injectors/injectWhy.mdx). diff --git a/docs/docs/v2/api/injectors/injectSignal.mdx b/docs/docs/v2/api/injectors/injectSignal.mdx index 8769605f..82810403 100644 --- a/docs/docs/v2/api/injectors/injectSignal.mdx +++ b/docs/docs/v2/api/injectors/injectSignal.mdx @@ -5,6 +5,10 @@ title: injectSignal import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' +```ts +import { injectSignal } from '@zedux/react' +``` + An [injector](../glossary.mdx#injector) that creates and returns a stable [`Signal`](../classes/Signal.mdx) instance. The reference will never change for the lifetime of the injecting atom. Registers a [dynamic graph dependency](../glossary.mdx#dynamic-graph-dependency) on the injected signal. By default, every state change in the injected signal will trigger a reevaluation of the current atom. diff --git a/docs/docs/v2/api/injectors/injectWhy.mdx b/docs/docs/v2/api/injectors/injectWhy.mdx new file mode 100644 index 00000000..ea42acb6 --- /dev/null +++ b/docs/docs/v2/api/injectors/injectWhy.mdx @@ -0,0 +1,94 @@ +--- +id: injectWhy +title: injectWhy +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +An [unrestricted injector](../glossary.mdx#unrestricted-injector) that returns an array of [reasons](/not-done?path=../types/EvaluationReason.mdx) that explain why the current atom is evaluating. + +This is essentially an alias for the following: + +```ts +const reasons = injectEcosystem().why() +``` + +As an injector, `injectWhy` can only be used in atoms. To see why a selector is evaluating, use [`ecosystem.why()`](../classes/Ecosystem.mdx#why). + +## Example + +```tsx live ecosystemId=injectWhy/example resultVar=App version=2 +const sourceAtom = atom('source', () => 1) + +const exampleAtom = atom('example', () => { + const signal = injectSignal(1) + const sourceInstance = injectAtomInstance(sourceAtom) + const reasons = injectWhy() + const reasonsRef = injectRef(reasons) + + const composedSignal = injectMappedSignal({ + ownState: signal, + sourceState: sourceInstance, + }) + + reasonsRef.current = reasons + + return api(composedSignal).setExports({ reasonsRef, signal }) +}) + +function App() { + const [state, { reasonsRef, signal }] = useAtomState(exampleAtom) + const [source, setSource] = useAtomState(sourceAtom) + + return ( +
+

+ Own State: {state.ownState}. Source State: {state.sourceState}. +

+ + +

Operations:

+
    + {reasonsRef.current.map(reason => ( +
  • {reason.operation}
  • + ))} +
+

New state for each source: +

+        {reasonsRef.current
+          .map(reason => JSON.stringify(reason.newState))
+          .join('\n')}
+      
+
+ ) +} +``` + +## Signature + + + {tab1(`injectWhy = () => evaluationReasons`)} + {tab2(`declare const injectWhy: () => EvaluationReason[]`)} + + + + + An array of [EvaluationReasons](/not-done?path=../types/EvaluationReason.mdx) that explain why the current atom is evaluating. + + Returns an empty array on the initial evaluation (and only on the initial evaluation). This can be used to determine if this is the first evaluation. But it's recommended to use [`injectSelf`](./injectSelf.mdx) instead: + + ```ts + // both of these mean it's the first evaluation. Prefer `injectSelf`: + injectSelf().status === 'Initializing' + injectWhy().length === 0 + ``` + + + + +## See Also + +- [`ecosystem.why()`](../classes/Ecosystem.mdx#why) +- [The `EvaluationReason` type](../types/EvaluationReason.mdx) diff --git a/docs/sidebars.js b/docs/sidebars.js index dc10a653..0af36d74 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -221,6 +221,7 @@ module.exports = { 'v2/api/injectors/injectSignal', 'v2/api/injectors/injectRef', 'v2/api/injectors/injectSelf', + 'v2/api/injectors/injectWhy', ], }, { From 767885f72ab25a3704f6e3326a43bee806d04196 Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Sat, 15 Nov 2025 14:18:46 -0500 Subject: [PATCH 41/49] docs: document the `AtomConfig` type for v2 --- docs/docs/v2/api/classes/AtomTemplate.mdx | 8 +- docs/docs/v2/api/factories/atom.mdx | 25 ++-- docs/docs/v2/api/factories/ion.mdx | 2 +- .../docs/v2/api/injectors/injectHydration.mdx | 4 +- docs/docs/v2/api/types/AtomConfig.mdx | 131 ++++++++++++++++++ docs/sidebars.js | 2 +- 6 files changed, 152 insertions(+), 20 deletions(-) create mode 100644 docs/docs/v2/api/types/AtomConfig.mdx diff --git a/docs/docs/v2/api/classes/AtomTemplate.mdx b/docs/docs/v2/api/classes/AtomTemplate.mdx index e96604f2..11d6e812 100644 --- a/docs/docs/v2/api/classes/AtomTemplate.mdx +++ b/docs/docs/v2/api/classes/AtomTemplate.mdx @@ -60,13 +60,13 @@ When creating your own, custom atom types, you'll usually want to extend this cl A function. Can be undefined. - A reference to the [`dehydrate` atom config option](/not-done?path=../types/AtomConfig#dehydrate) passed to [the `atom()` factory](../factories/atom), if any. + A reference to the [`dehydrate` atom config option](../types/AtomConfig.mdx#dehydrate) passed to [the `atom()` factory](../factories/atom), if any. An array of strings. Can be undefined. - A reference to the [`tags` atom config option](/not-done?path=../types/AtomConfig#tags) passed to [the `atom()` factory](../factories/atom), if any. + A reference to the [`tags` atom config option](../types/AtomConfig.mdx#tags) passed to [the `atom()` factory](../factories/atom), if any. If the ecosystem has [tags configured](./Ecosystem#tags), these tags will be checked against the ecosystem's to warn about unsafe atom templates being used in certain environments. @@ -74,7 +74,7 @@ When creating your own, custom atom types, you'll usually want to extend this cl A function. Can be undefined. - A reference to the [`hydrate` atom config option](/not-done?path=../types/AtomConfig#hydrate) passed to [`atom()`](../factories/atom), if any. + A reference to the [`hydrate` atom config option](../types/AtomConfig.mdx#hydrate) passed to [`atom()`](../factories/atom), if any. @@ -88,7 +88,7 @@ When creating your own, custom atom types, you'll usually want to extend this cl A number. Can be undefined. - This is the [`ttl` atom config option](/not-done?path=../types/AtomConfig#ttl) passed to [the `atom()` factory](../factories/atom), if any. + This is the [`ttl` atom config option](../types/AtomConfig.mdx#ttl) passed to [the `atom()` factory](../factories/atom), if any. If not set, instances of this atom will live forever unless configured with [`.setTtl()`](./AtomApi#setttl) on an AtomApi returned by the state factory. diff --git a/docs/docs/v2/api/factories/atom.mdx b/docs/docs/v2/api/factories/atom.mdx index f33c86cb..45c1c6be 100644 --- a/docs/docs/v2/api/factories/atom.mdx +++ b/docs/docs/v2/api/factories/atom.mdx @@ -12,7 +12,7 @@ import { atom } from '@zedux/react' Where it all starts. `atom()` is a factory for creating atom templates. Zedux creates atoms from these templates as you use them in various hooks and injectors. -An atom template is actually an instance of [the AtomTemplate class](../classes/AtomTemplate). +An atom template is actually an instance of [the AtomTemplate class](../classes/AtomTemplate.mdx). ## Example @@ -176,7 +176,7 @@ function App() { Required. A string. - This key must be unique **except** when creating [atom overrides](../../../walkthrough/overrides). + This key must be unique **except** when creating [atom overrides](../../../walkthrough/overrides.mdx). @@ -197,9 +197,9 @@ function App() { - A [state factory](../glossary.mdx#state-factory) function that returns a raw value. That raw value can be anything (including a function). The returned value will be the atom instance's initial state. - - A state factory function that returns a [signal](../classes/Signal). When the atom is instantiated, the new atom instance will become a thin wrapper around the returned signal, forwarding events and state changes between it and the atom's own observers. + - A state factory function that returns a [signal](../classes/Signal.mdx). When the atom is instantiated, the new atom instance will become a thin wrapper around the returned signal, forwarding events and state changes between it and the atom's own observers. - - A state factory function that returns an [AtomApi](../classes/AtomApi) instance. + - A state factory function that returns an [AtomApi](../classes/AtomApi.mdx) instance. The Atom API's value can be any of the following: @@ -207,22 +207,22 @@ function App() { - A signal. The atom instance will become a thin wrapper around the returned signal, forwarding events and state changes between it and the atom's own observers. - - A promise. This will turn the atom into a [query atom](../../../walkthrough/query-atoms). + - A promise. This will turn the atom into a [query atom](../../../walkthrough/query-atoms.mdx). The Atom API's exports will be set as the atom instance's `.exports`. The Atom API's promise will be set as the atom instance's `.promise`. - Any [`ttl`](../classes/AtomApi#ttl) configured in the returned Atom API will control the atom instance's destruction timing. + Any [`ttl`](../classes/AtomApi.mdx#ttl) configured in the returned Atom API will control the atom instance's destruction timing. When a state factory function is passed, any parameters defined on the function will become the [params](../classes/AtomInstance.mdx#params) of the atom. - Optional. An [AtomConfig](/not-done?path=../types/AtomConfig) object. + Optional. An [AtomConfig](../types/AtomConfig.mdx) object. - An [atom template](../classes/AtomTemplate). + An [atom template](../classes/AtomTemplate.mdx). Zedux will manage creating and maintaining instances of the atom template as you use it in various hooks, injectors, and ecosystem methods. @@ -231,7 +231,8 @@ function App() { ## See Also -- [The `AtomTemplate` class](../classes/AtomTemplate) -- [The `AtomApi` class](../classes/AtomApi) -- [The Quick Start](../../../walkthrough/quick-start) -- [The Configuring Atoms walkthrough](../../../walkthrough/configuring-atoms) +- [The `AtomTemplate` class](../classes/AtomTemplate.mdx) +- [The `AtomApi` class](../classes/AtomApi.mdx) +- [The `AtomConfig` type](../types/AtomConfig.mdx) +- [The Quick Start](../../../walkthrough/quick-start.mdx) +- [The Configuring Atoms walkthrough](../../../walkthrough/configuring-atoms.mdx) diff --git a/docs/docs/v2/api/factories/ion.mdx b/docs/docs/v2/api/factories/ion.mdx index a24b68d7..0808b438 100644 --- a/docs/docs/v2/api/factories/ion.mdx +++ b/docs/docs/v2/api/factories/ion.mdx @@ -143,7 +143,7 @@ function App() { - Optional. An [AtomConfig](/not-done?path=../types/AtomConfig) object. + Optional. An [AtomConfig](../types/AtomConfig) object. Note that the `ttl` config option is set to `0` by default for ions. diff --git a/docs/docs/v2/api/injectors/injectHydration.mdx b/docs/docs/v2/api/injectors/injectHydration.mdx index 413bb2f0..e5164604 100644 --- a/docs/docs/v2/api/injectors/injectHydration.mdx +++ b/docs/docs/v2/api/injectors/injectHydration.mdx @@ -11,7 +11,7 @@ import { injectHydration } from '@zedux/react' An [injector](../glossary.mdx#injector) that retrieves data previously passed to [`ecosystem.hydrate`](../classes/Ecosystem.mdx#hydrate) for the currently-evaluating atom. -If the atom has a [`hydrate`](/not-done?path=../types/AtomConfig.mdx#hydrate) function configured, `injectHydration` will call it with the atom's hydration and return the result. Pass `{ transform: false }` to prevent this. +If the atom has a [`hydrate`](../types/AtomConfig.mdx#hydrate) function configured, `injectHydration` will call it with the atom's hydration and return the result. Pass `{ transform: false }` to prevent this. `injectHydration` is unique in that it has a side effect: When called, it prevents Zedux from [automatically hydrating](/not-done?path=../../advanced/persistence#automatic-hydration) the atom after its initial evaluation. Pass `{ intercept: false }` to prevent this. @@ -161,6 +161,6 @@ function App() { - [`ecosystem.hydrate`](../classes/Ecosystem.mdx#hydrate) - [`ecosystem.dehydrate`](../classes/Ecosystem.mdx#dehydrate) -- [The `AtomConfig` type](/not-done?path=../types/AtomConfig.mdx) +- [The `AtomConfig` type](../types/AtomConfig.mdx) - [The persistence walkthrough](../../../advanced/persistence.mdx) - [The SSR walkthrough](../../../advanced/ssr.mdx) diff --git a/docs/docs/v2/api/types/AtomConfig.mdx b/docs/docs/v2/api/types/AtomConfig.mdx new file mode 100644 index 00000000..4c345fe0 --- /dev/null +++ b/docs/docs/v2/api/types/AtomConfig.mdx @@ -0,0 +1,131 @@ +--- +id: AtomConfig +title: AtomConfig +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +```ts +import { AtomConfig } from '@zedux/react' +``` + +The last argument passed to the [`atom`](../factories/atom.mdx) and [`ion`](../factories/ion.mdx) factories. This can be used to configure how the atom is hydrated and dehydrated, when it's destroyed, and to give the atom tags for grouping and filtering. + +These properties are set on the resulting [atom template](../classes/AtomTemplate.mdx) object. + +## Example + +```tsx live ecosystemId=AtomConfig/example resultVar=App version=2 +interface UserData { + id: number + name: string + preferences: { theme: string } +} + +const userAtom = atom( + 'user', + { + id: 1, + name: 'John Doe', + preferences: { theme: 'light' }, + } as UserData, + { + // Custom dehydration - only save essential data + dehydrate: state => ({ + id: state.id, + name: state.name, + }), + + // Custom hydration - restore with defaults + hydrate: (dehydratedState: any) => ({ + ...dehydratedState, + preferences: { theme: 'light' }, + }), + + // Keep alive for 5 seconds after last use + ttl: 5000, + + // Add tags for filtering/debugging + tags: ['user', 'persistent'], + } +) + +function App() { + const [user, setUser] = useAtomState(userAtom) + + return ( +
+
User: {user.name}
+
Theme: {user.preferences.theme}
+ +
+ ) +} +``` + +## Interface + + + {tab1(`interface AtomConfig`)} + {tab2(`interface AtomConfig { + dehydrate?: (state: State) => any + tags?: string[] + hydrate?: (dehydratedState: unknown) => State + ttl?: number + }`)} + + + + + Optional. A function that accepts the current state of the atom and transforms it for serialization/persistence. + + ```ts + dehydrate: (state: State) => any + ``` + + This is usually used to transform unserializable data types like a JS Map or custom class instance into a serializable format. + + This is called by [`ecosystem.dehydrate`](../classes/Ecosystem.mdx#dehydrate) unless `{ transform: false }` is passed. It runs once for each instance of the atom. + + + + Optional. A function that accepts previously-deserialized data that's been passed to [`ecosystem.hydrate`](../classes/Ecosystem.mdx#hydrate) and transforms it back into the atom's state. + + ```ts + hydrate: (dehydratedState: unknown) => State + ``` + + This is called before Zedux [automatically hydrates](/not-done?path=../../advanced/persistence#automatic-hydration) the atom or when [`injectHydration`](../injectors/injectHydration.mdx) is called without `{ transform: false }`. + + + + Optional. Time To Live in milliseconds. Controls how long instances of this atom stay alive after they're no longer used. + + Accepted values: + + - `0` - Destroy immediately, without setting any timeouts, when the last observer of an atom instance is removed. This is the default for [ions](../factories/ion.mdx). It's also how [selectors](../classes/SelectorInstance.mdx#destruction) always behave. + - `undefined` or `null` or `-1` - Never auto-destroy. This is the default for normal [atoms](../factories/atom.mdx). + - A positive `number` - Destroy after specified milliseconds. + + + + Optional. An array of strings for categorizing atoms. Useful for filtering, grouping, debugging, or telling plugins which atoms to operate on. + + [`ecosystem.dehydrate`](../classes/Ecosystem.mdx#dehydrate), [`ecosystem.find`](../classes/Ecosystem.mdx#find), and [`ecosystem.findAll`](../classes/Ecosystem.mdx#findall) all have `includeTags` and `excludeTags` options to filter by these tags. + + + + +## See Also + +- [The `atom` factory](../factories/atom.mdx) +- [The `ion` factory](../factories/ion.mdx) +- [The `AtomTemplate` class](../classes/AtomTemplate.mdx) +- [the `injectHydration` injector](../injectors/injectHydration.mdx) +- [`ecosystem.hydrate`](../classes/Ecosystem.mdx#hydrate) +- [`ecosystem.dehydrate`](../classes/Ecosystem.mdx#dehydrate) +- [The destruction walkthrough](../../../walkthrough/destruction.mdx) +- [The configuring atoms walkthrough](../../../walkthrough/configuring-atoms.mdx) +- [The persistence walkthrough](../../../advanced/persistence.mdx) diff --git a/docs/sidebars.js b/docs/sidebars.js index 0af36d74..e03fe465 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -227,7 +227,7 @@ module.exports = { { type: 'category', label: 'Types', - items: ['v2/api/types/SelectorTemplate'], + items: ['v2/api/types/AtomConfig', 'v2/api/types/SelectorTemplate'], }, 'v2/api/glossary', ], From 6f7583b57834020e9b584e6867f3a9da37d65222 Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Sat, 15 Nov 2025 16:27:08 -0500 Subject: [PATCH 42/49] docs: document the new `inject` util --- docs/docs/v2/api/glossary.mdx | 21 ++- docs/docs/v2/api/utils/inject.mdx | 222 ++++++++++++++++++++++++++ docs/sidebars.js | 5 + docs/src/components/Legend/Legend.tsx | 2 +- packages/react/src/inject.ts | 23 +++ 5 files changed, 271 insertions(+), 2 deletions(-) create mode 100644 docs/docs/v2/api/utils/inject.mdx diff --git a/docs/docs/v2/api/glossary.mdx b/docs/docs/v2/api/glossary.mdx index 06eb287e..bae5e99b 100644 --- a/docs/docs/v2/api/glossary.mdx +++ b/docs/docs/v2/api/glossary.mdx @@ -13,6 +13,17 @@ If the dependent is a React component, it will rerender when the dependency atom If the dependent is another atom instance, it will reevaluate when the dependency atom instance's state changes. +Some APIs that create dynamic dependencies include: + +- [`ecosystem.get`](./classes/Ecosystem.mdx#get) when called during atom/selector evaluation. +- [`node.get`](./classes/ZeduxNode.mdx#get) when called during atom/selector evaluation. +- [`injectAtomValue`](./injectors/injectAtomValue.mdx) +- [`injectAtomState`](./injectors/injectAtomState.mdx) +- [`useAtomValue`](./hooks/useAtomValue.mdx) +- [`useAtomState`](./hooks/useAtomState.mdx) + +Note that [`ecosystem.getOnce`](./classes/Ecosystem.mdx#getonce) and [`node.getOnce`](./classes/ZeduxNode.mdx#getonce) never create any dependencies. + ### Graph Edge The edges between [graph nodes](#graph-node). These edges can have several properties, depending on how the edge was created and how it should behave. @@ -82,7 +93,7 @@ const val = ecosystem.withScope(exampleScope, () => ecosystem.get(scopedAtom)) ### Scoped Atom -An atom that contains one or more [`inject`](/not-done?path=./injectors/inject) calls. Such atoms must be called with ["scope"](#scope) - e.g. by providing contextual values via Provider components in React or by calling [`ecosystem.withScope`](./classes/Ecosystem.mdx#withscope) +An atom that contains one or more [`inject`](./utils/inject.mdx) calls. Such atoms must be called with ["scope"](#scope) - e.g. by providing contextual values via Provider components in React or by calling [`ecosystem.withScope`](./classes/Ecosystem.mdx#withscope). ### State Factory @@ -100,6 +111,14 @@ A "static" dependency is a dependency that does not trigger updates in the depen While they don't trigger updates, static dependencies are still useful for informing Zedux that an atom instance is in use. Zedux won't try to clean up atom instances that still have dependents. +Some APIs that create static dependencies include: + +- [`ecosystem.getNode`](./classes/Ecosystem.mdx#getnode) when called during atom/selector evaluation. +- [`injectAtomInstance`](./injectors/injectAtomInstance.mdx) +- [`useAtomInstance`](./hooks/useAtomInstance.mdx) + +Note that [`ecosystem.getNodeOnce`](./classes/Ecosystem.mdx#getnodeonce) never creates any dependencies. + ### Unrestricted Injector An [injector](#injector) whose use isn't restricted like normal injectors. An unrestricted injector still must be used inside an atom state factory (called synchronously during evaluation). However, unlike normal injectors, unrestricted injectors can be used in control flow statements (`if`, `for`, `while`) or after early returns. diff --git a/docs/docs/v2/api/utils/inject.mdx b/docs/docs/v2/api/utils/inject.mdx new file mode 100644 index 00000000..051d0c98 --- /dev/null +++ b/docs/docs/v2/api/utils/inject.mdx @@ -0,0 +1,222 @@ +--- +id: inject +title: inject +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +```ts +import { inject } from '@zedux/react' +``` + +A utility function that retrieves a contextual value from a provided scope. + +:::note +This is currently an [unrestricted injector](../glossary.mdx#unrestricted-injector). As such, it can only be used in [atom state factories](../glossary.mdx#state-factory). + +It doesn't have to be an injector. We may update this to work with selectors too. In that case, this will become a "utility" function, not an injector. This is similar to React's `use` utility which isn't a hook. +::: + +Any atom that calls `inject` becomes "scoped". Its id will change to reflect the retrieved context and it can only be accessed in a scoped context - either from React hooks or in an [`ecosystem.withScope`](../classes/Ecosystem.mdx#withscope) call. + +Any atom that uses a scoped atom is also scoped, inheriting the scope dependency of the scoped atom. An atom and all transitive dependencies it creates can access the provided scope. + +- When created from React hooks, `inject` can access provided React context values or atom instances. +- When created in an `ecosystem.withScope` callback, `inject` can access the provided scope values. + +## Examples + +Simple example with React context: + +```tsx live ecosystemId=inject/example resultVar=App version=2 +const UserContext = createContext<{ name: string } | undefined>(undefined) + +const greetingAtom = atom('greeting', () => { + const user = inject(UserContext) // access the provided context! + return `Hello, ${user.name}!` +}) + +function UserGreeting() { + const greeting = useAtomValue(greetingAtom) + return
{greeting}
+} + +function App() { + return ( + + + + ) +} +``` + +In this example, `userAtom` becomes a scoped atom because it calls `inject(UserContext)`. It can only be used within a component tree that provides the `UserContext` or in an [`ecosystem.withScope`](../classes/Ecosystem.mdx#withscope) call that manually maps the `UserContext` to a provided value. + +Here's the same example, but using an `ecosystem.withScope` call: + +```tsx live ecosystemId=inject/example2 resultVar=UserGreeting version=2 +const UserContext = createContext<{ name: string } | undefined>(undefined) +const scope = new Map([[UserContext, { name: 'Bob' }]]) + +const userAtom = atom('user', () => { + const user = inject(UserContext) // access the provided context! + return `Hello, ${user.name}!` +}) + +function UserGreeting() { + // use an inline selector to quickly use the ecosystem reactively + const greeting = useAtomValue(({ ecosystem, get }) => + ecosystem.withScope(scope, () => get(userAtom)) + ) + + return
{greeting}
+} +``` + +Simple example with an atom template: + +```tsx live ecosystemId=inject/example3 resultVar=App version=2 +const userAtom = atom('user', () => ({ name: 'Jane' })) + +const greetingAtom = ion('greeting', ({ get }) => { + const userInstance = inject(userAtom) // access the provided atom! + const user = get(userInstance) + return `Hello, ${user.name}!` +}) + +function UserGreeting() { + const greeting = useAtomValue(greetingAtom) + return
{greeting}
+} + +function App() { + const userInstance = useAtomInstance(userAtom) + return ( + + + + ) +} +``` + +A more complex example where a transitive dependency uses `inject`: + +```tsx live ecosystemId=inject/example4 resultVar=App version=2 +const ThemeContext = createContext<{ color: string } | undefined>(undefined) +const userAtom = atom('user', () => ({ name: 'Alice' })) + +// This atom uses inject, making it scoped to ThemeContext and userAtom +const styledGreetingAtom = atom('styledGreeting', () => { + const theme = inject(ThemeContext) + const userInstance = inject(userAtom) + + // create a dynamic dependency on the provided instance + const user = userInstance.get() + + return { + text: `Welcome, ${user.name}!`, + style: { color: theme.color, fontWeight: 'bold' }, + } +}) + +// This atom doesn't call inject directly, but it depends on a scoped atom. +// Therefore, it inherits the scope dependencies and becomes scoped too! +const uppercaseGreetingAtom = ion('uppercaseGreeting', ({ get }) => { + const greeting = get(styledGreetingAtom) + + return { + text: greeting.text.toUpperCase(), + style: greeting.style, + } +}) + +// And this atom depends on uppercaseGreetingAtom, so it's also scoped! +const finalGreetingAtom = ion('finalGreeting', ({ get }) => { + const greeting = get(uppercaseGreetingAtom) + return `${greeting.text} 🎉` +}) + +function Greeting() { + const greeting = useAtomValue(finalGreetingAtom) + const style = useAtomValue(uppercaseGreetingAtom).style + + return
{greeting}
+} + +function App() { + const [color, setColor] = useState('blue') + const userInstance = useAtomInstance(userAtom) + const [user, setUser] = useAtomState(userInstance) + + return ( + + +
+ +
+ setUser({ ...user, name: e.target.value })} + placeholder="Name" + /> + +
+
+
+
+ ) +} +``` + +In this example: + +- `styledGreetingAtom` is scoped because it calls `inject(ThemeContext)` and `inject(UserContext)`. +- `uppercaseGreetingAtom` becomes scoped by depending on `styledGreetingAtom`, even though it doesn't call `inject` directly. +- `finalGreetingAtom` also becomes scoped by depending on `uppercaseGreetingAtom`. +- All three atoms can access the provided context values through the scope chain. All three can thus only be accessed in scoped contexts - either from React hooks or in an [`ecosystem.withScope`](../classes/Ecosystem.mdx#withscope) call. + +:::tip +When used with atoms, remember that `inject` returns the atom instance itself, not the value. You need to create a [dynamic graph dependency](../glossary.mdx#dynamic-graph-dependency) on the instance to make it reactive. In this example, simply calling `userInstance.get()` created the dependency. +::: + +## Signature + + + {tab1(`inject = (context) => providedValue`)} + {tab2(`declare const inject: | AtomTemplateBase>( + context: T + ) => T extends React.Context + ? V + : T extends AtomTemplateBase + ? NodeOf + : never`)} + + + + + Required. Either a React Context object or an atom template. The value + currently provided for this context will be returned. + + + The provided value for the passed context. If a React Context is passed, + returns the context value. If an atom template is passed, returns the atom + instance itself. + + + An error if: + + - Called outside an atom state factory. + - Called outside a scoped context (a React hook or `ecosystem.withScope` call). + - The requested context was not provided. + + + + +## See Also + +- [`ecosystem.withScope`](../classes/Ecosystem.mdx#withscope) +- [The scoped atoms walkthrough](/not-done?path=../../walkthrough/scoped-atoms.mdx) diff --git a/docs/sidebars.js b/docs/sidebars.js index e03fe465..4dc8a6b4 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -229,6 +229,11 @@ module.exports = { label: 'Types', items: ['v2/api/types/AtomConfig', 'v2/api/types/SelectorTemplate'], }, + { + type: 'category', + label: 'Utils', + items: ['v2/api/utils/inject'], + }, 'v2/api/glossary', ], }, diff --git a/docs/src/components/Legend/Legend.tsx b/docs/src/components/Legend/Legend.tsx index 439f9250..2d61a0c4 100644 --- a/docs/src/components/Legend/Legend.tsx +++ b/docs/src/components/Legend/Legend.tsx @@ -52,7 +52,7 @@ export const Item = ({ className="anchor anchorWithStickyNavbar_node_modules-@docusaurus-theme-classic-lib-theme-Heading-styles-module" id={id} > - {name === 'Returns' ? name : {name}} + {name === 'Returns' || name === 'Throws' ? name : {name}} {suffix && {suffix}}
diff --git a/packages/react/src/inject.ts b/packages/react/src/inject.ts index 91ff1989..ac1dd49b 100644 --- a/packages/react/src/inject.ts +++ b/packages/react/src/inject.ts @@ -1,6 +1,29 @@ import { AtomTemplateBase, injectSelf, NodeOf } from '@zedux/atoms' import { Context } from 'react' +/** + * Retrieves a contextual value from a provided scope. + * + * Any atom that calls this becomes "scoped". Its id will change to reflect the + * retrieved context and it can only be accessed in a scoped context - either + * from React hooks or in an `ecosystem.withScope` call. + * + * Any atom that uses a scoped atom is also scoped, inheriting the scope + * dependency of the scoped atom. + * + * An atom and all transitive dependencies it creates can access the provided + * scope. + * + * - When created from React hooks, `inject` can access provided React context + * values or atom instances. + * - When created in an `ecosystem.withScope` callback, `inject` can access the + * provided scope values. + * + * NOTE: `inject` is currently an injector. But it doesn't have to be. We may + * update this to work with selectors too. In that case, this will become a + * "utility" function, not an injector. This is similar to React's `use` utility + * which isn't a hook. + */ export const inject = | AtomTemplateBase>( context: T ): T extends Context From eca6cc0660d97ca9a57b0c99b4330da5b8d05b08 Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Sat, 15 Nov 2025 17:57:26 -0500 Subject: [PATCH 43/49] docs: document the new `As` util --- docs/docs/v2/api/classes/Ecosystem.mdx | 8 +- docs/docs/v2/api/classes/Signal.mdx | 2 +- .../v2/api/injectors/injectMappedSignal.mdx | 4 +- docs/docs/v2/api/injectors/injectRef.mdx | 4 +- docs/docs/v2/api/injectors/injectSignal.mdx | 4 +- docs/docs/v2/api/utils/As.mdx | 77 +++++++++++++++++++ docs/sidebars.js | 2 +- 7 files changed, 89 insertions(+), 12 deletions(-) create mode 100644 docs/docs/v2/api/utils/As.mdx diff --git a/docs/docs/v2/api/classes/Ecosystem.mdx b/docs/docs/v2/api/classes/Ecosystem.mdx index b57ca173..e5d52aad 100644 --- a/docs/docs/v2/api/classes/Ecosystem.mdx +++ b/docs/docs/v2/api/classes/Ecosystem.mdx @@ -980,7 +980,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context - Creates and caches a new signal. It's recommended to use [`injectSignal`](../injectors/injectSignal) instead when possible. However, `ecosystem.signal` can be useful for dynamically creating signals, at the cost of having to manage their lifecycles. For example: + Creates and caches a new signal. It's recommended to use [`injectSignal`](../injectors/injectSignal.mdx) instead when possible. However, `ecosystem.signal` can be useful for dynamically creating signals, at the cost of having to manage their lifecycles. For example: ```ts // `injectSignal` manages the signal's lifecycle... @@ -996,7 +996,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context } ``` - This is just one example. You can manage these signals however you like - e.g. calling [`.destroy`](./ZeduxNode#destroy) on them manually or just keeping them around forever. + This is just one example. You can manage these signals however you like - e.g. calling [`.destroy`](./ZeduxNode.mdx#destroy) on them manually or just keeping them around forever. Signature: @@ -1015,7 +1015,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context { events } ``` - - `events`: An object mapping custom event names to `As`. See [the `As` util](/not-done?path=../utils/As) for more details. + - `events`: An object mapping custom event names to `As`. See [the `As` util](../utils/As.mdx) for more details. A new signal instance. @@ -1045,7 +1045,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context Optional. One of `'flat'`, `'top-down'`, or `'bottom-up'`. Default: `'flat'`. - An object whose structure depends on the requested view. See [the graph walkthrough](../../../walkthrough/the-graph#getting-the-graph). + An object whose structure depends on the requested view. See [the graph walkthrough](../../../walkthrough/the-graph.mdx#getting-the-graph). diff --git a/docs/docs/v2/api/classes/Signal.mdx b/docs/docs/v2/api/classes/Signal.mdx index b480eec5..8cba436a 100644 --- a/docs/docs/v2/api/classes/Signal.mdx +++ b/docs/docs/v2/api/classes/Signal.mdx @@ -160,7 +160,7 @@ See below for the full list of [built-in events](#built-in-events). ### Custom Events -Built-in events like `change` are fired automatically. You can also specify custom events that your signal accepts via the `events` config option. Use the [`As` util](/not-done?path=../utils/As) to type event payloads: +Built-in events like `change` are fired automatically. You can also specify custom events that your signal accepts via the `events` config option. Use the [`As` util](../utils/As.mdx) to type event payloads: ```ts const greetingSignal = ecosystem.signal(null, { diff --git a/docs/docs/v2/api/injectors/injectMappedSignal.mdx b/docs/docs/v2/api/injectors/injectMappedSignal.mdx index cd0d0e8d..4cdbce28 100644 --- a/docs/docs/v2/api/injectors/injectMappedSignal.mdx +++ b/docs/docs/v2/api/injectors/injectMappedSignal.mdx @@ -93,7 +93,7 @@ const mixedValues = injectMappedSignal({ a: signalA, b: 'anything here' }) - Optional. An object mapping custom event names to `As`. See [the `As` util](/not-done?path=../utils/As.mdx). + Optional. An object mapping custom event names to `As`. See [the `As` util](../utils/As.mdx). This object is unused. It's just for type inference. As such, passing the second `EventMap` generic to `injectMappedSignal` has the same effect as passing this config option. @@ -114,4 +114,4 @@ const mixedValues = injectMappedSignal({ a: signalA, b: 'anything here' }) - [The `MappedSignal` class](../classes/MappedSignal.mdx) - [The `Signal` class](../classes/Signal.mdx) - [The `injectSignal` injector](./injectSignal.mdx) -- [The `As` util](/not-done?path=../utils/As.mdx) +- [The `As` util](../utils/As.mdx) diff --git a/docs/docs/v2/api/injectors/injectRef.mdx b/docs/docs/v2/api/injectors/injectRef.mdx index 120c7220..7a689239 100644 --- a/docs/docs/v2/api/injectors/injectRef.mdx +++ b/docs/docs/v2/api/injectors/injectRef.mdx @@ -76,5 +76,5 @@ function App() { ## See Also -- [`injectCallback`](./injectCallback.mdx) -- [`injectMemo`](./injectMemo.mdx) +- [The `injectCallback` injector](./injectCallback.mdx) +- [The `injectMemo` injector](./injectMemo.mdx) diff --git a/docs/docs/v2/api/injectors/injectSignal.mdx b/docs/docs/v2/api/injectors/injectSignal.mdx index 82810403..4e7e8ae4 100644 --- a/docs/docs/v2/api/injectors/injectSignal.mdx +++ b/docs/docs/v2/api/injectors/injectSignal.mdx @@ -66,9 +66,9 @@ const passingGenerics = injectSignal([]) - An object mapping event names to [`As`](/not-done?path=../utils/As) where [`As`](/not-done?path=../utils/As) is Zedux's [`As`](/not-done?path=../utils/As) util and `MyType` is the payload type of the event. + An object mapping event names to `As` where `As` is Zedux's [`As` util](../utils/As.mdx) and `MyType` is the payload type of the event. - This payload must always be passed when the event is sent. Specify [`As`](/not-done?path=../utils/As) to allow no payload. + This payload must always be passed when the event is sent. Specify `As` to allow no payload. A boolean. Default: `true`. diff --git a/docs/docs/v2/api/utils/As.mdx b/docs/docs/v2/api/utils/As.mdx new file mode 100644 index 00000000..126f1bef --- /dev/null +++ b/docs/docs/v2/api/utils/As.mdx @@ -0,0 +1,77 @@ +--- +id: As +title: As +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +```ts +import { As } from '@zedux/react' +``` + +A runtime helper for typing [signal events](../classes/Signal#events). This is just a stable no-op function reference that Zedux never calls. + +## Examples + +Pass different generics to `As` to type custom events: + +```ts +interface Todo { + id: string + isComplete: boolean + text: string +} + +const todosSignal = injectSignal([] as Todo[], { + events: { + addTodo: As, + removeTodo: As, + }, +}) + +// in a callback or effect: +todosSignal.send('addTodo', { id: '1', isComplete: false, text: 'succeed' }) +todosSignal.send('removeTodo', 1) // Type Error! Payload must be a string +``` + +Usage of `As` is not required. You can either specify no-op functions yourself: + +```ts +const todosSignal = injectMappedSignal( + { todos: todosSignal }, + { + events: { + addTodo: () => null as unknown as Todo, + removeTodo: () => null as unknown as string, + }, + } +) +``` + +Or pass TypeScript generics: + +```ts +const signal = ecosystem.signal( + [] +) +``` + +All Zedux's signal-creating APIs accept all of these methods for typing custom events. + +## Signature + + + {tab1(`As = () => 0`)} + {tab2(`declare const As: () => 0 as unknown as T`)} + + +`As` is a no-op function that's only used for type inference. Zedux never calls it. + +## See Also + +- APIs that can use this util: + - [`injectMappedSignal`](../injectors/injectMappedSignal.mdx) + - [`injectPromise`](../injectors/injectPromise.mdx) + - [`injectSignal`](../injectors/injectSignal.mdx) + - [`Ecosystem#signal`](../classes/Ecosystem.mdx#signal) +- [The Signal events documentation](../classes/Signal.mdx#events) diff --git a/docs/sidebars.js b/docs/sidebars.js index 4dc8a6b4..daa0f869 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -232,7 +232,7 @@ module.exports = { { type: 'category', label: 'Utils', - items: ['v2/api/utils/inject'], + items: ['v2/api/utils/As', 'v2/api/utils/inject'], }, 'v2/api/glossary', ], From 50a09219827167a8bd735b200b02aa557b527e72 Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Sat, 15 Nov 2025 18:05:50 -0500 Subject: [PATCH 44/49] docs: document the new `getDefaultEcosystem` util for v2 --- docs/docs/v2/api/classes/Ecosystem.mdx | 19 ++++---- docs/docs/v2/api/hooks/useEcosystem.mdx | 2 +- .../docs/v2/api/utils/getDefaultEcosystem.mdx | 46 +++++++++++++++++++ docs/sidebars.js | 6 ++- 4 files changed, 61 insertions(+), 12 deletions(-) create mode 100644 docs/docs/v2/api/utils/getDefaultEcosystem.mdx diff --git a/docs/docs/v2/api/classes/Ecosystem.mdx b/docs/docs/v2/api/classes/Ecosystem.mdx index e5d52aad..8661c2a4 100644 --- a/docs/docs/v2/api/classes/Ecosystem.mdx +++ b/docs/docs/v2/api/classes/Ecosystem.mdx @@ -28,7 +28,7 @@ import { createEcosystem } from '@zedux/react' const rootEcosystem = createEcosystem({ id: 'root' }) ``` -Ecosystems are also created automatically when using an [``](../components/EcosystemProvider) without passing an `ecosystem` prop: +Ecosystems are also created automatically when using an [``](../components/EcosystemProvider.mdx) without passing an `ecosystem` prop: ```tsx import { EcosystemProvider } from '@zedux/react' @@ -44,15 +44,15 @@ function App() { ### Default Ecosystem -The [default ecosystem](../../../walkthrough/ecosystems#global) will be created automatically if atoms are used in React outside any `` or the first time you call [`getDefaultEcosystem()`](/not-done?path=../utils/getDefaultEcosystem). +The [default ecosystem](../../../walkthrough/ecosystems.mdx#global) will be created automatically if atoms are used in React outside any `` or the first time you call [`getDefaultEcosystem()`](../utils/getDefaultEcosystem.mdx). The default ecosystem is great for simple apps. It's a full ecosystem, which means you can use features like [overrides](#overrides) and [ecosystem events](#events). However, it comes preconfigured with no (good) way to set config options like [`ssr`](#ssr) and [`onReady`](#onready). -It's generally recommended to create your own ecosystem(s) via [`createEcosystem()`](../factories/createEcosystem.mdx) and provide them to your app via [``](../components/EcosystemProvider) instead of using the default ecosystem. This is especially needed for SSR. +It's generally recommended to create your own ecosystem(s) via [`createEcosystem()`](../factories/createEcosystem.mdx) and provide them to your app via [``](../components/EcosystemProvider.mdx) instead of using the default ecosystem. This is especially needed for SSR. ## Providing -Ecosystems can take control of all atom usages in a React component tree by wrapping the tree in [``](../components/EcosystemProvider). +Ecosystems can take control of all atom usages in a React component tree by wrapping the tree in [``](../components/EcosystemProvider.mdx). ```tsx function App() { @@ -1091,12 +1091,11 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context ## See Also -- [the Ecosystems walkthrough](../../../walkthrough/ecosystems) +- [the Ecosystems walkthrough](../../../walkthrough/ecosystems.mdx) - [the `createEcosystem()` factory](../factories/createEcosystem.mdx) -- [the Overrides walkthrough](../../../walkthrough/overrides) -- [the Plugins guide](../../../advanced/plugins) -- [`getDefaultEcosystem()`](/not-done?path=../utils/getDefaultEcosystem) -- [`setDefaultEcosystem()`](/not-done?path=../utils/setDefaultEcosystem) -- [the `` component](../components/EcosystemProvider) +- [the Overrides walkthrough](../../../walkthrough/overrides.mdx) +- [the Plugins guide](../../../advanced/plugins.mdx) +- [`getDefaultEcosystem()`](../utils/getDefaultEcosystem.mdx) +- [the `` component](../components/EcosystemProvider.mdx) - [the `useEcosystem` hook](../hooks/useEcosystem.mdx) - [the `injectEcosystem` injector](../injectors/injectEcosystem.mdx) diff --git a/docs/docs/v2/api/hooks/useEcosystem.mdx b/docs/docs/v2/api/hooks/useEcosystem.mdx index 6e0ad98f..acb879a8 100644 --- a/docs/docs/v2/api/hooks/useEcosystem.mdx +++ b/docs/docs/v2/api/hooks/useEcosystem.mdx @@ -57,4 +57,4 @@ function App() { - [The `Ecosystem` class](../classes/Ecosystem.mdx) - [The `` component](../components/EcosystemProvider.mdx) -- [The `getDefaultEcosystem()` utility](/not-done?path=../utils/getDefaultEcosystem.mdx) +- [The `getDefaultEcosystem` utility](../utils/getDefaultEcosystem.mdx) diff --git a/docs/docs/v2/api/utils/getDefaultEcosystem.mdx b/docs/docs/v2/api/utils/getDefaultEcosystem.mdx new file mode 100644 index 00000000..0a7c827c --- /dev/null +++ b/docs/docs/v2/api/utils/getDefaultEcosystem.mdx @@ -0,0 +1,46 @@ +--- +id: getDefaultEcosystem +title: getDefaultEcosystem +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +```ts +import { getDefaultEcosystem } from '@zedux/react' +``` + +Returns the [default ecosystem](../classes/Ecosystem.mdx#default-ecosystem). Creates it if it doesn't exist yet. + +## Example + +```tsx live ecosystemId=example/getDefaultEcosystem resultVar=id version=2 +import { getDefaultEcosystem } from '@zedux/react' + +const ecosystem = getDefaultEcosystem() +const { id } = ecosystem +``` + +## Signature + + + {tab1(`getDefaultEcosystem = () => Ecosystem`)} + {tab2(`declare const getDefaultEcosystem: () => Ecosystem`)} + + + + + + An [Ecosystem](../classes/Ecosystem.mdx) instance. + + The first time this is called, the default ecosystem will be created and cached in Zedux's module state. Subsequent calls will return the cached instance. + + + + +## See Also + +- [The Ecosystems walkthrough](../../../walkthrough/ecosystems.mdx) +- [The `Ecosystem` class](../classes/Ecosystem.mdx) +- [The `` component](../components/EcosystemProvider.mdx) +- [The `createEcosystem()` factory](../factories/createEcosystem.mdx) +- [The `useEcosystem()` hook](../hooks/useEcosystem.mdx) diff --git a/docs/sidebars.js b/docs/sidebars.js index daa0f869..85bee5f4 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -232,7 +232,11 @@ module.exports = { { type: 'category', label: 'Utils', - items: ['v2/api/utils/As', 'v2/api/utils/inject'], + items: [ + 'v2/api/utils/As', + 'v2/api/utils/getDefaultEcosystem', + 'v2/api/utils/inject', + ], }, 'v2/api/glossary', ], From 89cd2b22b894062da13616f978d8354290ea9171 Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Sat, 15 Nov 2025 18:11:27 -0500 Subject: [PATCH 45/49] docs: document `getInternals` for v2 --- docs/docs/v2/api/utils/getInternals.mdx | 58 +++++++++++++++++++++++++ docs/sidebars.js | 1 + 2 files changed, 59 insertions(+) create mode 100644 docs/docs/v2/api/utils/getInternals.mdx diff --git a/docs/docs/v2/api/utils/getInternals.mdx b/docs/docs/v2/api/utils/getInternals.mdx new file mode 100644 index 00000000..2c2122aa --- /dev/null +++ b/docs/docs/v2/api/utils/getInternals.mdx @@ -0,0 +1,58 @@ +--- +id: getInternals +title: getInternals +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +```ts +import { getInternals } from '@zedux/react' +``` + +A utility function that returns Zedux's internal state. This is primarily used for advanced use cases like sharing state between multiple Zedux instances or debugging. + +:::warning Advanced Usage +This is an advanced utility that exposes Zedux internals. Use with caution and only when necessary. +::: + +Pass the returned internals object to [`setInternals`](/not-done?path=./setInternals.mdx) e.g. in a child window or iframe to share state between multiple Zedux instances. Note that you'll typically want to share your [ecosystem(s)](../classes/Ecosystem.mdx) as well. + +## Example + +```tsx live ecosystemId=getInternals/example resultVar=App version=2 +function App() { + const showInternals = () => { + const internals = getInternals() + console.log('Current evaluation context:', internals.c) + console.log('Default ecosystem:', internals.g) + console.log('Ecosystem ID:', internals.g.id) + } + + return +} +``` + +## Signature + + + {tab1(`getInternals = () => internals`)} + {tab2(`declare const getInternals: () => { + c: EvaluationContext + g: Ecosystem + }`)} + + + + + + An object containing two obfuscated internal objects: + + - **`c`** - The current evaluation context. This is how [injectors](../glossary.mdx#injector) and other utilities like [`ecosystem.why`](../classes/Ecosystem.mdx#why) work. + - **`g`** - The default ecosystem instance. Note that calling `getInternals` creates this if it hasn't been created yet. + + + + +## See Also + +- [The `setInternals` utility](/not-done?path=./setInternals.mdx) diff --git a/docs/sidebars.js b/docs/sidebars.js index 85bee5f4..e04ac3c9 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -235,6 +235,7 @@ module.exports = { items: [ 'v2/api/utils/As', 'v2/api/utils/getDefaultEcosystem', + 'v2/api/utils/getInternals', 'v2/api/utils/inject', ], }, From 3413f6a74b3952b26ab15c6c13c0275b479bba2f Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Sat, 15 Nov 2025 19:16:51 -0500 Subject: [PATCH 46/49] docs: document `is` util for v2 --- docs/docs/v2/api/utils/is.mdx | 82 +++++++++++++++++++++++++++++++++++ docs/sidebars.js | 1 + 2 files changed, 83 insertions(+) create mode 100644 docs/docs/v2/api/utils/is.mdx diff --git a/docs/docs/v2/api/utils/is.mdx b/docs/docs/v2/api/utils/is.mdx new file mode 100644 index 00000000..67d58690 --- /dev/null +++ b/docs/docs/v2/api/utils/is.mdx @@ -0,0 +1,82 @@ +--- +id: is +title: is +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +```ts +import { is } from '@zedux/react' +``` + +A utility function for checking if a value is an instance of a given Zedux class. It provides type guards for all Zedux classes that it can check, helping with runtime type checking. + +:::warning +This is primarily for internal use. It relies on TypeScript to enforce that you pass a valid class to check against. You can get false positives if you pass a class that doesn't have a `$$typeof` property. While we could change this behavior, we don't want to for performance reasons. +::: + +## Example + +```tsx live ecosystemId=is/example resultVar=App version=2 +const ecosystem = createEcosystem() +const anAtom = atom('anAtom', 'Hello') +const aSignal = ecosystem.signal('World') +const aSelectorInstance = ecosystem.getNode(() => aSignal.get() + '!') + +function TypeChecker() { + const anAtomApi = api() + const atomInstance = useAtomInstance(anAtom) + + const checkTypes = () => { + console.log('Is atom template:', is(anAtom, AtomTemplate)) + console.log('Is atom instance:', is(atomInstance, AtomInstance)) + console.log('Is atom api:', is(anAtomApi, AtomApi)) + console.log( + 'Is selector instance:', + is(aSelectorInstance, SelectorInstance) + ) + } + + return +} + +function App() { + return +} +``` + +## Signature + + + {tab1(`is = (value, type) => boolean`)} + {tab2(`declare const is: { + (value: any, type: AtomInstance): value is AtomInstance + (value: any, type: AtomTemplate): value is AtomTemplate + (value: any, type: AtomApi): value is AtomApi + (value: any, type: SelectorInstance): value is SelectorInstance +}`)} + + + + Required. The value to check. Can be anything. + + + Required. A Zedux class constructor reference to check against. Accepted classes: + + - `AtomInstance` + - `AtomTemplate` + - `AtomApi` + - `SelectorInstance` + + + + `true` if the value is an instance of the specified class, `false` otherwise. + + + +## See Also + +- [The `AtomInstance` class](../classes/AtomInstance.mdx) +- [The `AtomTemplate` class](../classes/AtomTemplate.mdx) +- [The `AtomApi` class](../classes/AtomApi.mdx) +- [The `SelectorInstance` class](../classes/SelectorInstance.mdx) diff --git a/docs/sidebars.js b/docs/sidebars.js index e04ac3c9..b166d8eb 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -237,6 +237,7 @@ module.exports = { 'v2/api/utils/getDefaultEcosystem', 'v2/api/utils/getInternals', 'v2/api/utils/inject', + 'v2/api/utils/is', ], }, 'v2/api/glossary', From 2f720fc006342ad524d1b6459b7820f0fe49ff11 Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Sat, 15 Nov 2025 19:29:23 -0500 Subject: [PATCH 47/49] docs: document `setInternals` for v2 --- docs/docs/v2/api/utils/getInternals.mdx | 4 +- docs/docs/v2/api/utils/setInternals.mdx | 72 +++++++++++++++++++++++++ docs/sidebars.js | 1 + 3 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 docs/docs/v2/api/utils/setInternals.mdx diff --git a/docs/docs/v2/api/utils/getInternals.mdx b/docs/docs/v2/api/utils/getInternals.mdx index 2c2122aa..2d11228e 100644 --- a/docs/docs/v2/api/utils/getInternals.mdx +++ b/docs/docs/v2/api/utils/getInternals.mdx @@ -15,7 +15,7 @@ A utility function that returns Zedux's internal state. This is primarily used f This is an advanced utility that exposes Zedux internals. Use with caution and only when necessary. ::: -Pass the returned internals object to [`setInternals`](/not-done?path=./setInternals.mdx) e.g. in a child window or iframe to share state between multiple Zedux instances. Note that you'll typically want to share your [ecosystem(s)](../classes/Ecosystem.mdx) as well. +Pass the returned internals object to [`setInternals`](./setInternals.mdx) e.g. in a child window or iframe to share state between multiple Zedux instances. Note that you'll typically want to share your [ecosystem(s)](../classes/Ecosystem.mdx) as well. ## Example @@ -55,4 +55,4 @@ function App() { ## See Also -- [The `setInternals` utility](/not-done?path=./setInternals.mdx) +- [The `setInternals` utility](./setInternals.mdx) diff --git a/docs/docs/v2/api/utils/setInternals.mdx b/docs/docs/v2/api/utils/setInternals.mdx new file mode 100644 index 00000000..37c9fcdc --- /dev/null +++ b/docs/docs/v2/api/utils/setInternals.mdx @@ -0,0 +1,72 @@ +--- +id: setInternals +title: setInternals +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +```ts +import { setInternals } from '@zedux/react' +``` + +A utility function that sets Zedux's internal state to the state previously retrieved from [`getInternals`](./getInternals.mdx). This is primarily used for sharing state between multiple Zedux instances, such as in child windows or iframe scenarios. + +:::warning Advanced Usage +This is an advanced utility that modifies Zedux internals. Use with caution and only when necessary. +::: + +## Example + +```ts +// in the parent window: +import { getInternals } from '@zedux/react' + +window.getZeduxInternals = getInternals + +// In a child window: +const parentInternals = window.opener.getZeduxInternals() +setInternals(parentInternals) + +// Now the Zedux instance in this window shares its internal state with the +// parent window's Zedux instance. +``` + +If you're using the [default ecosystem](../classes/Ecosystem.mdx#default-ecosystem), this is all you need to do to share all your state across windows. But when using custom ecosystems, you'll typically want to share that across windows as well: + +```tsx +// in the parent window: +import { createEcosystem } from '@zedux/react' + +window.rootEcosystem = createEcosystem() + +// In a child window: +const rootEcosystem = window.opener.rootEcosystem + +// And use it as normal, e.g. providing it to an : +function ChildWindow() { + return ( + + + + ) +} +``` + + + {tab1(`setInternals = (internals) => void`)} + {tab2(`declare const setInternals: (internals: { + c: EvaluationContext + g: Ecosystem + }) => void`)} + + + + + Required. An internals object previously retrieved from + [`getInternals()`](./getInternals.mdx). + + + +## See Also + +- [The `getInternals` utility](./getInternals.mdx) diff --git a/docs/sidebars.js b/docs/sidebars.js index b166d8eb..9caf52a9 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -238,6 +238,7 @@ module.exports = { 'v2/api/utils/getInternals', 'v2/api/utils/inject', 'v2/api/utils/is', + 'v2/api/utils/setInternals', ], }, 'v2/api/glossary', From 6d612669c9a37f4841f1ed13a80d074dfe89107b Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Sat, 15 Nov 2025 19:47:31 -0500 Subject: [PATCH 48/49] docs: document new `untrack` util --- docs/docs/v2/api/utils/untrack.mdx | 72 ++++++++++++++++++++++++++++++ docs/sidebars.js | 1 + 2 files changed, 73 insertions(+) create mode 100644 docs/docs/v2/api/utils/untrack.mdx diff --git a/docs/docs/v2/api/utils/untrack.mdx b/docs/docs/v2/api/utils/untrack.mdx new file mode 100644 index 00000000..f18e4d22 --- /dev/null +++ b/docs/docs/v2/api/utils/untrack.mdx @@ -0,0 +1,72 @@ +--- +id: untrack +title: untrack +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +```ts +import { untrack } from '@zedux/react' +``` + +A utility function that prevents Zedux from registering graph edges during the execution of a callback. This is useful when you want to read atom state without creating dependencies. + +Note that [`node.getOnce`](../classes/ZeduxNode.mdx#getonce), [`ecosystem.getOnce`](../classes/Ecosystem.mdx#getonce), and [`ecosystem.getNodeOnce`](../classes/Ecosystem.mdx#getnodeonce) have this same behavior of never creating dependencies, but are more explicit at the point of call. It's typically recommended to use those in places that should never register dependencies. + +:::tip +Avoid this unless you're sure you need it. Most Zedux APIs are reactive by default, and that's a good thing! +::: + +## Example + +```tsx live ecosystemId=untrack/example resultVar=App version=2 +const sourceAtom = atom('source', 0) + +const derivedAtom = atom('derived', () => { + const sourceInstance = injectAtomInstance(sourceAtom) + + // This read won't create a graph dependency between derivedAtom and sourceAtom + const dependent = untrack(() => sourceInstance.get()) + + // This atom will not reevaluate when sourceAtom changes, + return dependent + 2 +}) + +function App() { + const [source, setSource] = useAtomState(sourceAtom) + const derived = useAtomValue(derivedAtom) + + return ( +
+
Source: {source}
+
Derived: {derived} (never updates)
+ +
+ ) +} +``` + +Remove the `untrack` call in the above example to see the derived state update when the source state changes. + +Note that this example could use `sourceInstance.getOnce()` instead to get the same effect. + + + {tab1(`untrack = (callback) => result`)} + {tab2(`declare const untrack: (callback: () => T) => T`)} + + + + + Required. A function to execute without tracking dependencies. + + + The result of calling the callback function, returned as-is. + + + +## See Also + +- [The graph walkthrough](../../../walkthrough/the-graph.mdx) +- [`ZeduxNode#getOnce`](../classes/ZeduxNode.mdx#getonce) +- [`Ecosystem#getOnce`](../classes/Ecosystem.mdx#getonce) +- [`Ecosystem#getNodeOnce`](../classes/Ecosystem.mdx#getnodeonce) diff --git a/docs/sidebars.js b/docs/sidebars.js index 9caf52a9..f3697d20 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -239,6 +239,7 @@ module.exports = { 'v2/api/utils/inject', 'v2/api/utils/is', 'v2/api/utils/setInternals', + 'v2/api/utils/untrack', ], }, 'v2/api/glossary', From 3f3692051ed0fd65e3c5c63634a943b5c1562e29 Mon Sep 17 00:00:00 2001 From: Joshua Claunch Date: Sun, 16 Nov 2025 17:42:53 -0500 Subject: [PATCH 49/49] docs: rework selector types documentation; add some page placeholders --- docs/docs/v2/advanced/batching.mdx | 10 + docs/docs/v2/advanced/persistence.mdx | 10 + docs/docs/v2/api/classes/AsyncScheduler.mdx | 10 + docs/docs/v2/api/classes/Ecosystem.mdx | 51 +++-- docs/docs/v2/api/classes/SelectorInstance.mdx | 14 +- docs/docs/v2/api/classes/Signal.mdx | 4 +- docs/docs/v2/api/classes/SyncScheduler.mdx | 10 + docs/docs/v2/api/classes/ZeduxNode.mdx | 25 +-- docs/docs/v2/api/components/AtomProvider.mdx | 2 +- .../v2/api/components/EcosystemProvider.mdx | 6 +- docs/docs/v2/api/factories/atom.mdx | 2 +- .../docs/v2/api/factories/createEcosystem.mdx | 2 +- docs/docs/v2/api/factories/ion.mdx | 2 +- docs/docs/v2/api/hooks/useAtomInstance.mdx | 5 +- docs/docs/v2/api/hooks/useAtomSelector.mdx | 4 +- docs/docs/v2/api/hooks/useAtomState.mdx | 1 - docs/docs/v2/api/hooks/useAtomValue.mdx | 1 - .../docs/v2/api/injectors/injectHydration.mdx | 2 +- docs/docs/v2/api/injectors/injectSelf.mdx | 4 +- docs/docs/v2/api/injectors/injectWhy.mdx | 4 +- docs/docs/v2/api/types/AtomConfig.mdx | 19 +- docs/docs/v2/api/types/AtomSelector.mdx | 112 +++++++++++ docs/docs/v2/api/types/AtomSelectorConfig.mdx | 139 ++++++++++++++ docs/docs/v2/api/types/DehydrationFilter.mdx | 10 + docs/docs/v2/api/types/EcosystemConfig.mdx | 10 + docs/docs/v2/api/types/EvaluationReason.mdx | 10 + docs/docs/v2/api/types/NodeFilter.mdx | 10 + docs/docs/v2/api/types/SelectorTemplate.mdx | 174 +++--------------- docs/docs/v2/api/types/Transaction.mdx | 10 + docs/docs/v2/api/utils/inject.mdx | 2 +- docs/docs/v2/walkthrough/atom-lifecycle.mdx | 10 + docs/docs/v2/walkthrough/scoped-atoms.mdx | 10 + docs/sidebars.js | 7 +- 33 files changed, 465 insertions(+), 227 deletions(-) create mode 100644 docs/docs/v2/advanced/batching.mdx create mode 100644 docs/docs/v2/advanced/persistence.mdx create mode 100644 docs/docs/v2/api/classes/AsyncScheduler.mdx create mode 100644 docs/docs/v2/api/classes/SyncScheduler.mdx create mode 100644 docs/docs/v2/api/types/AtomSelector.mdx create mode 100644 docs/docs/v2/api/types/AtomSelectorConfig.mdx create mode 100644 docs/docs/v2/api/types/DehydrationFilter.mdx create mode 100644 docs/docs/v2/api/types/EcosystemConfig.mdx create mode 100644 docs/docs/v2/api/types/EvaluationReason.mdx create mode 100644 docs/docs/v2/api/types/NodeFilter.mdx create mode 100644 docs/docs/v2/api/types/Transaction.mdx create mode 100644 docs/docs/v2/walkthrough/atom-lifecycle.mdx create mode 100644 docs/docs/v2/walkthrough/scoped-atoms.mdx diff --git a/docs/docs/v2/advanced/batching.mdx b/docs/docs/v2/advanced/batching.mdx new file mode 100644 index 00000000..975a534c --- /dev/null +++ b/docs/docs/v2/advanced/batching.mdx @@ -0,0 +1,10 @@ +--- +id: batching +title: Batching +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +:::note +🚧 This page is under construction +::: diff --git a/docs/docs/v2/advanced/persistence.mdx b/docs/docs/v2/advanced/persistence.mdx new file mode 100644 index 00000000..0725d9a8 --- /dev/null +++ b/docs/docs/v2/advanced/persistence.mdx @@ -0,0 +1,10 @@ +--- +id: persistence +title: Persistence +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +:::note +🚧 This page is under construction +::: diff --git a/docs/docs/v2/api/classes/AsyncScheduler.mdx b/docs/docs/v2/api/classes/AsyncScheduler.mdx new file mode 100644 index 00000000..d43156ca --- /dev/null +++ b/docs/docs/v2/api/classes/AsyncScheduler.mdx @@ -0,0 +1,10 @@ +--- +id: AsyncScheduler +title: AsyncScheduler +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +:::note +🚧 This page is under construction +::: diff --git a/docs/docs/v2/api/classes/Ecosystem.mdx b/docs/docs/v2/api/classes/Ecosystem.mdx index 8661c2a4..4f8f234b 100644 --- a/docs/docs/v2/api/classes/Ecosystem.mdx +++ b/docs/docs/v2/api/classes/Ecosystem.mdx @@ -119,7 +119,7 @@ Full event list: - `newState` - The new state of the graph node. Can be anything. - `oldState` - The previous state of the graph node. Can be anything. - - `reasons` - An indefinitely-nested array of [reasons](/not-done?path=../types/EvaluationReason) why the state changed. This tracks the series of state changes through the dependency graph that led to the current node changing. If the node was updated directly, this will be undefined. + - `reasons` - An indefinitely-nested array of [reasons](../types/EvaluationReason.mdx) why the state changed. This tracks the series of state changes through the dependency graph that led to the current node changing. If the node was updated directly, this will be undefined. - `source` - The [`ZeduxNode`](./ZeduxNode) that changed. In normal Zedux v2+ apps, this will always be defined. The legacy `@zedux/stores` package's stores are the only exception. - `type` - The string `"change"`. @@ -220,7 +220,7 @@ function App() { { reasons, source, type } ``` - - `reasons` - An indefinitely-nested array of [reasons](/not-done?path=../types/EvaluationReason) that resulted in the atom reevaluating and changing its promise. This tracks the series of state changes through the dependency graph that led to the current node reevaluating. If the node was updated directly, this will be undefined. + - `reasons` - An indefinitely-nested array of [reasons](../types/EvaluationReason.mdx) that resulted in the atom reevaluating and changing its promise. This tracks the series of state changes through the dependency graph that led to the current node reevaluating. If the node was updated directly, this will be undefined. - `source` - The [`ZeduxNode`](./ZeduxNode) whose promise changed. Though the types don't reflect it yet, this will always be defined. - `type` - The string `"promiseChange"`. @@ -327,10 +327,10 @@ Every ecosystem has the following properties. All properties are readonly! - A reference to the [async scheduler](/not-done?path=./AsyncScheduler) used by this ecosystem. You may want to access this scheduler's `queue` or `flush` methods. See the [AsyncScheduler doc](/not-done?path=./AsyncScheduler) for details. + A reference to the [async scheduler](./AsyncScheduler.mdx) used by this ecosystem. You may want to access this scheduler's `queue` or `flush` methods. See the [AsyncScheduler doc](./AsyncScheduler.mdx) for details. - A boolean. May be undefined. This is set to the `complexParams` value you passed via [EcosystemConfig](/not-done?path=../types/EcosystemConfig#complexparams) when creating this ecosystem. + A boolean. May be undefined. This is set to the `complexParams` value you passed via [EcosystemConfig](../types/EcosystemConfig.mdx#complexparams) when creating this ecosystem. Whether to allow non-serializable values as atom and selector params. See [the complex params guide](../../../advanced/complex-params). @@ -342,18 +342,18 @@ Every ecosystem has the following properties. All properties are readonly! - A string or undefined. The id of this ecosystem as set via [EcosystemConfig](/not-done?path=../types/EcosystemConfig#id) when creating this ecosystem (if any). The string `'@@zedux/default'` is reserved for the [default ecosystem](#default-ecosystem). + A string or undefined. The id of this ecosystem as set via [EcosystemConfig](../types/EcosystemConfig.mdx#id) when creating this ecosystem (if any). The string `'@@zedux/default'` is reserved for the [default ecosystem](#default-ecosystem). This is just for debugging and is not guaranteed to be unique. - A reference to the [sync scheduler](/not-done?path=./SyncScheduler) used by this ecosystem. Unlike the [`asyncScheduler`](#asyncscheduler), normal users should never need to access this. This is mostly for plugin authors to efficiently schedule Zedux jobs. + A reference to the [sync scheduler](./SyncScheduler.mdx) used by this ecosystem. Unlike the [`asyncScheduler`](#asyncscheduler), normal users should never need to access this. This is mostly for plugin authors to efficiently schedule Zedux jobs. - An array of strings. This is set to the `tags` value you passed via [EcosystemConfig](/not-done?path=../types/EcosystemConfig#tags) when creating this ecosystem. + An array of strings. This is set to the `tags` value you passed via [EcosystemConfig](../types/EcosystemConfig.mdx#tags) when creating this ecosystem. - These work in conjunction with [atom tags](/not-done?path=../types/AtomTemplate#tags) to raise warnings when unsafe atom templates are not overridden in certain environments. + These work in conjunction with [atom tags](../types/AtomTemplate.mdx#tags) to raise warnings when unsafe atom templates are not overridden in certain environments. If an atom template is used that has a flag that is not present in this array, Zedux will log a warning. @@ -385,7 +385,7 @@ Every ecosystem has the following properties. All properties are readonly! - A function. May be undefined. This is set to the `onReady` value you passed via [EcosystemConfig](/not-done?path=../types/EcosystemConfig#onready) when creating this ecosystem. + A function. May be undefined. This is set to the `onReady` value you passed via [EcosystemConfig](../types/EcosystemConfig.mdx#onready) when creating this ecosystem. Will be called as soon as the ecosystem has initialized. Is also called every time the ecosystem is [`reset`](#reset). @@ -449,13 +449,10 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context If an initial `overrides` array is passed, they will be immediately mapped into this object. - - The [Selectors](/not-done?path=../classes/Selectors) class instance that tracks all cached atom selectors in this ecosystem. - A boolean. Default: `false`. Whether the ecosystem is being used on the server to generate SSR content. - This is set to the `ssr` value you passed via [EcosystemConfig](/not-done?path=../types/EcosystemConfig) when creating this ecosystem. + This is set to the `ssr` value you passed via [EcosystemConfig](../types/EcosystemConfig.mdx) when creating this ecosystem. Currently the only thing this affects is [`injectEffect()`](../injectors/injectEffect.mdx) - SSR mode prevents effects from running at all in this ecosystem. @@ -497,7 +494,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context - See [the batching guide](/not-done?path=../../advanced/batching). + See [the batching guide](../../advanced/batching.mdx).
@@ -517,7 +514,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context Optional. Filters to limit the atoms included in the snapshot. If not specified, all atom instances will be dehydrated. - See [the DehydrationFilter type](/not-done?path=../types/DehydrationFilter) for allowed values and what they do. + See [the DehydrationFilter type](../types/DehydrationFilter.mdx) for allowed values and what they do. An object mapping atom instance ids to their current state. @@ -595,7 +592,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context Optional. Filters to limit the nodes returned. If not specified, `findAll` returns all nodes. - See [the NodeFilter type](/not-done?path=../types/NodeFilter) for allowed values and what they do. + See [the NodeFilter type](../types/NodeFilter.mdx) for allowed values and what they do. An array of all cached nodes in the ecosystem that match the filter. @@ -624,7 +621,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context `get` is reactive by default. This behavior changes depending on when it's called: - - When called in a [reactive context](../glossary#reactive-context) (e.g. while an atom or selector is evaluating), `get` registers a [dynamic graph dependency](../glossary#dynamic-graph-dependency) between the currently-evaluating node and the retrieved node. + - When called in a [reactive context](../glossary.mdx#reactive-context) (e.g. while an atom or selector is evaluating), `get` registers a [dynamic graph dependency](../glossary.mdx#dynamic-graph-dependency) between the currently-evaluating node and the retrieved node. - When called outside evaluation (e.g. in an effect or callback), `get` doesn't register any graph dependencies. Note that [`node.get()`](./ZeduxNode#get) has this exact same behavior. In fact, `ecosystem.get(myAtom)` is almost a shorthand for `ecosystem.getNode(myAtom).get()`, with one key difference. See next section: @@ -665,7 +662,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context - Required. An [atom template](./AtomTemplate), [selector template](../types/SelectorTemplate.mdx), or atom instance. + Required. An [atom template](./AtomTemplate.mdx), [selector template](../types/SelectorTemplate.mdx), or [atom instance](./AtomInstance.mdx). Optional (required if the template takes params). An array of the atom or selector's params. Only relevant when passing an atom or selector template. @@ -677,7 +674,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context - Gets a cached [graph node](../glossary#graph-node), namely an [atom instance](./AtomInstance) or [selector instance](./SelectorInstance.mdx). Creates and caches the node if it doesn't exist yet. + Gets a cached [graph node](../glossary.mdx#graph-node), namely an [atom instance](./AtomInstance.mdx) or [selector instance](./SelectorInstance.mdx). Creates and caches the node if it doesn't exist yet. ```ts const { getNode } = myEcosystem // `getNode` can be destructured like this @@ -698,7 +695,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context `getNode` registers graph dependencies by default. This behavior changes depending on when it's called: - - When called in a [reactive context](../glossary#reactive-context) (e.g. while an atom or selector is evaluating), `getNode` registers a [static graph dependency](../glossary#static-graph-dependency) (unlike [`ecosystem.get`](#get), which registers dynamic dependencies) between the currently-evaluating node and the retrieved node. + - When called in a [reactive context](../glossary.mdx#reactive-context) (e.g. while an atom or selector is evaluating), `getNode` registers a [static graph dependency](../glossary.mdx#static-graph-dependency) (unlike [`ecosystem.get`](#get), which registers dynamic dependencies) between the currently-evaluating node and the retrieved node. - When called outside evaluation (e.g. in an effect or callback), `getNode` doesn't register any graph dependencies. #### Note on Caching @@ -754,7 +751,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context - Returns a [graph node](../glossary#graph-node). Functions exactly like [`ecosystem.getNode`](#getnode) except it never registers graph dependencies even when called in [reactive contexts](../glossary#reactive-context). + Returns a [graph node](../glossary.mdx#graph-node). Functions exactly like [`ecosystem.getNode`](#getnode) except it never registers graph dependencies even when called in [reactive contexts](../glossary.mdx#reactive-context). Signature: @@ -766,7 +763,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context - Returns the current value of the resolved atom or selector node. Functions exactly like [`ecosystem.get`](#get) except it never registers graph dependencies even when called in [reactive contexts](../glossary#reactive-context). + Returns the current value of the resolved atom or selector node. Functions exactly like [`ecosystem.get`](#get) except it never registers graph dependencies even when called in [reactive contexts](../glossary.mdx#reactive-context). Signature: @@ -792,7 +789,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context Required. An array of anything. - If any item is a [graph node](../glossary#graph-node), it will be serialized as the node's id. + If any item is a [graph node](../glossary.mdx#graph-node), it will be serialized as the node's id. Optional. A boolean. Defaults to the ecosystem's [`complexParams` config](#complexparams). @@ -835,7 +832,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context Generates a consistent id that is guaranteed to be unique in this ecosystem, but not at all guaranteed to be unique globally. - You can override this by passing the [`makeId`](/not-done?path=../types/EcosystemConfig#makeid) option to [`createEcosystem`](../factories/createEcosystem.mdx). The default implementation is suitable for most use cases, including: + You can override this by passing the [`makeId`](../types/EcosystemConfig.mdx#makeid) option to [`createEcosystem`](../factories/createEcosystem.mdx). The default implementation is suitable for most use cases, including: - apps that use only one ecosystem (the most common). - snapshot testing the ecosystem graph and dehydrations - calling `ecosystem.reset()` after each test will reset the ecosystem's id counter. @@ -922,7 +919,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context - Force destroys all [graph nodes](../glossary#graph-node) in the ecosystem. Calls the cleanup function returned from the [onReady](/not-done?path=../types/EcosystemConfig#onready) function (if any), and calls `onReady` again to reinitialize the ecosystem. + Force destroys all [graph nodes](../glossary.mdx#graph-node) in the ecosystem. Calls the cleanup function returned from the [onReady](../types/EcosystemConfig.mdx#onready) function (if any), and calls `onReady` again to reinitialize the ecosystem. Accepts several options to also clear cached hydrations, listeners, and overrides. @@ -1051,7 +1048,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context - Returns a list of [EvaluationReasons](/not-done?path=../types/EvaluationReason) detailing why the current atom instance or selector is evaluating. + Returns a list of [EvaluationReasons](../types/EvaluationReason.mdx) detailing why the current atom instance or selector is evaluating. If called outside a selector or atom state factory, `why` always returns `undefined`. @@ -1059,7 +1056,7 @@ ecosystem.reset({ context: { redux: otherReduxStore } }) // replaces context - Runs a callback in a scoped context. This is the only way to create or retrieve [scoped atoms](../glossary#scoped-atom) outside React components. + Runs a callback in a scoped context. This is the only way to create or retrieve [scoped atoms](../glossary.mdx#scoped-atom) outside React components. A "scope" is a JS Map mapping "contexts" to values. "Context" simultaneously means two completely different things: diff --git a/docs/docs/v2/api/classes/SelectorInstance.mdx b/docs/docs/v2/api/classes/SelectorInstance.mdx index 126de013..30992e27 100644 --- a/docs/docs/v2/api/classes/SelectorInstance.mdx +++ b/docs/docs/v2/api/classes/SelectorInstance.mdx @@ -5,9 +5,9 @@ title: SelectorInstance import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' -A cached [selector](/not-done?path=../types/AtomSelector). When a selector is used for the first time in most Zedux hooks, injectors, or ecosystem methods, Zedux caches the selector's result by creating an instance of this class. +A cached [selector](../types/AtomSelector.mdx). When a selector is used for the first time in most Zedux hooks, injectors, or ecosystem methods, Zedux caches the selector's result by creating an instance of this class. -Selectors are cached by reference. Each unique [AtomSelector](/not-done?path=../types/AtomSelector) function reference or [AtomSelectorConfig](/not-done?path=../types/AtomSelectorConfig) object creates a unique cached selector instance. +Selectors are cached by reference. Each unique [AtomSelector](../types/AtomSelector.mdx) function reference or [AtomSelectorConfig](../types/AtomSelectorConfig.mdx) object creates a unique cached selector instance. Selectors that take params also create a unique cached selector instance for each unique set of params. This works exactly the same as [atom params](./AtomInstance#params). @@ -15,7 +15,7 @@ Selector instances are graph nodes, meaning this class extends [the `ZeduxNode` ## Creation -You never instantiate this class yourself. Zedux instantiates it automatically as you use [selectors](/not-done?path=../types/AtomSelector) in your app. +You never instantiate this class yourself. Zedux instantiates it automatically as you use [selectors](../types/AtomSelector.mdx) in your app. ```ts const getUsername = ({ get }: Ecosystem) => get(userDataAtom).username @@ -98,7 +98,7 @@ For TypeScript users, selector instances inherit the following generics from the See [`ZeduxNode<{ Template }>`](./ZeduxNode.mdx#gtemplate). - For selectors, this is a reference to the [AtomSelector](/not-done?path=../types/AtomSelector) function or [AtomSelectorConfig](/not-done?path=../types/AtomSelectorConfig) object that was used to create this selector instance. + For selectors, this is a reference to the [AtomSelector](../types/AtomSelector.mdx) function or [AtomSelectorConfig](../types/AtomSelectorConfig.mdx) object that was used to create this selector instance. @@ -155,7 +155,7 @@ Selector instances inherit the following methods from [`ZeduxNode`](./ZeduxNode. ## See Also -- The [selectors walkthrough](../../../walkthrough/selectors.mdx) +- The [selectors walkthrough](/not-done?path=../../walkthrough/selectors.mdx) - The "selector template" types: - - [The `AtomSelector` type](/not-done?path=../types/AtomSelector) - - [The `AtomSelectorConfig` type](/not-done?path=../types/AtomSelectorConfig) + - [The `AtomSelector` type](../types/AtomSelector.mdx) + - [The `AtomSelectorConfig` type](../types/AtomSelectorConfig.mdx) diff --git a/docs/docs/v2/api/classes/Signal.mdx b/docs/docs/v2/api/classes/Signal.mdx index 8cba436a..84abe9eb 100644 --- a/docs/docs/v2/api/classes/Signal.mdx +++ b/docs/docs/v2/api/classes/Signal.mdx @@ -211,7 +211,7 @@ All signals have the following built-in events: Sent whenever [`signal.mutate`](#mutate) is called. - The payload is an array of [transactions](/not-done?path=../types/Transaction) efficiently documenting the changes made to the signal's state by the `mutate` call. + The payload is an array of [transactions](../types/Transaction.mdx) efficiently documenting the changes made to the signal's state by the `mutate` call. ```tsx live noProvide=true resultVar=result version=2 const ecosystem = createEcosystem() @@ -274,7 +274,7 @@ Signals have the following unique methods: This is recursive! Zedux will lazily create proxies as needed when you access nested properties. - Every `mutate` call also causes the signal to fire both a [`change` event](#change-event) and a [`mutate` event](#mutate-event) with an array of [transactions](/not-done?path=../types/Transaction) efficiently documenting the changes made. + Every `mutate` call also causes the signal to fire both a [`change` event](#change-event) and a [`mutate` event](#mutate-event) with an array of [transactions](../types/Transaction.mdx) efficiently documenting the changes made. :::important `mutate` can only be called on signals whose state is a JavaScript object, array, or Set. diff --git a/docs/docs/v2/api/classes/SyncScheduler.mdx b/docs/docs/v2/api/classes/SyncScheduler.mdx new file mode 100644 index 00000000..213e787e --- /dev/null +++ b/docs/docs/v2/api/classes/SyncScheduler.mdx @@ -0,0 +1,10 @@ +--- +id: SyncScheduler +title: SyncScheduler +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +:::note +🚧 This page is under construction +::: diff --git a/docs/docs/v2/api/classes/ZeduxNode.mdx b/docs/docs/v2/api/classes/ZeduxNode.mdx index 713b219b..3b6aa68d 100644 --- a/docs/docs/v2/api/classes/ZeduxNode.mdx +++ b/docs/docs/v2/api/classes/ZeduxNode.mdx @@ -6,7 +6,7 @@ title: ZeduxNode import useBaseUrl from '@docusaurus/useBaseUrl' import { Legend, Item, Link, Tabs, tab1, tab2 } from '@site/src/all' -This is the base class for every [graph node](../glossary#graph-node). The [AtomInstance class](./AtomInstance), [SelectorInstance class](./SelectorInstance.mdx), [Signal class](./Signal), and several other graph node types extend this class. +This is the base class for every [graph node](../glossary.mdx#graph-node). The [AtomInstance class](./AtomInstance), [SelectorInstance class](./SelectorInstance.mdx), [Signal class](./Signal), and several other graph node types extend this class. You never create this class directly and should never need to reference it directly except as a type. @@ -47,12 +47,13 @@ Full list of keys on the `NodeGenerics` (`G`) type generic: A tuple type. The parameters of this node. Only exists on [atom - instances](./AtomInstance) and [selector instances](./SelectorInstance.mdx). + instances](./AtomInstance.mdx) and [selector + instances](./SelectorInstance.mdx). The state type of this node. Can be anything. A recursive reference to the current node's full template type. `undefined` - if none. Only [atom instances](./AtomInstance) and [selector + if none. Only [atom instances](./AtomInstance.mdx) and [selector instances](./SelectorInstance.mdx) have templates.
@@ -71,7 +72,7 @@ A node's events can be listened to using [`node.on()`](#on). Every node has the - `newState` - The new state of the graph node. Can be anything. - `oldState` - The previous state of the graph node. Can be anything. - - `reasons` - An indefinitely-nested array of [reasons](/not-done?path=../types/EvaluationReason) why the state changed. This tracks the series of state changes through the dependency graph that led to the current node changing. If the node was updated directly, this will be undefined. + - `reasons` - An indefinitely-nested array of [reasons](../types/EvaluationReason.mdx) why the state changed. This tracks the series of state changes through the dependency graph that led to the current node changing. If the node was updated directly, this will be undefined. - `source` - The node that changed. Though the types don't reflect it yet, this will always be defined. - `type` - The string `"change"`. @@ -164,7 +165,7 @@ Every node has the following **readonly** properties: A string. The unique id of this node. Zedux always tries to make this somewhat human-readable for easier debugging. - For nodes that take params, the id is the combination of the node template's key and a deterministic stringification of the params. A node template's "key" can the string passed to the [`atom()`](../factories/atom) factory for atoms or the selector function's name for selectors See [`ecosystem.hash()`](./Ecosystem#hash) or [`node.params`](#params) for more details on how params are stringified. + For nodes that take params, the id is the combination of the node template's key and a deterministic stringification of the params. A node template's "key" can the string passed to the [`atom()`](../factories/atom.mdx) factory for atoms or the selector function's name for selectors See [`ecosystem.hash()`](./Ecosystem.mdx#hash) or [`node.params`](#params) for more details on how params are stringified. ```ts ecosystem.getNode(atom('a', null)).id // 'a' @@ -178,7 +179,7 @@ Every node has the following **readonly** properties: An array. The parameters passed to this node when it was created. - A reference to the raw, unserialized params that were used to create this atom instance. Only [atom instances](./AtomInstance) and [selector instances](./SelectorInstance.mdx) take params. This will be `undefined` for node types that don't take params (like signals). For nodes that can take params but weren't passed any (like singleton atoms), this will be an empty array. + A reference to the raw, unserialized params that were used to create this atom instance. Only [atom instances](./AtomInstance.mdx) and [selector instances](./SelectorInstance.mdx) take params. This will be `undefined` for node types that don't take params (like signals). For nodes that can take params but weren't passed any (like singleton atoms), this will be an empty array. ```ts const instanceA = useAtomInstance(myAtom, ['param 1', 'param 2']) @@ -203,7 +204,7 @@ Every node has the following **readonly** properties: useAtomInstance(myAtom, ['b', 'a']) ``` - The only exception to the serializable rule is other atom instances. That's right! You can pass an atom instance to another atom instance. You can then use [`instance.get()`](#get) or [`ecosystem.get(instance)`](./Ecosystem#get), [`injectAtomValue()`](../injectors/injectAtomValue.mdx), or any other dynamic injector to register a [dynamic graph dependency](../glossary#dynamic-graph-dependency) on the passed atom instance. + The only exception to the serializable rule is other atom instances. That's right! You can pass an atom instance to another atom instance. You can then use [`instance.get()`](#get) or [`ecosystem.get(instance)`](./Ecosystem#get), [`injectAtomValue()`](../injectors/injectAtomValue.mdx), or any other dynamic injector to register a [dynamic graph dependency](../glossary.mdx#dynamic-graph-dependency) on the passed atom instance. ```tsx live ecosystemId=AtomInstance-params resultVar=Shout version=2 const normalAtom = atom( @@ -236,13 +237,13 @@ function Shout() { src={useBaseUrl('/img/diagrams/status-machine.png')} /> - This is mostly for debugging, but it has some practical uses. For example, you can check that `node.status !== 'Destroyed'` when holding a reference to a node outside of React/atoms (and if it is, update your local reference using [`ecosystem.getNode()`](./Ecosystem#getnode)). + This is mostly for debugging, but it has some practical uses. For example, you can check that `node.status !== 'Destroyed'` when holding a reference to a node outside of React/atoms (and if it is, update your local reference using [`ecosystem.getNode()`](./Ecosystem.mdx#getnode)). - A reference to the template this node was created from. `undefined` if none. Only [atom instances](./AtomInstance) and [selector instances](./SelectorInstance.mdx) have templates. + A reference to the template this node was created from. `undefined` if none. Only [atom instances](./AtomInstance.mdx) and [selector instances](./SelectorInstance.mdx) have templates. - For atom instances, this will be the [atom template](./AtomTemplate). For selector instances, this will be the [selector template](../types/SelectorTemplate.mdx). + For atom instances, this will be the [atom template](./AtomTemplate.mdx). For selector instances, this will be the [selector template](../types/SelectorTemplate.mdx). @@ -271,10 +272,10 @@ Every node has the following methods: - Gets the current value of the node. Registers a [dynamic graph dependency](../glossary#dynamic-graph-dependency) on the node when called in [reactive contexts](../glossary#reactive-context). + Gets the current value of the node. Registers a [dynamic graph dependency](../glossary.mdx#dynamic-graph-dependency) on the node when called in [reactive contexts](../glossary.mdx#reactive-context). - Gets the current value of the node. Unlike [`node.get()`], `getOnce` does not register any graph dependencies, even when called in [reactive contexts](../glossary#reactive-context). + Gets the current value of the node. Unlike [`node.get()`], `getOnce` does not register any graph dependencies, even when called in [reactive contexts](../glossary.mdx#reactive-context). Attaches an event listener to the node. diff --git a/docs/docs/v2/api/components/AtomProvider.mdx b/docs/docs/v2/api/components/AtomProvider.mdx index cee988ca..16ce9c8b 100644 --- a/docs/docs/v2/api/components/AtomProvider.mdx +++ b/docs/docs/v2/api/components/AtomProvider.mdx @@ -160,4 +160,4 @@ AtomProvider accepts **either** an `instance` prop to provide a single atom inst - [The React Context walkthrough](../../../walkthrough/react-context.mdx) - [The `useAtomContext` hook](../hooks/useAtomContext.mdx) -- [The `useAtomInstance` hook](/not-done?path=../hooks/useAtomInstance) +- [The `useAtomInstance` hook](../hooks/useAtomInstance.mdx) diff --git a/docs/docs/v2/api/components/EcosystemProvider.mdx b/docs/docs/v2/api/components/EcosystemProvider.mdx index 89be727d..c7c4c796 100644 --- a/docs/docs/v2/api/components/EcosystemProvider.mdx +++ b/docs/docs/v2/api/components/EcosystemProvider.mdx @@ -11,7 +11,7 @@ import { EcosystemProvider } from '@zedux/react' A React component that provides an [ecosystem](../classes/Ecosystem) to a component tree. The provided ecosystem will take control of all atom usages below it. -You can pass either an existing [ecosystem](../classes/Ecosystem) via the `ecosystem` prop or any number of [EcosystemConfig properties](/not-done?path=../types/EcosystemConfig) via their respectively named props. +You can pass either an existing [ecosystem](../classes/Ecosystem) via the `ecosystem` prop or any number of [EcosystemConfig properties](../types/EcosystemConfig.mdx) via their respectively named props. When passing config properties, the EcosystemProvider will create an ecosystem for you. @@ -127,7 +127,7 @@ You must pass either an `ecosystem` prop or any combination of the ecosystem con - See [the EcosystemConfig type](/not-done?path=../types/EcosystemConfig) for all the other props and their types. The EcosystemConfig key names have a one-to-one mapping with props of this component. + See [the EcosystemConfig type](../types/EcosystemConfig.mdx) for all the other props and their types. The EcosystemConfig key names have a one-to-one mapping with props of this component. If the `id` prop is changed, Zedux will completely destroy the previous ecosystem and create a new one using the id and the current value of all the other EcosystemConfig props. Changing any other props besides `id` will have no effect unless `id` is also changed. @@ -139,6 +139,6 @@ You must pass either an `ecosystem` prop or any combination of the ecosystem con ## See Also - [The Ecosystems walkthrough](../../../walkthrough/ecosystems.mdx) -- [The `EcosystemConfig` type](/not-done?path=../types/EcosystemConfig) +- [The `EcosystemConfig` type](../types/EcosystemConfig.mdx) - [The `Ecosystem` class](../classes/Ecosystem.mdx) - [The `useEcosystem` hook](../hooks/useEcosystem.mdx) diff --git a/docs/docs/v2/api/factories/atom.mdx b/docs/docs/v2/api/factories/atom.mdx index 45c1c6be..8728a2b9 100644 --- a/docs/docs/v2/api/factories/atom.mdx +++ b/docs/docs/v2/api/factories/atom.mdx @@ -234,5 +234,5 @@ function App() { - [The `AtomTemplate` class](../classes/AtomTemplate.mdx) - [The `AtomApi` class](../classes/AtomApi.mdx) - [The `AtomConfig` type](../types/AtomConfig.mdx) -- [The Quick Start](../../../walkthrough/quick-start.mdx) +- [The Quick Start](/not-done?path=../../walkthrough/quick-start.mdx) - [The Configuring Atoms walkthrough](../../../walkthrough/configuring-atoms.mdx) diff --git a/docs/docs/v2/api/factories/createEcosystem.mdx b/docs/docs/v2/api/factories/createEcosystem.mdx index 7e453601..650e204d 100644 --- a/docs/docs/v2/api/factories/createEcosystem.mdx +++ b/docs/docs/v2/api/factories/createEcosystem.mdx @@ -47,7 +47,7 @@ const forSsr = createEcosystem({ ssr: true }) - Optional. An [EcosystemConfig](/not-done?path=../types/EcosystemConfig) object. + Optional. An [EcosystemConfig](../types/EcosystemConfig.mdx) object. All fields are optional. The `id` is just for debugging and does not have to be unique (though of course uniqueness is recommended). diff --git a/docs/docs/v2/api/factories/ion.mdx b/docs/docs/v2/api/factories/ion.mdx index 0808b438..5925596a 100644 --- a/docs/docs/v2/api/factories/ion.mdx +++ b/docs/docs/v2/api/factories/ion.mdx @@ -159,4 +159,4 @@ function App() { - [The AtomTemplate class](../classes/AtomTemplate.mdx) - [The SelectorInstance class](../classes/SelectorInstance.mdx) - [The AtomApi class](../classes/AtomApi.mdx) -- [The selectors walkthrough](../../../walkthrough/selectors.mdx) +- [The selectors walkthrough](/not-done?path=../../walkthrough/selectors.mdx) diff --git a/docs/docs/v2/api/hooks/useAtomInstance.mdx b/docs/docs/v2/api/hooks/useAtomInstance.mdx index 607a0fc9..28814198 100644 --- a/docs/docs/v2/api/hooks/useAtomInstance.mdx +++ b/docs/docs/v2/api/hooks/useAtomInstance.mdx @@ -142,15 +142,13 @@ There are a few ways to do this: A string. Default: `useAtomInstance`. Used for debugging to describe the reason for creating this graph edge. This default is usually fine. - A boolean. Default: `false`. Pass `subscribe: true` to make `useAtomInstance` create a [dynamic graph dependency](../glossary.mdx#dynamic-graph-dependency) instead. + A boolean. Default: `false`. Pass `true` to make `useAtomInstance` create a [dynamic graph dependency](../glossary.mdx#dynamic-graph-dependency) instead. A boolean. Default: `true`. Whether the component should suspend if the resolved atom has a [promise](../classes/AtomInstance.mdx#promise) set. Pass `suspend: false` to prevent this hook from triggering React suspense. - See the [React Hooks walkthrough](/not-done?path=/../../walkthrough/react-hooks.mdx) for more details. - The resolved [atom instance](../classes/AtomInstance.mdx) for the passed template + params combo. @@ -164,4 +162,3 @@ There are a few ways to do this: - [The `useAtomState` hook](./useAtomState.mdx) - [The `useAtomValue` hook](./useAtomValue.mdx) - [The `injectAtomInstance` injector](../injectors/injectAtomInstance.mdx) -- [The React Hooks walkthrough](/not-done?path=/../../walkthrough/react-hooks.mdx) diff --git a/docs/docs/v2/api/hooks/useAtomSelector.mdx b/docs/docs/v2/api/hooks/useAtomSelector.mdx index 8e423326..5fadf0fc 100644 --- a/docs/docs/v2/api/hooks/useAtomSelector.mdx +++ b/docs/docs/v2/api/hooks/useAtomSelector.mdx @@ -49,7 +49,7 @@ function Count() { - Required. Either a [selector function](../types/SelectorTemplate.mdx#atomselector) or an [atom selector config object](../types/SelectorTemplate.mdx#atomselectorconfig). + Required. Either a [selector function](../types/AtomSelector.mdx) or an [atom selector config object](../types/AtomSelectorConfig.mdx). See [the `SelectorTemplate` type](../types/SelectorTemplate.mdx) for more details. @@ -68,4 +68,4 @@ function Count() { ## See Also - [The `useAtomValue` hook](./useAtomValue.mdx) -- [The Selectors walkthrough](../../../walkthrough/selectors.mdx) +- [The Selectors walkthrough](/not-done?path=../../walkthrough/selectors.mdx) diff --git a/docs/docs/v2/api/hooks/useAtomState.mdx b/docs/docs/v2/api/hooks/useAtomState.mdx index 421f31ba..d3cb5f76 100644 --- a/docs/docs/v2/api/hooks/useAtomState.mdx +++ b/docs/docs/v2/api/hooks/useAtomState.mdx @@ -137,4 +137,3 @@ const [, { exports, only }] = useAtomState(myAtom) - [The `useAtomValue` hook](./useAtomValue.mdx) - [The `useAtomInstance` hook](./useAtomInstance.mdx) - [The `injectAtomState` injector](../injectors/injectAtomState.mdx) -- [The React Hooks walkthrough](/not-done?path=../../../walkthrough/react-hooks.mdx) diff --git a/docs/docs/v2/api/hooks/useAtomValue.mdx b/docs/docs/v2/api/hooks/useAtomValue.mdx index 75373dcb..ced281bf 100644 --- a/docs/docs/v2/api/hooks/useAtomValue.mdx +++ b/docs/docs/v2/api/hooks/useAtomValue.mdx @@ -174,4 +174,3 @@ const fromInstance = useAtomValue(instance) - [The `useAtomState` hook](./useAtomState.mdx) - [The `useAtomInstance` hook](./useAtomInstance.mdx) - [The `injectAtomValue` injector](../injectors/injectAtomValue.mdx) -- [The React Hooks walkthrough](/not-done?path=../../../walkthrough/react-hooks.mdx) diff --git a/docs/docs/v2/api/injectors/injectHydration.mdx b/docs/docs/v2/api/injectors/injectHydration.mdx index e5164604..b69009ad 100644 --- a/docs/docs/v2/api/injectors/injectHydration.mdx +++ b/docs/docs/v2/api/injectors/injectHydration.mdx @@ -13,7 +13,7 @@ An [injector](../glossary.mdx#injector) that retrieves data previously passed to If the atom has a [`hydrate`](../types/AtomConfig.mdx#hydrate) function configured, `injectHydration` will call it with the atom's hydration and return the result. Pass `{ transform: false }` to prevent this. -`injectHydration` is unique in that it has a side effect: When called, it prevents Zedux from [automatically hydrating](/not-done?path=../../advanced/persistence#automatic-hydration) the atom after its initial evaluation. Pass `{ intercept: false }` to prevent this. +`injectHydration` is unique in that it has a side effect: When called, it prevents Zedux from [automatically hydrating](../../advanced/persistence.mdx#automatic-hydration) the atom after its initial evaluation. Pass `{ intercept: false }` to prevent this. :::tip Simple atoms don't need this. Zedux automatically hydrates atoms after their initial evaluation. diff --git a/docs/docs/v2/api/injectors/injectSelf.mdx b/docs/docs/v2/api/injectors/injectSelf.mdx index f8e5e989..d263f34a 100644 --- a/docs/docs/v2/api/injectors/injectSelf.mdx +++ b/docs/docs/v2/api/injectors/injectSelf.mdx @@ -14,7 +14,7 @@ An [unrestricted injector](../glossary.mdx#unrestricted-injector) that returns t This gives you access to the atom instance object from within the atom itself, allowing you to read properties like the atom's [`.id`](../classes/AtomInstance.mdx#id) or even update, invalidate, or destroy the atom instance. Make sure you only call these methods in a callback, after the atom has been fully initialized. :::warning -The atom instance returned by `injectSelf` is not fully initialized on initial evaluation. It may not have a `.exports` or `.promise` set yet - that's the purpose of the atom evaluation! [Scoped atoms](/not-done?path=../../walkthrough/scoped-atoms.mdx) may not even have their real id determined yet. +The atom instance returned by `injectSelf` is not fully initialized on initial evaluation. It may not have a `.exports` or `.promise` set yet - that's the purpose of the atom evaluation! [Scoped atoms](../../walkthrough/scoped-atoms.mdx) may not even have their real id determined yet. The types reflect this by returning a `PartialAtomInstance`. On subsequent evaluations, or when used later in a closure, you may check if the atom has been fully initialized and cast it to an `AtomInstance` if so. ::: @@ -80,4 +80,4 @@ function App() { - [The `AtomInstance` class](../classes/AtomInstance.mdx) - [The `injectAtomInstance` injector](./injectAtomInstance.mdx) -- [The Atom Lifecycle walkthrough](/not-done?path=../../../walkthrough/atom-lifecycle.mdx) +- [The Atom Lifecycle walkthrough](../../../walkthrough/atom-lifecycle.mdx) diff --git a/docs/docs/v2/api/injectors/injectWhy.mdx b/docs/docs/v2/api/injectors/injectWhy.mdx index ea42acb6..effc87cd 100644 --- a/docs/docs/v2/api/injectors/injectWhy.mdx +++ b/docs/docs/v2/api/injectors/injectWhy.mdx @@ -5,7 +5,7 @@ title: injectWhy import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' -An [unrestricted injector](../glossary.mdx#unrestricted-injector) that returns an array of [reasons](/not-done?path=../types/EvaluationReason.mdx) that explain why the current atom is evaluating. +An [unrestricted injector](../glossary.mdx#unrestricted-injector) that returns an array of [reasons](../types/EvaluationReason.mdx) that explain why the current atom is evaluating. This is essentially an alias for the following: @@ -75,7 +75,7 @@ function App() { - An array of [EvaluationReasons](/not-done?path=../types/EvaluationReason.mdx) that explain why the current atom is evaluating. + An array of [EvaluationReasons](../types/EvaluationReason.mdx) that explain why the current atom is evaluating. Returns an empty array on the initial evaluation (and only on the initial evaluation). This can be used to determine if this is the first evaluation. But it's recommended to use [`injectSelf`](./injectSelf.mdx) instead: diff --git a/docs/docs/v2/api/types/AtomConfig.mdx b/docs/docs/v2/api/types/AtomConfig.mdx index 4c345fe0..66df6d7f 100644 --- a/docs/docs/v2/api/types/AtomConfig.mdx +++ b/docs/docs/v2/api/types/AtomConfig.mdx @@ -67,15 +67,14 @@ function App() { ## Interface - - {tab1(`interface AtomConfig`)} - {tab2(`interface AtomConfig { - dehydrate?: (state: State) => any - tags?: string[] - hydrate?: (dehydratedState: unknown) => State - ttl?: number - }`)} - +```ts +interface AtomConfig { + dehydrate?: (state: State) => any + tags?: string[] + hydrate?: (dehydratedState: unknown) => State + ttl?: number +} +``` @@ -97,7 +96,7 @@ function App() { hydrate: (dehydratedState: unknown) => State ``` - This is called before Zedux [automatically hydrates](/not-done?path=../../advanced/persistence#automatic-hydration) the atom or when [`injectHydration`](../injectors/injectHydration.mdx) is called without `{ transform: false }`. + This is called before Zedux [automatically hydrates](../../advanced/persistence.mdx#automatic-hydration) the atom or when [`injectHydration`](../injectors/injectHydration.mdx) is called without `{ transform: false }`. diff --git a/docs/docs/v2/api/types/AtomSelector.mdx b/docs/docs/v2/api/types/AtomSelector.mdx new file mode 100644 index 00000000..2d5ada2e --- /dev/null +++ b/docs/docs/v2/api/types/AtomSelector.mdx @@ -0,0 +1,112 @@ +--- +id: AtomSelector +title: AtomSelector +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +```ts +import type { AtomSelector } from '@zedux/react' +``` + +A function that takes an [ecosystem](../classes/Ecosystem.mdx) as its first parameter and returns a derived value. The return value is cached and the selector automatically reevaluates when its dependencies change. + +Selectors are lightweight, pure functions that derive state from atoms and other selectors. They're the easiest way to compose state in Zedux. + +## Example + +Simple selector: + +```ts +const getAdminUsers = ({ get }: Ecosystem) => + get(usersAtom).filter(user => user.role === 'admin') + +// Use the selector in a component: +function AdminList() { + const adminUsers = useAtomValue(getAdminUsers) + return ( +
    + {adminUsers.map(user => ( +
  • {user.name}
  • + ))} +
+ ) +} +``` + +Selector with params: + +```ts +const getUsersByRole = ({ get }: Ecosystem, role: string) => + get(usersAtom).filter(user => user.role === role) + +// TypeScript enforces that params are passed: +const adminUsers = useAtomValue(getUsersByRole, ['admin']) +const moderators = useAtomValue(getUsersByRole, ['moderator']) +``` + +Composing selectors: + +```ts +const getActiveUsers = ({ get }: Ecosystem) => + get(usersAtom).filter(user => user.isActive) + +const getActiveUserCount = ({ get }: Ecosystem) => get(getActiveUsers).length + +// Selectors can depend on other selectors +const summary = useAtomValue(getActiveUserCount) // 42 +``` + +## Unstable Results + +When a selector returns a different object reference every time it runs, Zedux's caching behavior will be useless - all dependents will reevaluate every time the selector runs. + +To fix this, convert the selector to a [selector config object](./AtomSelectorConfig.mdx) with a results comparator. + +```ts +const getActiveUserCount: AtomSelectorConfig = { + name: 'getActiveUserCount', + // this selector returns a new object reference every time it's called: + selector: ({ get }) => ({ length: get(getActiveUsers).length }), + resultsComparator: (prev, next) => prev.length === next.length, +} +``` + +## Interface + +```ts +type AtomSelector = ( + ecosystem: Ecosystem, + ...args: Params +) => State +``` + + + + The first parameter is always an [Ecosystem](../classes/Ecosystem.mdx) + instance. Use this to access atoms and other selectors via `get` or + `getNode`. + + + Any additional parameters after the ecosystem become the selector's + [params](../classes/SelectorInstance.mdx#params). When params are provided, + TypeScript will enforce that consumers pass matching params when using the + selector. + + + The selector can return any value. This value will be cached and returned to + all consumers of the selector. + + + +## See Also + +- [The selectors walkthrough](/not-done?path=../../walkthrough/selectors.mdx) +- [The `AtomSelectorConfig` type](./AtomSelectorConfig.mdx) +- [The `SelectorTemplate` type](./SelectorTemplate.mdx) +- [The `SelectorInstance` class](../classes/SelectorInstance.mdx) +- APIs that accept selectors: + - [`ecosystem.get`](../classes/Ecosystem.mdx#get) + - [`ecosystem.getNode`](../classes/Ecosystem.mdx#getnode) + - [The `useAtomValue` hook](../hooks/useAtomValue.mdx) + - [The `injectAtomValue` injector](../injectors/injectAtomValue.mdx) diff --git a/docs/docs/v2/api/types/AtomSelectorConfig.mdx b/docs/docs/v2/api/types/AtomSelectorConfig.mdx new file mode 100644 index 00000000..358ecee4 --- /dev/null +++ b/docs/docs/v2/api/types/AtomSelectorConfig.mdx @@ -0,0 +1,139 @@ +--- +id: AtomSelectorConfig +title: AtomSelectorConfig +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +```ts +import type { AtomSelectorConfig } from '@zedux/react' +``` + +An object that defines a [selector](./AtomSelector.mdx) plus additional configuration for controlling when the selector reevaluates and how results are compared. + +[Basic selector functions](./AtomSelector.mdx) work well for most cases. Selector config objects are mainly for selectors that don't return a stable result. + +## Example + +Basic usage with a name: + +```ts +const getAdminUsers: AtomSelectorConfig = { + name: 'getAdminUsers', + selector: ({ get }) => get(usersAtom).filter(user => user.role === 'admin'), +} + +const adminUsers = useAtomValue(getAdminUsers) +``` + +With a results comparator to prevent unnecessary updates: + +```ts +const getUserSummary: AtomSelectorConfig = { + name: 'getUserSummary', + // this selector returns a new object reference every time it's called, + // so we need to compare the results to prevent unnecessary updates + selector: ({ get }) => ({ + total: get(usersAtom).length, + active: get(usersAtom).filter(u => u.isActive).length, + }), + resultsComparator: (prev, next) => + prev.total === next.total && prev.active === next.active, +} + +// Components won't rerender if the counts haven't changed, +// even if the users array reference changes +const summary = useAtomValue(getUserSummary) +``` + +With an args comparator to control param comparison (only works in React hooks): + +```ts +const searchUsers: AtomSelectorConfig = { + name: 'searchUsers', + selector: ({ get }, query: string) => + get(usersAtom).filter(u => u.name.includes(query)), + argsComparator: (newArgs, oldArgs) => { + // Only reevaluate if query changed by more than just whitespace + return newArgs[0].trim() === oldArgs[0].trim() + }, +} + +const results = useAtomValue(searchUsers, ['john']) +``` + +## Interface + +```ts +interface AtomSelectorConfig { + argsComparator?: ( + newArgs: NoInfer, + oldArgs: NoInfer + ) => boolean + name?: string + resultsComparator?: ( + newResult: NoInfer, + oldResult: NoInfer + ) => boolean + selector: AtomSelector +} +``` + + + + Optional. A function that receives the params from the last selector run and the latest arguments passed to the selector. Return `true` to prevent the selector from reevaluating, or `false` to allow it. + + ```ts + argsComparator?: (newArgs: Params, oldArgs: Params) => boolean + ``` + + If this returns `false` and the params create a different [hash](../classes/ZeduxNode.mdx#params), the selector will reevaluate. + + This option is only respected by Zedux's React hooks and only runs after the selector has already evaluated once. Its purpose is to prevent excess React rerenders from running the selector unnecessarily. This option is a noop when passing selectors to other Zedux APIs like [`ecosystem.get`](../classes/Ecosystem.mdx#get), [`ecosystem.getNode`](../classes/Ecosystem.mdx#getnode), or [`injectAtomValue`](../injectors/injectAtomValue.mdx). + + + + Optional. A string that will be used to create user-friendly [ids](../classes/SelectorInstance.mdx#id) for the selector. This takes precedence over the [`selector`](#selector) function's name. + + ```ts + name?: string + ``` + + This is useful when programmatically generating selectors, or if you prefer setting the [`selector`](#selector) field to anonymous arrow functions. + + + + Optional. A function that receives the previous and latest return values of the selector. Return `true` if the results should be considered "equal". + + ```ts + resultsComparator?: (newResult: State, oldResult: State) => boolean + ``` + + When a selector returns "equivalent" values on subsequent reevaluations, the latest result is discarded, the previous result remains cached, and consumers of the selector are not notified of any change (since there wasn't one). + + + + Required. The actual [AtomSelector](./AtomSelector.mdx) function to run. + + ```ts + selector: AtomSelector + ``` + + :::note + `AtomSelectorConfig` objects are cached by object reference, not by their `selector` function reference. + ::: + + + + +## See Also + +- [The selectors walkthrough](/not-done?path=../../walkthrough/selectors.mdx) +- [The `AtomSelectorConfig` type](./AtomSelectorConfig.mdx) +- [The `SelectorTemplate` type](./SelectorTemplate.mdx) +- [The `SelectorInstance` class](../classes/SelectorInstance.mdx) +- APIs that accept selector config objects: + - [`ecosystem.get`](../classes/Ecosystem.mdx#get) + - [`ecosystem.getNode`](../classes/Ecosystem.mdx#getnode) + - [The `useAtomValue` hook](../hooks/useAtomValue.mdx) + - [The `injectAtomValue` injector](../injectors/injectAtomValue.mdx) diff --git a/docs/docs/v2/api/types/DehydrationFilter.mdx b/docs/docs/v2/api/types/DehydrationFilter.mdx new file mode 100644 index 00000000..d32ae52b --- /dev/null +++ b/docs/docs/v2/api/types/DehydrationFilter.mdx @@ -0,0 +1,10 @@ +--- +id: DehydrationFilter +title: DehydrationFilter +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +:::note +🚧 This page is under construction +::: diff --git a/docs/docs/v2/api/types/EcosystemConfig.mdx b/docs/docs/v2/api/types/EcosystemConfig.mdx new file mode 100644 index 00000000..0c3716a7 --- /dev/null +++ b/docs/docs/v2/api/types/EcosystemConfig.mdx @@ -0,0 +1,10 @@ +--- +id: EcosystemConfig +title: EcosystemConfig +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +:::note +🚧 This page is under construction +::: diff --git a/docs/docs/v2/api/types/EvaluationReason.mdx b/docs/docs/v2/api/types/EvaluationReason.mdx new file mode 100644 index 00000000..829e15a7 --- /dev/null +++ b/docs/docs/v2/api/types/EvaluationReason.mdx @@ -0,0 +1,10 @@ +--- +id: EvaluationReason +title: EvaluationReason +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +:::note +🚧 This page is under construction +::: diff --git a/docs/docs/v2/api/types/NodeFilter.mdx b/docs/docs/v2/api/types/NodeFilter.mdx new file mode 100644 index 00000000..15de240a --- /dev/null +++ b/docs/docs/v2/api/types/NodeFilter.mdx @@ -0,0 +1,10 @@ +--- +id: NodeFilter +title: NodeFilter +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +:::note +🚧 This page is under construction +::: diff --git a/docs/docs/v2/api/types/SelectorTemplate.mdx b/docs/docs/v2/api/types/SelectorTemplate.mdx index 81c29210..6b7d60fb 100644 --- a/docs/docs/v2/api/types/SelectorTemplate.mdx +++ b/docs/docs/v2/api/types/SelectorTemplate.mdx @@ -6,184 +6,64 @@ title: SelectorTemplate import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' ```ts -import type { - AtomSelector, - AtomSelectorConfig, - SelectorTemplate, -} from '@zedux/react' +import type { SelectorTemplate } from '@zedux/react' ``` -Selectors are the lightweight champions of the Zedux world. They're easy to create and use. They have a small footprint and give you a quick way to tap into Zedux's reactive power. +A union type that encompasses both [AtomSelector](./AtomSelector.mdx) functions and [AtomSelectorConfig](./AtomSelectorConfig.mdx) objects. This type is used by all Zedux APIs that accept selectors. -Unlike [atom templates](../classes/AtomTemplate.mdx), which are instances of a class, "selector templates" in Zedux are just plain functions or objects that define a selection (aka derivation) operation. +In other words, anywhere you can pass a selector function, you can also pass a selector config object. -The `SelectorTemplate` type is essentially just this: +## Example -```ts -type SelectorTemplate = AtomSelector | AtomSelectorConfig -``` - -This type is used by all Zedux APIs that accept selectors. In other words, anywhere you can pass a [selector](#atomselector) function, you can also pass an [`AtomSelectorConfig`](#atomselectorconfig) object. - -## `AtomSelector` - -A function that takes an [ecosystem](../classes/Ecosystem.mdx) as its first parameter and can return absolutely anything. +Using a selector function: ```ts const getAdminUsers = ({ get }: Ecosystem) => get(usersAtom).filter(user => user.role === 'admin') -``` - -The return value will be cached, using the selector function reference as the cache key. This uses a `WeakMap` internally. - -Any [`ecosystem.get`](../classes/Ecosystem.mdx#get) calls inside the selector will automatically create a [dynamic graph dependency](../glossary.mdx#dynamic-graph-dependency) between the selector and the retrieved node. The selector will automatically reevaluate whenever the retrieved node's state changes. - -Any [`ecosystem.getNode`](../classes/Ecosystem.mdx#getnode) calls inside the selector will automatically create a [static graph dependency](../glossary.mdx#static-graph-dependency) between the selector and the retrieved node. These dependencies will not cause the selector to reevaluate. The main use for this is to prevent the retrieved node from being destroyed while the selector still needs it. - -Selectors can take other parameters too. Any parameters after the first `ecosystem` parameter will become the selector's [params](../classes/SelectorInstance.mdx#params). - -```ts -const getUsersByRole = ({ get }, role: string) => - get(usersAtom).filter(user => user.role === role) -// since the `role` param is required, consumers must pass it: -const adminUsers = useAtomValue(getUsersByRole) // ❌ Error! -const adminUsers = useAtomValue(getUsersByRole, ['admin']) // ✅ +// Pass the function directly: +const adminUsers = useAtomValue(getAdminUsers) ``` -TypeScript will enforce that consumers pass the correct params when using the selector via any hooks, injectors, or ecosystem methods. - -## `AtomSelectorConfig` - -An object that defines a selector plus some additional configuration: +Using a selector config object: ```ts -{ argsComparator?, name?, resultsComparator, selector? } -``` - - - - Optional. A function that receives the params from the last selector run and the latest arguments passed to the selector. It should compare the two and return a boolean indicating whether they should be considered "equal". - - Return true to prevent the selector from reevaluating. - - Return false to allow the selector to reevaluate. In this case, if the params create a different [hash](../classes/ZeduxNode.mdx#params), the selector will be reevaluated. - - This config option is only respected by Zedux's React hooks and only runs after the selector has already evaluated once and is about to reevaluate. Its use is to prevent excess React rerenders from running the selector unnecessarily. This option is a noop when passing selectors to other Zedux APIs like [`ecosystem.get`](../classes/Ecosystem.mdx#get), [`ecosystem.getNode`](../classes/Ecosystem.mdx#getnode), or [`injectAtomValue`](../injectors/injectAtomValue.mdx). - - - - Optional. A string that will be used to create user-friendly [ids](../classes/SelectorInstance.mdx#id) for the selector. This will take precedence over the [`selector`](#selector) function's name. - - This can be useful when programmatically generating selectors, or if you prefer setting the [`selector`](#selector) field to anonymous arrow functions. - - - - Optional. A function that receives the previous and latest return values of the selector. Return true if the results should be considered "equal". - - When a selector returns "equivalent" values on subsequent reevaluations, the latest result is discarded, the previous result remains cached, and consumers of the selector are not notified of any change (since there wasn't one). - - - - Required. The actual [selector function](#atomselector) to run. - - :::note - `AtomSelectorConfig` objects are cached by object reference, not by their `selector` function reference. - ::: - - - - -## Inline Selectors - -Since selectors are just functions, it's easy to create them on the fly: +const getAdminUsersConfig: AtomSelectorConfig = { + name: 'getAdminUsers', + selector: ({ get }) => get(usersAtom).filter(user => user.role === 'admin'), + resultsComparator: (prev, next) => prev.length === next.length, +} -```ts -const adminUsers = useAtomValue(({ get }) => - get(usersAtom).filter(user => user.role === 'admin') -) +// Pass the config object: +const adminUsers = useAtomValue(getAdminUsersConfig) ``` -:::note -This works for `AtomSelectorConfig` objects too: +Both work in any API that accepts a `SelectorTemplate`: ```ts -const adminUsers = useAtomValue( - { - name: 'getAdminUsersForUsersTable', - selector: getUsersByRole, - }, - ['admin'] -) -``` - -::: - -This is supported, and you typically shouldn't need to worry about it. But since the selector function reference is recreated every render, Zedux has to do extra work to handle this. +// In hooks: +const value = useAtomValue(selectorOrConfig) -Because of this, for larger apps, it's generally recommended to define selectors outside of components, atoms, or other selectors. +// In ecosystem methods: +const value = ecosystem.get(selectorOrConfig) -This is also useful for debugging, since Zedux uses the selector's function name to generate a user-friendly id for each [instance](../classes/SelectorInstance.mdx) of the selector. - -## Refactoring to Ions - -Selectors are designed for simple, pure calculations. They always reevaluate when any of their dependencies update. Sometimes you need more control over when/how often a selector evaluates. - -It's common to refactor a selector to an ion for better control over memoization details via [`injectMemo`](../injectors/injectMemo.mdx) or even combining [`injectEffect`](../injectors/injectEffect.mdx) with async tools like RxJS to throttle or buffer updates. - -```tsx -// Before (using an atom selector): -const getUserName = ({ get }: Ecosystem) => get(userAtom).name - -// After (using an ion): -const userNameAtom = ion('userName', ({ get }) => get(userAtom).name) - -// Consumers of the selector require minimal changes: -const userName = useAtomValue(getUserName) // Before -const userName = useAtomValue(userNameAtom) // After +// In injectors: +const value = injectAtomValue(selectorOrConfig) ``` -This is one of the primary reasons why Zedux v2 deprecated many selector-specific APIs like [`useAtomSelector`](../hooks/useAtomSelector.mdx) and [`ecosystem.select`](../classes/Ecosystem.mdx#select). - ## Interface -The `SelectorTemplate` type itself is just a union of the `AtomSelector` and `AtomSelectorConfig` types: - ```ts -type AtomSelector = ( - ecosystem: Ecosystem, - ...args: Params -) => State - -interface AtomSelectorConfig { - argsComparator?: ( - newArgs: NoInfer, - oldArgs: NoInfer - ) => boolean - name?: string - resultsComparator?: ( - newResult: NoInfer, - oldResult: NoInfer - ) => boolean - selector: AtomSelector -} - type SelectorTemplate = | AtomSelector | AtomSelectorConfig ``` - - {tab1(`interface AtomConfig`)} - {tab2(`interface AtomConfig { - dehydrate?: (state: State) => any - tags?: string[] - hydrate?: (dehydratedState: unknown) => State - ttl?: number -}`)} - - ## See Also +- [The selectors walkthrough](/not-done?path=../../walkthrough/selectors.mdx) +- [The `AtomSelector` type](./AtomSelector.mdx) +- [The `AtomSelectorConfig` type](./AtomSelectorConfig.mdx) - [The `SelectorInstance` class](../classes/SelectorInstance.mdx) -- [The selectors walkthrough](../../../walkthrough/selectors.mdx) +- [`useAtomValue` hook](../hooks/useAtomValue.mdx) +- [`ecosystem.get`](../classes/Ecosystem.mdx#get) diff --git a/docs/docs/v2/api/types/Transaction.mdx b/docs/docs/v2/api/types/Transaction.mdx new file mode 100644 index 00000000..597390a5 --- /dev/null +++ b/docs/docs/v2/api/types/Transaction.mdx @@ -0,0 +1,10 @@ +--- +id: Transaction +title: Transaction +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +:::note +🚧 This page is under construction +::: diff --git a/docs/docs/v2/api/utils/inject.mdx b/docs/docs/v2/api/utils/inject.mdx index 051d0c98..d5621abf 100644 --- a/docs/docs/v2/api/utils/inject.mdx +++ b/docs/docs/v2/api/utils/inject.mdx @@ -219,4 +219,4 @@ When used with atoms, remember that `inject` returns the atom instance itself, n ## See Also - [`ecosystem.withScope`](../classes/Ecosystem.mdx#withscope) -- [The scoped atoms walkthrough](/not-done?path=../../walkthrough/scoped-atoms.mdx) +- [The scoped atoms walkthrough](../../walkthrough/scoped-atoms.mdx) diff --git a/docs/docs/v2/walkthrough/atom-lifecycle.mdx b/docs/docs/v2/walkthrough/atom-lifecycle.mdx new file mode 100644 index 00000000..21c9e207 --- /dev/null +++ b/docs/docs/v2/walkthrough/atom-lifecycle.mdx @@ -0,0 +1,10 @@ +--- +id: atom-lifecycle +title: Atom Lifecycle +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +:::note +🚧 This page is under construction +::: diff --git a/docs/docs/v2/walkthrough/scoped-atoms.mdx b/docs/docs/v2/walkthrough/scoped-atoms.mdx new file mode 100644 index 00000000..f11474a6 --- /dev/null +++ b/docs/docs/v2/walkthrough/scoped-atoms.mdx @@ -0,0 +1,10 @@ +--- +id: scoped-atoms +title: Scoped Atoms +--- + +import { Legend, Item, Link, Tabs, Ts, tab1, tab2 } from '@site/src/all' + +:::note +🚧 This page is under construction +::: diff --git a/docs/sidebars.js b/docs/sidebars.js index f3697d20..d6e07cde 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -227,7 +227,12 @@ module.exports = { { type: 'category', label: 'Types', - items: ['v2/api/types/AtomConfig', 'v2/api/types/SelectorTemplate'], + items: [ + 'v2/api/types/AtomConfig', + 'v2/api/types/AtomSelector', + 'v2/api/types/AtomSelectorConfig', + 'v2/api/types/SelectorTemplate', + ], }, { type: 'category',