Skip to main content

V2 Migration Guide

Zedux v2 introduces lots of new features and several breaking changes. This guide will give a basic introduction to each new feature and highlight some important breaking changes and migration tips. The full list of changes is at the end.

If you know the gist of Zedux v2, you can skip to the end for the full migration details.

React 19

Zedux v2 works best with React 19. Scoped atoms require React 19's new use util for hooking into React context. And we removed some old code for working around React 18's useId StrictMode bug.

This means you can use Zedux v2 in React 18 if you

  • disable React StrictMode
  • don't use scoped atoms (or call ecosystem.withScope yourself, but that can be tedious)

Upgrade React to v19 to get those features back.

Feature: Signals

Stores no longer exist in the @zedux/atoms or @zedux/react package by default. They're replaced with signals.

Live Sandbox
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
// const usernameAtom = atom('username', '')

const formAtom = atom('form', () => {
// const usernameInstance = injectAtomInstance(usernameAtom)
const usernameSignal = injectSignal('')
const passwordSignal = injectSignal('')

const signal = injectMappedSignal({
// atom instances _are_ signals:
// username: usernameInstance,
username: usernameSignal,
password: passwordSignal,
})

return api(signal).setExports({ mutate: (...args) => signal.mutate(...args) })
})

function Form() {
const [{ username, password }, { mutate }] = useAtomState(formAtom)

return (
<form
onSubmit={event => {
event.preventDefault()
console.log('submitted!', { username, password })
}}
>
<input
onChange={event =>
mutate(state => {
state.username = event.target.value
})
}
placeholder="Username"
value={username}
/>
<input
onChange={event =>
mutate(state => {
state.password = event.target.value
})
}
placeholder="Password"
/>
<button>Submit</button>
</form>
)
}

Atoms are signals! Try swapping in the commented-out usernameAtom for the usernameSignal. Behavior is the same.

Signals can be created anywhere via ecosystem.signal:

const signal = ecosystem.signal('some state')

Events

All graph nodes emit events. Signals have several built-in events:

  • change - fires on state change.
  • cycle - fires when the signal's lifecycle status changes (e.g. from Initializing to Active or Active to Destroyed).
  • mutate - fires when transactions are generated from a signal.mutate call.

Atoms also have a few atom-specific built-in events:

  • invalidate - fires when atomInstance.invalidate() is called.
  • promiseChange - fires when the atom's promise reference changes.

Signals can also be given custom, typed events. These types propagate to all mapped signals and atoms the signal is composed in.

Live Sandbox
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
const injectListener = (signal, event, callback) =>
injectEffect(() => signal.on(event, callback), [])

const eventuallyUpdatingAtom = atom('eventuallyUpdating', () => {
const signal = injectSignal(0, {
events: {
// use `As<MyType>` to type events:
updateLater: As<number>,
},
})

injectListener(signal, 'updateLater', ms =>
setTimeout(() => signal.set(state => state + 1), ms)
)

injectEffect(() => {
const cleanup = signal.on('change', event => {
console.log('state changed')
})

return cleanup
}, [])

return signal // the atom inherits the returned signal's events
})

function App() {
const eventuallyUpdatingInstance = useAtomInstance(eventuallyUpdatingAtom)
const count = useAtomValue(eventuallyUpdatingInstance)

useEffect(() => {
// this is fully typed:
const cleanup = eventuallyUpdatingInstance.on('updateLater', ms => {
console.log('saw updateLater', ms)
})

return cleanup
}, [eventuallyUpdatingInstance])

return (
<div>
<span>Count: {count}</span>
<button
onClick={() => eventuallyUpdatingInstance.send('updateLater', 1000)}
>
Update Later
</button>
</div>
)
}

Mutations

Signals have proxy-powered, transaction-translated mutations:

Live Sandbox
123456789101112131415161718192021
const ecosystem = createEcosystem()

const signal = ecosystem.signal({
foo: 'bar',
baz: [{ whateverIsAfterBaz: 1 }],
})

signal.on('mutate', transactions => {
// uncomment this and open browser console:
// console.log('got transactions:', transactions)
})

signal.mutate(state => {
state.foo = 'bar none'
})

signal.mutate(state => {
state.baz[0].whateverIsAfterBaz++
})

const state = signal.get()

Why Signals?

Apart from these new features, the main reason we decided to switch to signals is that stores are so different from atoms. Storing state, reading state, and reacting to state updates are all different.

