A predictable & observable state container for React apps
Restore
A predictable & observable state container for React apps
- Simple - Reduced boilerplate, minimal interface, refined patterns
- Observable - Subscriptions to value changes are automatic, eliminating unnecessary renders
- Predictable - Unidirectional data makes it easy to test, debug and reason about your application
- Immutable - Frozen state along with thaw/replace updates provide baked in immutability
- DevTools - Helpful tools, including time travel, provide clear visibility of your state, actions, updates & observers
Install
npm install react-restore
Creating a store
A store
holds the state
of the application and the actions
used to update
that state
import Restore from 'react-restore'
import * as actions from './actions'
let initialState = {text: 'Hello World'}
let store = Restore.create(initialState, actions)
Now we have a store!
Accessing values in the store
To get the text
value from the store
store('text') // 'Hello World'
Updating values in the store
actions
are used to make updates to the state of thestore
actions
are passed as an object during thestore
's creation- The
actions
object can be created explicitly or by using import syntax- e.g.
import * as actions from './actions'
- e.g.
actions
contain the whole lifecycle of an update making async updates easy to create and track
Let's create an action called setText
to update
the text
value in our store
export const setText = (update, newText) => {
update(state => {
state.text = newText
return state
})
}
setText
can now be called via the store
store.setText('Updated World')
This would update the value text
in the store
to 'Updated World'
The update method
actions
are passedupdate
as their first argument (followed by any arguments you passed to them)- The
update
method is how we replace values held by thestore
- The
update
method uses a pure updater function to preform these updates
If you look back at our setText
action
you can see our updater
function
state => {
state.text = newText
return state
}
The updater
function is passed the state
(or more likely, part of the state
) and returns an updated version of it
Targeting state updates
update
takes a dot notation path as an optional first argument- This path allows you to target part of the
state
instead of the wholestate
- By doing this, only the components that care about what you're targeting will re-render and the rest will not
For example, our setText
action
could be
export const setText = (update, newText) => {
update('text', text => {
return newText
})
}
Targeting a more complex state
import Restore from 'react-restore'
import * as actions from './actions'
let initialState = {
nested: {
wordOne: 'Hello',
wordTwo: 'World'
}
}
let store = Restore.create(initialState, actions)
Let's create an action
called setNestedText
to update
wordTwo
in our store
export const setNestedText = (update, newValue) => {
update('nested.wordTwo', wordTwo => newValue)
}
Calling it is the same as before
store.setNestedText('Updated World')
This would update
the value of wordTwo
from 'World'
to 'Updated World'
Multi-arg Paths
Instead of concatenating a string for the path passed to store
or update
, you can define your path with multiple arguments. For example if you had an id (let id = 123
) for an item within the state you could break the path into multiple arguments, like so...
let name = store('items', id, 'name') // Gets the value of items[id].name from the store
// When updating, the last argument is always the updater function
update('items', id, 'name', name => 'bar') // Updates the value of items[id].name to 'bar'
Connecting the store to your React components
Connecting React components to the store
is easy
Restore.connect(Component)
- Once a component is connected, it will have access to the
store
viathis.store
- It will automatically re-render itself when a value it consumes from the
store
changes - A connected component inherits the
store
of its closest connected parent - At the top-level of your app you will explicitly connect a
store
, since it has no parent to inherit from - This top-level
store
will be passed down to your other connected components - We recommend using a single top-level
store
for your app
Restore.connect(Component, store) // Explicitly connects store to Component
Restore.connect(Component) // Component inherits store from closest parent Component
To access the store
from within a connected component, we do the same as before but this time referencing this.store
this.store('text')
// or
this.store.setText('Updated World')
Async Updates
Actions can contain synchronous and asynchronous updates, both are tracked and attributed to the action throughout its lifecycle. Here we'll make our setText
action get the newText
value from the server and then update the state asynchronously.
export const setText = update => {
getTextFromServer(newText => {
update('text', text => newText)
})
}
It can be useful to compose synchronous and asynchronous updates together. Say you wanted to show a loading message while you fetched the newText
value from the server. You could update a loading
flag synchronously and then unset it later when you get the response.
export const setText = update => {
update('loading', loading => true)
getTextFromServer(newText => {
update('loading', loading => false)
update('text', text => newText)
})
}
Enabling DevTools / Time Travel
- Restore ships with a dev tools component
<Restore.DevTools />
- Drop it anywhere in your application to enable the dev tools
Standalone Observers
Connected components are observers
but you can use this functionality outside of components too!
store.observer(() => {
console.log(store('text'))
})
This function will run once immediately and again anytime the values it consumes change, in this case our text
value
Putting it together
App.jsx
import React from 'react'
import Restore from 'react-restore'
class App extends React.Component {
render () {
return (
<div onClick={() => this.store.setText('Updated World')}>
{this.store('text')}
</div>
)
}
}
export default Restore.connect(App)
actions.js
export const setText = (update, newText) => {
update('text', text => {
return newText
})
}
index.js
import React from 'react'
import ReactDOM from 'react-dom'
import Restore from 'react-restore'
import App from './App.jsx'
import * as actions from './actions.js'
let initialState = {text: 'Hello World'}
let store = Restore.create(initialState, actions)
let Root = Restore.connect(App, store)
ReactDOM.render(<Root />, document.getElementById('root'))