raid
v7.0.0
Published
Centralised state container
Downloads
47
Maintainers
Readme
Raid
De/Centralised state container
Getting Started
yarn add raid
npm i -S raid
Raid manages the state layer by providing an observable that supplies the current state of the application.
import { Signal } from 'raid'
const signal = Signal.of({
count: 0
})
signal.observe(state => {
// Current signal state, typically a side effect
console.log(state)
})
State is held within the signal and changes can be triggered by emitting action objects and using update functions to mutate the state.
import { Signal } from 'raid'
const signal = Signal.of({
count: 0
})
// Update function
signal.register(state => {
// Mutations occur here
// Up to you if you want to mutate or return new objects
state.count++
return state
})
// Observable
signal.observe(state => {
// Current signal state
console.log(state)
})
// Emit action
signal.emit({
type: 'ADD'
})
Raid works great with immutable state objects to ensure that all mutations occur within the update functions, although this is not enforced, it’ll work great with regular javascript structures too.
Further reading exists in the documentation.
Additionally there are a number of examples and test cases.
See the changelog for details regarding changes between major versions. Raid adheres to semantic versionin and strives to keep breaking changes to a minimum and provide upgrade instructions (or codemods) where necessary.
Applying updates
Update functions are applied to the signal using the register
method, which accepts an update function.
signal.register((state, event) => {
return state
})
An update function is always of the form:
(state: Object<any>, event: EventObject) => state: Object<any>
// EventObject
{
type: String,
payload: Object<any>
}
Note that whilst type is assigned as a string here, which is usual, it needn't be and any type would work. See the actions example for a differing implementation.
Update functions must always return the new (or mutated) state object, or the next update function in the chain (or, if the last or only, the observers will get it) will receive a void state object.
See safe from @raid/addons
for a higher order function to ensure state is returned from updates.
signal.register
can additionally take an options object, which is currently only used to assign a key to the update function, and returns a function which can be used to remove (dispose) the update function from the signal:
// signal.register
(update: Function(<state>, <event>), options: Object<RegisterOptions>) => Function(<void>)
// RegisterOptions
{
key: String
}
Note that whilst the type of key
is assigned as a string here, which is usual, anything would work so long as it can be used a key for map.
Further reading exists in the documentation.
Attaching observers
Observers are usually where your side effects will live (although nothing in Raid mandates this) and receive the current state object moving through the signal when attached and on every emit through the stream.
Note that pre-version 6 signal observers attached after the signal is created would not receive an immediate execution. This change is to allow a reactive model where observer side effects can run immediately. As observers typically perform updates elsewhere in the system (a GUI or TUI, for example), this is usually what you want and avoids potentially costly re-renders to work around.
signal.observe(state => {
console.log(state)
})
Observer functions should always take the form:
(state: Object<any>) => void
The signal.observe
function itself accepts a next
and an error
observer (which will fire if an error is detected) and an options object:
// signal.observe
(next: Function<state>, error: Function<Error>, options: Object<ObserveOptions>) => void
// ObserveOptions
{
key: String,
subscription: {
next: Function<state>,
error: Function<error>,
complete: Function<state>
}
}
Note that key
is declared as a String here, which is usual, but anything that can be used as a key within a map would work.
If a subscription
object is supplied as an option then it will take precedence over next
and/or error
parameters and be used as outputs of the stream. The complete
function is mentioned above for completeness, Raid signals typically never complete as they are the stream form of event emitters.
signal.subscribe
exists as an alias to signal.observe
.
Managing the signal lifecycle
Signals have a clean and minimal API and each function that creates resources will return a function to remove them, i.e.
const dispose = signal.register(updateFn)
const detach = signal.observe(observeFn)
Signals will keep track of updates and observers and provides methods to clean up when (if?) you want to destroy a signal:
signal.detachAll()
signal.disposeAll()
Using keys to keep track of resources
Raid will manage resources for you and provide functions when you do want to perform clean-up, however, if you’d prefer to supply a key then you can:
signal.register(fn, {
key: 'uid for an update function'
})
signal.observe(fn, {
key: 'uid for an observer'
})
Both register
and observe/subscribe
will return functions to clean up, but you can use the key to remove them:
// Dispose is to register
signal.dispose('uid for an update function')
// Detach is to observe
signal.detach('uid for an update function')
Applying functions to updates
Raid Signals can additionally keep a stack of functions to apply to every update passing through the stream. They are applied lazily so can be added and removed and will execute for every update on every emit.
signal.apply(fn => (state, event) => fn(state, event))
Applicator functions are higher-order functions that accept an update function to decorate.
An example is using safe from @raid/addons
to ensure that updates within the signal always return a state.
import { safe } from '@raid/addons'
signal.apply(safe)
Mounting other streams
It is often very useful to create streams which emit action objects and then mount those streams on to a Signal.
import { actions, keyStream } from '@raid/streams'
const signal = Signal.of()
signal.update((state, event) => {
if (event.type === actions.keydown) {
// Respond to the keydown event here
}
return state
})
const subscription = signal.mount(keyStream())
Mount attempts to use the subscribe
method of the passed-in stream and will pass the return value back. For streams which implement the ES Observable proposal the returned value will be a Subscription
object which can be used to unmount the stream.
subscription.unsubscibe()
Mount can also mount another Signal, whereby the source Signal will receive any events that pass through the mounted signal (but not its state). When mounting a Signal the return value will be a function that can be invoked to unmount.
const signal = Signal.of({})
const mounted = Signal.of({})
const unmount = signal.mount(mounted)
mounted.emit({ type: 'action', payload: 'I ❤️ Raid'})
unmount()
Running tests
yarn
yarn test
Contributing
Pull requests are always welcome, the project uses the standard code style. Please run yarn test
to ensure all tests are passing and add tests for any new features or updates.
For bugs and feature requests, please create an issue.
See the root readme for more information about how the repository is structured.
License
MIT