Signals, on the other hand, use the same exact graph-based paradigm for reactivity. As a result, Zedux's API surface area is essentially halved.

The switch naturally removes several edge cases with synchronizing stores and atoms. It also lets us remove so much code that all these new features are free, bundle-size-wise, and then some. The minified build of Zedux v2 is a few kb smaller than v1.

For more info, see the original signals spec.

Feature: Ecosystem Events

The old plugin system is gone, replaced with "ecosystem events". Simply call ecosystem.on to register a listener

Live Sandbox
12345678910111213141516171819202122232425262728293031323334353637
const childCountAtom = atom('childCount', 0, { ttl: 0 })
const parentCountAtom = atom('parentCount', 0, { ttl: 0 })

function Child() {
const [count, setCount] = useAtomState(childCountAtom)

return (
<div>
<span>Child Count: {count}</span>
<button onClick={() => setCount(state => state + 1)}>
Increment Child
</button>
</div>
)
}

function App() {
const ecosystem = useEcosystem()
const cleanupRef = useRef()

if (!cleanupRef.current) {
// uncomment this and open browser console:
// cleanupRef.current = ecosystem.on(event => console.log(event))
}

const [count, setCount] = useAtomState(parentCountAtom)

return (
<div>
<span>Parent Count: {count}</span>
<button onClick={() => setCount(state => state + 1)}>
Increment Parent
</button>
<Child />
</div>
)
}

Plugin Migration

Full example of migrating a logging plugin to the new format:

// before:
const loggingPlugin = new ZeduxPlugin({
initialMods: ['stateChanged'],

registerEcosystem: ecosystem => {
const subscription = ecosystem.modBus.subscribe({
effects: ({ action }) => {
if (action.type === ZeduxPlugin.actions.stateChanged.type) {
console.log(
'node state updated',
action.oldState,
'->',
action.newState
)
}
},
})

return () => subscription.unsubscribe()
},
})

myEcosystem.registerPlugin(loggingPlugin) // register
myEcosystem.unregisterPlugin(loggingPlugin) // unregister

// after:
const cleanup = myEcosystem.on('change', event => {
console.log('node state updated', event.oldState, '->', event.newState)
})

Feature: Scoped Atoms

info

This feature requires React 19

The new inject util creates "scoped" atoms. Scoped atoms can only be initialized/retrieved in a scoped context:

  • in any React component
  • in an ecosystem.withScope callback

inject retrieves provided atoms and React contexts.

Live Sandbox
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
// it's recommended to default React contexts to `undefined` so Zedux can see if
// they weren't provided.
const reactContext = createContext<undefined | string>(undefined)

const contextAtom = atom('context', (initialState: number) => initialState)

const scopedAtom = atom(
'scoped',
() => {
const label = inject(reactContext)
const value = injectAtomValue(inject(contextAtom))

return `${label}: ${value}`
},
{ ttl: 0 }
) // it's highly recommended to set ttl for scoped atoms

function Child() {
const text = useAtomValue(scopedAtom)
const contextInstance = useAtomContext(contextAtom)

return (
<div>
<span>{text}</span>
<button onClick={() => contextInstance.set(state => state + 1)}>
Increment context
</button>
</div>
)
}

function App() {
const contextInstance1 = useAtomInstance(contextAtom, [1])
const contextInstance2 = useAtomInstance(contextAtom, [100])

return (
<reactContext.Provider value="Value from React context">
<AtomProvider instance={contextInstance1}>
<Child />
</AtomProvider>
<AtomProvider instance={contextInstance2}>
<Child />
</AtomProvider>
</reactContext.Provider>
)
}

Other New Features

  • untrack - A new top-level export for bumping out of a reactive context.
  • When called with no deps, injectMemo now automatically tracks any signal usages in the callback and reactively updates when they change, causing the injecting atom to reevaluate.
  • ecosystem.withScope - Runs a callback in a scoped context with the passed scope.
  • injectCallback now wraps the callback in ecosystem.withScope (in addition to ecosystem.batch) if the injecting atom is scoped.
  • Atom APIs wrap exports in ecosystem.batch and, if the atom that's evaluating when api() is called is scoped, ecosystem.withScope. Disable this by passing { wrap: false } as the second parameter to api.setExports and api.addExports.

Breaking Changes

Atom Getters

