raid

7.0.0 • Public • Published

Raid

De/Centralised state container

npm License Build Status Coverage Status js-standard-style

Documentation

Getting Started

Install with yarn or npm

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

Package Sidebar

Install

npm i raid

Weekly Downloads

17

Version

7.0.0

License

MIT

Unpacked Size

31.9 kB

Total Files

15

Last publish

Collaborators

  • mattstyles