Skip to main content

Store

The createStore() factory returns an instance of this class. The store is the basic unit of state management.

In Zedux, unlike Redux, the store is a class. A Zedux app will typically create many stores. It's therefore necessary that stores be as light as possible. With classes, we can take advantage of prototype method reuse, making each store use less memory.

All atom state is held in stores. Every atom instance has a store, whether you create one yourself using injectStore() or let Zedux create one for you.

Creation

Create using createStore().

import { createStore } from '@zedux/react'

const store = createStore()
const withReducer = createStore(rootReducer)
const withInitialState = createStore(null, initialState)
const withBoth = createStore(rootReducer, initialState)
const splittingReducers = createStore({
a: reducerA,
b: reducerB,
})
const childStores = createStore({
a: storeA,
b: storeB,
})
const mixed = createStore({
a: reducerA,
b: storeB,
})

In Atoms

In an atom evaluator, stores should almost always be stable references. The easiest way to ensure this is by using injectStore().

import { atom, injectStore } from '@zedux/react'

const testAtom = atom('test', () => {
const store = injectStore(initialState)

return store // remember to return the store
})

You'll often deal with multiple stores in a single atom. Take advantage of inline state updates and store composition:

const testAtom = atom('test', () => {
const storeFromInjector = injectCustomThing()
const localStore = injectStore()

// state can be set inline during atom evaluation:
storeFromInjector.setState(newState)

// compose stores together to get a single store you can return:
const combinedStore = injectStore(() =>
createStore({
otherState: storeFromInjector,
state: localStore,
})
)

return combinedStore
})

Extending

There are many aspects of a store's behavior you can overwrite when extending this class. This is an extremely advanced feature. We're not documenting it yet as the internals of this class may change.

Methods

Stores expose the following methods:

actionStream

Returns an "observable"-like object. This object has a single .subscribe() function that you can call directly:

store.actionStream().subscribe(action => ...)
// or, passing an object:
store.actionStream().subscribe({
complete: () => ..., // (complete is never called - stores are non-finite)
next: action => ...,
error: err => ...,
})

Or you can pass the object directly to RxJS's from():

from(store.actionStream()).pipe(take(2)).subscribe(...)

The stream will emit an ActionChain object every time an action is dispatched to the store.

dispatch

Dispatches an action to the store. The action will be passed to the store's reducers (if any) and thence to any child stores.

After the reducer layer has been given an opportunity to produce a new value, all effects subscribers (including action streams) will be notified of the action. If the dispatch resulted in a state change, all normal subscribers will also be notified.

Also accepts ActionChain objects. These can drastically alter the behavior of the dispatch. This is how Zedux's store composition model works and is very low-level.

Returns the new state or the existing state if the reducer layer didn't produce a different value. Note that unlike Redux, dispatches in Zedux are always synchronous - the state will always be updated by the time the dispatch returns.

getState

Returns the current state of the store.

setState

Overwrites the current state of the store with the passed state. Similar to React's useState() state setters.

Does nothing if the passed state is exactly the same (===) as the current state. Even effects subscribers will not be notified.

Generates and effectively dispatches a special hydrate pseudo-action:

// calling:
store.setState('myState')
// is exactly the same as calling:
store.dispatch({ type: zeduxTypes.hydrate, payload: 'myState' })

This is important for store composition and time travel, but you won't usually need to worry about it.

Signature:

setState = (stateOrFactory, meta?) => newState
stateOrFactory

Required. Either the new state or a function that accepts the current state and returns the new state. This must be done immutably - e.g. by spreading nested objects/arrays manually:

store.setState(state => ({
...state,
myField: {
...state.myField,
nestedField: 'myVal',
}
}))
meta

Optional. Can be anything. This will be set as the meta property of the generated action.

Can be set to the special ignore meta type to prevent the atom instance that created this store from reevaluating.

Returns
The new state after notifying all subscribers of the state change. Returns the existing state if there was no state change.
setStateDeep

Deeply merges the passed state into the current state. Only works with JS objects.

IMPORTANT! This cannot be used to delete keys. Use .setState() for that. .setStateDeep() is only for recursively adding/updating keys.

Generates and effectively dispatches a special merge pseudo-action:

// calling:
store.setStateDeep({ myField: 'myVal' })
// is exactly the same as calling:
store.dispatch({
type: zeduxTypes.merge,
payload: { myField: 'myVal' }
})

This is important for store composition and time travel, but you won't usually need to worry about it.

Signature:

setStateDeep = (partialStateOrFactory, meta?) => newState
partialStateOrFactory
Required. Either a recursive partial state object or a function that returns one.
meta

Optional. Can be anything. This will be set as the meta property of the generated action.

Can be set to the special ignore meta type to prevent the atom instance that created this store from reevaluating.

subscribe

Registers listeners that will be notified of certain events/updates in the store.

Accepts a subscriber function or object.

Returns a subscription object.

use

The composition wizard of the Zedux realm. .use() modifies the structure of the store on the fly. Use it to add or remove child stores and reducers.

Accepts a HierarchyDescriptor, just like the first argument to createStore().

const myStore = createStore(rootReducer) // the initial "hierarchy"
myStore.use(anotherStore) // completely replace the store's state
myStore.use({
// completely replace the store's state
a: storeA,
b: storeB,
})
myStore.use({
// merge this hierarchy into the previous hierarchy
b: null, // remove "b" from the store
c: {
// add "c" and its nested nodes to the store
d: reducerD,
},
})
myStore.use(null) // wipe everything

See Also