"Atom getters" are now just the function properties on the ecosystem. In v1, the ecosystem's get and getInstance function properties were used to specifically avoid registering graph dependencies. Now they do register graph dependencies when called in reactive contexts.

To avoid registering graph dependencies, there are several new features:

import { Ecosystem, untrack } from '@zedux/react'

function maybeCalledReactively() {
return rootEcoSystem.get(myAtom)
}

// the ecosystem itself is now the first argument to selectors/ions:
function myExampleSelector((ecosystem: Ecosystem) => {
ecosystem.get(myAtom) // registers a dynamic dependency on myAtom
ecosystem.getNode(myAtom) // registers a static dependency on myAtom

ecosystem.getOnce(myAtom) // doesn't register anything
ecosystem.getNodeOnce(myAtom) // doesn't register anything

untrack(() => ecosystem.get(myAtom)) // doesn't register

let myAtomVal = maybeCalledReactively() // registers

myAtomVal = untrack(() => maybeCalledReactively()) // doesn't register
})

While using the old AtomGetters type will still work for typing the first argument of atom selectors, it's deprecated. Prefer Ecosystem instead - we'll probably require it in v3.

Stores

Stores now live in the @zedux/stores package. They are not deprecated, you can still use them. But it's recommended to use signals when possible 'cause they're better. And because of that, we may officially deprecate stores someday. That's a maybe, but keep it in mind.

The types for store atoms do have a few breaking changes:

  • AnyAtomInstance is renamed to AnyStoreAtomInstance
  • AnyAtomTemplate is renamed to AnyStoreAtomTemplate

Several other types in the stores package, like most of the Atom*Type helpers, are deprecated. See below for the full list.

injectEffect Order

In v1, injectEffect callbacks would always run in the next event loop cycle (via setTimeout) unless another event, e.g. a state update, triggered a scheduler flush. This led to confusing event ordering in some cases, especially in tests.

Zedux now runs effects as soon as it's "safe" to do so.

  • When atoms are initialized during a React component render, effects now always run in a microtask.
  • When atoms are initialized or evaluate outside React, all queued effects run immediately before the top-level ecosystem.get* call returns.

This is a big improvement when working with atoms outside React, especially in tests, as before you couldn't be sure if manual subscriptions were registered yet when using an atom. Now you can.

This ordering change may break effects that were expecting other synchronous code to run between atom initialization and the deferred effect.

const exampleAtom = atom('example', () => {
const store = injectStore(0)

injectEffect(() => {
console.log(store.getState()) // "1" in v1, "0" in v2
}, []) // adding `store.getState()` as a dep would fix this in v2

return store
})

const exampleInstance = ecosystem.getInstance(exampleAtom)
exampleInstance.setState(1)

For example, we had some test code awaiting an already-resolved promise right after initializing an atom. That microtask was expected to run before the effect (which was scheduled with setTimeout in v1) but in v2 it doesn't. In this case, the effect needed to explicitly await the asynchronous operation.

Hydration

Automatic hydration works almost the same in v2, with one key difference: Hydrations aren't "consumed". This means they're kept in memory (in ecosystem.hydration) and will be reused if the atom is destroyed and recreated.

To prevent this, you can hook into the cycle ecosystem event and delete hydrations yourself.

const cleanup = ecosystem.on('cycle', event => {
// you can check specific ids or template keys (`event.source.template.key`):
if (event.source?.id && ecosystem.hydration[event.source.id]) {
// you are free to mutate this object:
delete ecosystem.hydration[event.source.id]
// you can call `cleanup` after all expected nodes have been hydrated
}
})

Usually you won't need to do this. It's just an escape hatch if you have particularly heavy hydration data.

For manual hydration, injectHydration is the new kid on the block.

Previously, store hydration was tied to individual injectStore calls. This made hydration difficult when there are multiple hydration sources (e.g. localStorage, URL state, and server snapshot) or each store only controlled part of the atom's state.

injectHydration gives you full control. It will be usable in combination with other hydrators so you can determine priority when manually hydrating, especially in "local-first" setups.

const multiSourcedAtom = atom('multiSourced', () => {
const fromServer = injectHydration<string>()

// two example third-party injectors:
const fromLocalStorage = injectLocalStorageHydration<string>()
const fromUrl = injectUrlHydration<string>()

const signal = injectSignal(
fromUrl ?? fromLocalStorage ?? fromServer ?? 'the default value'
)

return signal
})

