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

useEffect(() => {
// uncomment this and open browser console:
// const cleanup = ecosystem.on(event => console.log(event))
// return () => cleanup()
}, [ecosystem])
const cleanupRef = useRef()

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

  • ecosystem.findAll now accepts all the same filtering parameters as ecosystem.dehydrate and then some:
    • Pass an @-prefixed string like @atom or @selector to only return nodes of that type.
    • The types have been improved to make editors autocomplete those @-prefixed strings, leading to some slick DX.
    • Pass an array of filters (.findAll([...myFilters])) as a shorthand for .findAll({ include: [...myFilters] })
  • untrack - A new top-level export for bumping out of a reactive context.
  • AtomProvider now accepts function overloads for its instance and instances props. The function will receive the ecosystem as its first parameter and should return an atom instance (or an array of instances for the instances prop).
  • 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.

Every export in the @zedux/stores package has been renamed to be store-specific. Some examples:

  • atom -> storeAtom
  • api -> storeApi
  • ion -> storeIon
  • AtomInstance -> StoreAtomInstance

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.

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() }), {})

The new signals-based ions now set ttl: 0 by default. This reflects their primary purpose - to derive state. Derivations are usually transient and should be cleaned up (by default!) when no longer in use.

Migrating to Signals

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

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

// after:
import { injectMemo } from '@zedux/react'
import { storeApi as api, storeAtom as 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 { storeApi as api, storeAtom as 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)
  • atomTemplate.getInstanceId -> atomTemplate.getNodeId
  • ecosystem._idGenerator.generateId -> ecosystem.makeId. Use the new makeId ecosystem config option in tests instead of mocking Zedux APIs.
  • AtomSelectorOrConfig -> SelectorTemplate
  • The initialState option of injectPromise -> initialData.
  • The dataOnly option of injectPromise -> use the returned dataSignal instead.

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:

  • atomInstance.setState -> atomInstance.set
  • 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.
  • atomDefaults ecosystem config options. Use custom atom factories to share atom behaviors. Or use ions to create ttl: 0 atoms easily.
  • 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.
  • any other usages of ecosystem._idGenerator.

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 the relevant imports from @zedux/stores. It may be easier to use import aliases to map the new names to the old. Refer to this for the full list:

import {
// APIs you may want to alias when migrating from Zedux v1:
injectStorePromise as injectPromise,
storeApi as api,
storeAtom as atom,
StoreAtomApi as AtomApi,
StoreAtomInstance as AtomInstance,
StoreAtomTemplate as AtomTemplate,
storeIon as ion,
StoreIonTemplate as IonTemplate,

// APIs that don't need aliasing:
actionFactory,
createReducer,
createStore,
detailedTypeof,
doSubscribe,
getMetaData,
getStoreInternals,
injectStore,
is,
isPlainObject,
removeAllMeta,
removeMeta,
setStoreInternals,
Store,
zeduxTypes,
} from '@zedux/stores'

import type {
// Types you may want to alias when migrating from Zedux v1:
AnyStoreAtomApi as AnyAtomApi,
AnyStoreAtomApiGenerics as AnyAtomApiGenerics,
AnyStoreAtomGenerics as AnyAtomGenerics,
AnyStoreAtomInstance as AnyAtomInstance,
AnyStoreAtomTemplate as AnyAtomTemplate,
PartialStoreAtomInstance as PartialAtomInstance,
StoreAtomApiGenerics as AtomApiGenerics,
StoreAtomApiGenericsPartial as AtomApiGenericsPartial,
StoreAtomApiPromise as AtomApiPromise,
StoreAtomGenerics as AtomGenerics,
StoreAtomGenericsToStoreAtomApiGenerics as AtomGenericsToAtomApiGenerics,
StoreAtomInstanceRecursive as AtomInstanceRecursive,
StoreAtomStateFactory as AtomStateFactory,
StoreAtomTemplateRecursive as AtomTemplateRecursive,
StoreAtomValueOrFactory as AtomValueOrFactory,
StoreIonInstanceRecursive as IonInstanceRecursive,
StoreIonStateFactory as IonStateFactory,
StoreIonTemplateRecursive as IonTemplateRecursive,
StoreSettable as Settable,

// types that don't need aliasing:
AtomTemplateType,
AtomStateType,
AtomStoreType,
AtomInstanceType,
AtomParamsType,
AtomPromiseType,
AtomEventsType,
AtomExportsType,
Action,
ActionChain,
ActionCreator,
ActionFactory,
ActionFactoryActionType,
ActionFactoryPayloadType,
ActionFactoryTypeType,
ActionMeta,
ActionMetaType,
ActionPayloadType,
ActionType,
ActionTypeType,
Branch,
Composable,
Dispatchable,
Dispatcher,
EffectType,
EffectsSubscriber,
ErrorSubscriber,
HierarchyDescriptor,
KnownHierarchyDescriptor,
NextSubscriber,
Observable,
Reactable,
RecursivePartial,
Reducer,
ReducerBuilder,
Scheduler,
Selector,
SetState,
StateSetter,
StoreEffect,
StoreStateType,
SubReducer,
Subscriber,
SubscriberObject,
Subscription,
} from '@zedux/stores'

The easiest way to do this at scale is to alias the @zedux/react package to point to a local file that re-exports all Zedux's APIs, named as above.

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): Remove any import aliases (like storeAtom as atom -> atom) and import from @zedux/react (or @zedux/atoms if not using React) instead.

Remove:

  • ttl: 0 in ions. This is the default for signals-based ions. That also means you may need to add ttl: -1 (infinity) to ions that you don't want to give ttl: 0 (tip: If you're unsure, leave the default).
  • 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.
  • atomTemplate._createInstance. Renamed to atomTemplate._instantiate. Zedux uses this internally. You shouldn't use it manually.
  • atomTemplate._config. Renamed to atomTemplate.c (short for config). This is for internal use so it's obfuscated
  • atomTemplate._value. Renamed to atomTemplate.v (short for valueOrFactory). This is for internal use.