React Mirror
A fractal state tree that decorates your views with local stores.
- Co-location - views are self-contained
- Composable - state tree is queryable
- Single-paradigm - everything is a component
Quick demo:
const Counter = <div> Value: value <button onClick= >+</button> <button onClick= >-</button> </div>
Rationale
Some state-management solutions like Redux put everything in a single global store. That's great for predictability, debugging, convenience and tooling. But tying components to a global store means they're no longer self-contained; unnecessarily distributing view, state & effects code across your app hampers reusability, while increasing cognitive overhead and architectural complexity.
Other solutions like Cycle.js suggest composing local state into a single state tree. The lack of context however is very inconvenient - actions are forced to percolate through your app layer-by-layer. Orchestrating views together (eg. search & results) often requires updating intermediate components and is a pain in the ass. Many of these solutions also have isolated ecosystems.
Mirror composes local stores into a single tree (like Cycle.js), and gives every store access to the entire tree (like Redux).
Stores can dispatch actions to, and query, (the action/state streams of) any other store (or collection of). Each store injects props from it's state stream into the component they decorate.
Usage
Cursors
Cursors point to a collection of stores, relative to another store. eg, "every Todo
which is a child of this store (a TodoList
)"
Examples
mirror // Local storemirrorparent // Immediate parentmirrorparent'todo-list' // Parent named "todo-list"mirrorparentTodoList // As abovemirrorall // Every storemirrorall'todo-list' // Every "todo-list"mirrorall'todo-list' // Every "todo-item" inside a "todo-list"mirror // Every "todo-item" inside "todo-list/shopping"
Methods
filter
can be a store ID, name or component.
// Get the root store // Get one or more parent stores (all ancestors) // Get one or more child stores (all descendants) // Same as `root().children(filter, maxStores)` // Same as `all(filter, 1)` // Same as `parents(filter, 1)` // Same as `children(filter, 1)`
Two types of cursor
The mirror
cursor. mirror
is the first argument of state
, it has four getters ($props
, $state
, $actions
& $stores
) for querying selected stores.
mirror$actionsmirror$state
The dispatch
cursor. dispatch
is the second argument of state
, it is also injected as a prop to decorated components. Call it like a function to dispatch actions.
dispatchparent'todo-list''ACTION_TYPE' payload
If nothing matches the dispatch
cursor Mirror will wait until something does - and then dispatch the action. Disable this behaviour by passing false
as the third argument.
Working with streams
Streams are basically arrays of events. You can map, reduce, combine, and do all sorts of cool things with them. state
returns a stream - the first value represents the store's state when it's initialized, and the last value in the stream represents the store's current state. This section describes some patterns for creating, and using streams with Mirror.
Mirror uses most.js (it's really fast). But you can use any streaming library that supports the Observable
API like RxJS or XStream.
Scanning actions
Every action has a type
, payload
& store
(id). Scanning actions means applying them to the current state one-by-one to produce new state.
// For every action, emit a value 1 greater than the previous value$actions
handleActions
combines multiple action handlers into one function.
Effects
For every value in a stream, tap
calls a function & ignores the result.
I like using tap
& dispatch
together to describe effects.
$actions
Multi-Store Streams
Streams taken from mirror
contain values from multiple stores. Mirror will merge
actions & combine
state / props.
merge
----a----b--------c------>--1----------2-----------> --1-a----b---2----c------>
Use most.merge
to include additional action streams.
most
combine
a--------b---c----------->1---2---------------3----> a1--a2---b2--c2-----c3--->
Every value in a state / prop stream is an Enum
. That's an array with object-like properties for each value.
stores = stores0 === stores'oigkzfajky'
$stores
By comparison, $stores
emits a value every time a store is added, removed or updated. Each value in $stores
contains low-level structural metadata with reference to the local store. Use $stores
for advanced use cases like accessing component instances, getting children up to a certain depth or generating dependency graphs.
Combining Streams
Mirror exports four helpers (combine
, combineEnums
, combineNested
& combineEventsWith
) for combining streams together.
combine
combine
returns a stream of arrays that combine values from each input stream.
// [{type: 'ACTION_TYPE'}, Enum({a: stateA, b: stateB})]
combineEnums
combineEnums
joins streams of enums together, & removes duplicates. Use combineEnums
to join multiple state streams, or multiple prop streams.
/* Enum({ a: stateA, b: stateB, c: stateC }) */
combineNested
combineNested
inverts an object containing streams of enums, resulting in a single enum. Use combineNested
to join state & prop streams together.
/* Enum({ a: {state: stateA, props: propsA}, b: {state: stateB} })*/
combineEventsWith
combineEventsWith
joins a stream of events with the value before & after that event from another stream.
// {event, before: stateBeforeEvent, after: stateAfterEvent}
The options
argument defaults to {before: true, after: true}
.
Store Configuration
The Mirror
configuration accepts name
, state()
, mapToProps()
& pure
. Every property is optional.
name
Mirror uses name
inside filters. name
is a string, or array of strings.
You can add additional names to a store by passing withName
as a prop, or calling the static withName
method. withName
is useful for distinguishing particular instances of a component.
const Form = /* ... */ const CreateProjectForm = Form
state(mirror, dispatch)
Called when the store mounts, and returns the instance's state stream. Mirror derives props from the latest value in the state stream, and injects those props into the wrapped component. Other stores can read the state stream.
If provided, the wrapped component only renders once the state stream emits it's first value.
mapToProps(state, props)
Accepts the current state & parent props, and returns props to inject into the wrapped component. Called whenever state or props change; and useful when using, for example, Immutable.js.
Note that Mirror ignores parent props not handled by mapToProps()
.
mapToProps()
can return a function. In this case, that function will be used as mapToProps()
for a particular component instance. This allows you to do per-instance memoization.
pure
If true
, avoids unnecessary computation:
- Only emit props / state changes if either changed
- Only call
mapToProps
if props / state changed - Only re-render if value returned by
mapToProps
changed
The equality check is based on shallowEqual
. You can alternatively provide an object with three functions (stateEqual
, propsEqual
, statePropsEqual
) which accept the previous and current props / state.
If disabled (by passing a falsey value), Mirror will re-render the wrapped component on every state / prop change.
Wrapping Up
Lifecycle Actions
Mirror dispatches INITIALIZE
to stores when they're created, and TEARDOWN
immediately before they unmount.
Static Cursors
Each component exposes mirror
& dispatch
as static cursors.
const MyComponent = /* ... */ MyComponentmirror // Every "MyComponent"MyComponent // First "MyComponent" to mount // Every mounted "MyComponent" after "INITIALIZATION_COMPLETE"MyComponentmirror $actions
Instance Properties
getWrappedInstance
Access the wrapped component instance with this.getWrappedInstance()
.
Cursors
mirror
& dispatch
can be accessed via this.mirror
/ this.dispatch
.
Circular State Dependency
The mirror
cursor omits local state that's part of a query (eg, mirror.all().$state
). Avoiding this circular dependency avoids infinite rendering loops. If you really want this access $state
directly without any traversal.
This doesn't apply to static cursors.
Caveats
Mirror makes it easy to build predictable applications. But things go wrong sometimes. This section will help you avoid things going wrong.
Commutative Updates
Mirror cannot guarantee the order stores are updated in, or the order of prop changes. Except for when handling actions state
should be commutative (order of state / prop values must not affect final state), for example:
state
is subscribed to multiple state / props streams- These streams emit some values in a random order
- The state stream's last value should be the same after every run
If state
is not commutative then dispatching an action with cascading effects can cause race conditions / unpredictable state. Tips to keep state
commutative:
- Streams that depend on actions-only are always commutative
- Reduce state from multiple sources to a single value
- Prefer
mapToProps
over using$props
insidestate
- Props which never change are safe to use inside
state
Uninitialized Stores
Because:
- Mirror cannot guarantee the order stores are initialized in
- Stores are dynamically added / removed at runtime
Actions might not be dispatched to every store they're intended for consistently. Mirror behaves consistently when dispatch
can be guaranteed to match one store only, but problems may arise when dispatching actions to a dynamic collection of stores. Tips to alleviate these problems:
- Avoid dispatching actions to collections as part of an effect
- Dispatching actions as part of asynchronous user interaction is safe
- Use contextual stores if children have reactive dependencies
See Also
Thanks
React Mirror was inspired by Cycle.js onionify, Redux & the Controller View pattern.