As always, these injectors can be consolidated into a single custom injector:

const signal = injectLocalFirstSignal('the default value')

Manual hydration is recommended in this case of having multiple third-party hydrators, as otherwise you have to rely on listener registration order to determine priority. With only one third-party hydrator, automatic hydration is fine, though it will result in three total initial evaluations.

Id Generation

Zedux node ids are now more readable and consistent. Everything but atoms now uses consistent id @ prefixes. See the jsdoc for Ecosystem#makeId for the full list.

The default id generator also doesn't add any randomized hashes to ids in v2. Most apps shouldn't need those, so they were unnecessary overhead. Ids are still guaranteed to be unique inside the ecosystem. This default is ideal for testing too, since ids are now predictable.

If a uniqueness guarantee is needed outside the ecosystem, e.g. when working with multiple ecosystems (not common), you can now supply your own id generator. Just pass a makeId function to createEcosystem. It will receive the id prefix and should return a unique string.

For example:

const ecosystem = createEcosystem({
makeId: function (...args) {
return `${Ecosystem.prototype.makeId.apply(this, args)}-${uuid()}`
},
})

This new id generation will likely break any code that checked for exact id matches. If you have lots of code relying on this or snapshot tests capturing the old ids, you can get them back by supplying a custom id generator. This should do:

const ecosystem = createEcosystem({
makeId: function (nodeType, context, suffix) {
const prefix = nodeType === 'selector' ? '@@selector-' : ''

const content =
nodeType === 'component' && context === 'unknown'
? 'rc'
: nodeType === 'listener'
? 'no'
: nodeType === 'selector' && context === 'unknown'
? 'unnamed'
: (context as GraphNode)?.id ?? context ?? ''

const uniqueId =
suffix === ''
? ''
: suffix
? `-${suffix}`
: `-${++this.idCounter}${Math.random().toString(36).slice(2, 8)}`

return `${prefix}${content}${uniqueId}`
},
})

Other Changes

ecosystem.findAll now returns an array instead of an object keyed by node id. This is to make it easier to sort/filter/map/reduce the returned list yourself.

In particular, the default filtering logic is only designed for the default id generation. Now that you can easily customize ids with makeId, we need an API that lends itself to custom filtering of those ids better. Simply adding the ability to chain array operations directly off the .findAll() call is really all we need. It also makes manual dehydration easier:

// custom dehydration example
ecosystem
.findAll('@atom')
.filter(myCustomFilter)
.reduce((obj, node) => ({ ...obj, [node.id]: node.get() }), {})

Migrating to Signals

To upgrade to v2 initially, you can simply replace most @zedux/react imports with @zedux/stores:

// before:
import { api, atom, injectMemo, injectStore } from '@zedux/react'

// after:
import { injectMemo } from '@zedux/react'
import { api, atom, injectStore } from '@zedux/stores'

For bigger apps, it's recommended to migrate incrementally to signals. For most store-based atoms it will be as simple as this:

// before:
import { api, atom, injectStore } from '@zedux/stores'

// after:
import { api, atom, injectSignal } from '@zedux/react'

// replace `injectStore` with `injectSignal`
// replace `store.getState` with `signal.get`
// replace `store.setState` with `signal.set`
// replace `store.setStateDeep` with `signal.mutate`

Some apps may be able to easily move directly to signals, especially if no manual store subscriptions or composed stores are involved. Globally replacing injectStore with injectSignal, getState with get, and setState with set completely "just worked" in one of my example projects.

Some more complex migration scenarios:

  • Composed stores
// before:
const storeA = injectStore('state a')
const storeB = injectStore('state b')

const composedStore = injectStore(
hydration => createStore({ a: storeA, b: storeB }, hydration),
{ hydrate: true }
)

composedStore.use({ a: storeA, b: storeB })

// after (simple, but will cause a reevaluation):
const signalA = injectSignal('state a')
const signalB = injectSignal('state b')
const hydration = injectHydration()

const composedSignal = injectMappedSignal({ a: signalA, b: signalB })

composedSignal.set(hydration)

// after (no reevaluation):
const hydration = injectHydration()
const signalA = injectSignal(hydration.a ?? 'state a')
const signalB = injectSignal(hydration.b ?? 'state b')

const composedSignal = injectMappedSignal({ a: signalA, b: signalB })

Note that the "after" example that causes a reevaluation is functionally similar to not doing any manual hydration logic. Zedux automatically hydrates atoms immediately after initial evaluation:

// this atom behaves the same with or without these commented lines:
const hydratingAtom = atom('hydrating', () => {
const signalA = injectSignal('state a')
const signalB = injectSignal('state b')
// const hydration = injectHydration()

const composedSignal = injectMappedSignal({ a: signalA, b: signalB })

// composedSignal.set(hydration)

return composedSignal
})

To prevent the double-evaluation, always hydrate at the source; instead of hydrating derived atoms/signals (like this mapped signal), hydrate the atoms/signals they derive values from.

  • Store effects
// before:
const store = injectStore('my state')

injectEffect(() => {
const subscription = store.subscribe({
effects: ({ action }) => {
console.log('got metadata', action.meta) // untyped :(
},
})

return () => subscription.unsubscribe()
}, [])

store.setState('new state', { someMetaData: 'for the effect' })

// after:
const signal = injectSignal('my state', {
events: {
sendMetaData: As<{ someMetaData: string }>,
},
})

injectEffect(() => {
return signal.on('sendMetaData', ({ someMetaData }) => {
console.log('got metadata', someMetaData) // typed! :)
})
})

signal.set('new state', { sendMetaData: { someMetaData: 'for the listener' } })

Full List

Enough talk. Let's fight.

warning

This section is a work in progress

Replace

  • ecosystem.get -> ecosystem.getOnce
  • ecosystem.getInstance -> ecosystem.getNodeOnce
  • ecosystem.select -> ecosystem.getOnce
  • atomGetters.get -> ecosystem.get
  • atomGetters.getInstance -> ecosystem.getNode
  • atomGetters.select -> ecosystem.select (but prefer ecosystem.get, see below deprecation note)
  • ecosystem.registerPlugin -> ecosystem.on() (see above example. See below for the list of mod-equivalent ecosystem events)
  • ecosystem.wipe -> ecosystem.reset
  • ecosystem.destroy -> ecosystem.reset
  • atomInstance.addDependent -> atomInstance.on, passing { active: true } as the last parameter (listeners are "passive" by default, meaning they don't impact node lifecycles).
  • atomInstance._promiseStatus -> atomInstance.promiseStatus
  • atomInstance._promiseError -> atomInstance.promiseError
  • { flags } -> { tags } (in atom config objects)
  • { includeFlags, excludeFlags } -> { includeTags, excludeTags } (in the object passed to ecosystem.dehydrate)

For mods (previously added to ZeduxPlugins, now "ecosystem events" registered with ecosystem.on), replace:

  • ecosystemWiped -> resetEnd. Use in combination with resetStart to capture/restore values.
  • edgeCreated -> edge. Use the event.action property to distinguish between edge add, update, and remove
  • edgeRemoved -> edge (same note)
  • evaluationFinished -> runEnd. Use in combination with runEnd to time atom/selector evaluation time yourself.
  • stateChanged -> change
  • statusChanged -> cycle

Note that the instanceReused mod is removed with no equivalent (see below). Also note the new ecosystem events, error, invalidate, and promiseChange.

Optionally replace deprecated APIs. May be needed for TS support in rare cases. These will be required in Zedux v3:

  • atomGetters.select(selector, arg1, arg2) -> atomGetters.get(selector, [arg1, arg2])
  • useAtomSelector(selector, arg1, arg2) -> useAtomValue(selector, [arg1, arg2])
  • injectAtomSelector(selector, arg1, arg2) -> injectAtomValue(selector, [arg1, arg2])
  • ecosystem.getInstance -> ecosystem.getNode
  • injectAtomGetters -> injectEcosystem

Also optionally replace some deprecated and/or "legacy" types:

  • AtomGetters -> Ecosystem
  • AtomExportsType -> ExportsOf
  • AtomInstanceType -> NodeOf
  • AtomParamsType -> ParamsOf
  • AtomPromiseType -> PromiseOf
  • AtomStateType -> StateOf

Remove

  • manualHydration atom config option.
  • AtomInstanceBase usages. This was an unnecessary superclass. The AtomInstance class itself now handles everything this class did.
  • ZeduxPlugin usages. Use ecosystem events instead.
  • instanceReused mods. No replacement. We're working on build tool plugins to do what this mod was trying to do. Those will be released later.
  • reading sourceType field on evaluation reasons.
  • internalStore usages. Zedux no longer stores ecosystems in module-level state. Pass them around yourself as needed.
  • wipe usages (the top-level export). Zedux no longer has internal state to clear.

Keeping Stores

If you're using stores extensively, especially with lots of advanced features, it may be easier to keep using stores for now and migrate incrementally to signals.

To do this, replace any store-atom-related imports from @zedux/atoms or @zedux/react with imports from @zedux/stores. Refer to this diff for the full list:

import {
// APIs:
api,
atom,
AtomApi,
AtomInstance,
AtomInstanceRecursive,
AtomTemplate,
AtomTemplateRecursive,
injectPromise,
injectStore,
ion,
IonTemplate,

// types:
AnyAtomApiGenerics,
AnyAtomGenerics,
AnyAtomApi,
- AnyAtomInstance,
+ AnyStoreAtomInstance,
- AnyAtomTemplate,
+ AnyStoreAtomTemplate,
AtomApiGenerics,
AtomApiGenericsPartial,
AtomApiPromise,
AtomEventsType,
AtomExportsType,
AtomGenerics,
AtomGenericsToAtomApiGenerics,
AtomInstanceType,
AtomParamsType,
AtomPromiseType,
AtomStateFactory,
AtomStateType,
AtomStoreType,
AtomValueOrFactory,
AtomTemplateType,
IonInstanceRecursive,
IonStateFactory,
IonTemplateRecursive,
- PartialAtomInstance,
+ PartialStoreAtomInstance,
SelectorGenerics,
- } from '@zedux/react' // or '@zedux/atoms'
+ } from '@zedux/stores'

Additionally, if you're currently sharing Zedux internals across windows via getInternals/setInternals, you'll also need to use the store package's getStoreInternals and setStoreInternals:

// an example parent-child setup
// parent window:
import { getInternals } from '@zedux/react'
import { getStoreInternals } from '@zedux/stores'

window.zeduxInternals = getInternals()
window.zeduxStoreInternals = getStoreInternals()

// child window:
import { setInternals } from '@zedux/react'
import { setStoreInternals } from '@zedux/stores'

setInternals(window.opener.zeduxInternals)
setStoreInternals(window.opener.zeduxStoreInternals)

Replacing Stores

When migrating (either incrementally or all at once) to signals, do the following:

Replace:

  • injectStore -> injectSignal (see above for migrating composed stores and hydrations)
    • use injectHydration and initialize/set signals yourself.
  • atomInstance.getState -> atomInstance.getOnce (but prefer .get for automatic reactivity)
  • atomInstance.setState -> atomInstance.set
  • atomInstance.setStateDeep -> atomInstance.mutate
  • (if you previously migrated to using the @zedux/stores package): AnyStoreAtomInstance -> back to AnyAtomInstance
  • (if you previously migrated to using the @zedux/stores package): AnyStoreAtomTemplate -> back to AnyAtomTemplate

Remove:

  • createStore. Use mapped signals instead for composing state primitives. Use ecosystem.signal instead for creating state containers on the fly.
  • atomInstance.store. Signal-based atoms don't expose the underlying signal. Use the atom instance itself; it forwards all state updates and sent events to wrapped signals
    • atomInstance.store.getState -> atomInstance.getOnce (but prefer .get for automatic reactivity)
    • atomInstance.store.setState -> atomInstance.set
    • atomInstance.store.setStateDeep -> atomInstance.mutate
    • atomInstance.store.subscribe -> atomInstance.on (see signal events above)
  • atomInstance.dispatch. Can be replaced with atomInstance.send for custom events
  • Same goes for atomInstance.store.dispatch. Replace with atomInstance.send if possible
  • atomInstance._isEvaluating. This is only present on store atom instances. For signals-based atoms, you don't need it. Buffering store updates was its primary purpose. Signals, however, can always be set during atom/selector evaluation; the graph will always resolve. But if plugin authors do need it, they can use the purposefully obfuscated getInternals().c.n to see what's currently evaluating.

Internal Changes

Some public-but-underscore-prefixed properties changed. Most apps shouldn't have been using these.

  • ecosystem._consumeHydration. Removed. The cycle ecosystem event can be used to delete ecosystem.hydration entries yourself when hydrated nodes become Active.
  • _createdAt (the property on atom and selector instances). Removed. Plugins can track time created themselves if needed.