Outstated
Simple hooks-based state management for React
Like unstated but with hooks
Installation
npm install outstated
Example
import React, {useState} from 'react';
import ReactDOM from 'react-dom';
import {Provider, useStore} from 'outstated';
const store = () => {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
const reset = () => setCount(0);
return {count, increment, decrement, reset};
};
function Counter() {
const {count, increment, decrement, reset} = useStore(store);
return (
<div>
<button onClick={decrement}>-</button>
<span>{count}</span>
<button onClick={increment}>+</button>
<button onClick={reset}>reset</button>
</div>
);
}
ReactDOM.render(
<Provider stores={[store]}>
<Counter />
</Provider>,
document.getElementById('root')
);
For more examples, see the example/
directory.
Guide
Unstated is awesome, but doesn't really use hooks.
Can we build something similar to unstated with hooks to make something even nicer?
Introducing Outstated
I really like unstated. I really like hooks. I wanted a simple hook-based app state management solution. This is why I've built Outstated.
Outstated is built on top of React hooks, context and patterns surrounding those elements.
It has three pieces:
Store
It's a place to store our state and some of the logic for updating it.
Store is a very simple React hook (which means you can re-use it, use other hooks within it, etc).
import {useState} from 'React';
const store = () => {
const [state, setState] = useState({test: true});
const update = val => setState(val);
return {state, update};
};
Note that stores use useState
hook from React for managing state.
When you call setState
it triggers components to re-render,
so be careful not to mutate state
directly or your components won't re-render.
useStore
Next we'll need a piece to introduce our state back into the tree so that:
- When state changes, our components re-render.
- We can depend on our store state.
- We can call functions exposed by the store.
For this we have the useStore
hook which allows us to get global store instances
by using specific store constructor.
function Counter() {
const {count, decrement, increment} = useStore(counterStore);
return (
<div>
<span>{count}</span>
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
</div>
);
}
<Provider>
The final piece that Outstated has is <Provider>
component.
It has two roles:
- It initializes global instances of given stores (this is required because React expects the number of hooks to be consistent across re-renders)
- It uses a set of contexts to pass initialized instances of given stores to all the components down the tree.
Different context is used for each store. This allows to only trigger re-renders in the components that use the updated store. As a (minor) downside of this approach - nested contexts are created for each store you pass.
render(
<Provider stores={[counterStore]}>
<Counter />
</Provider>
);
Testing
Whenever we consider the way that we write the state in our apps we should be
thinking about testing.
We want to make sure that our state containers have a clean way to test them.
Because our containers are just hooks, we can construct them in tests and assert different things about them very easily.
import {renderHook, act} from 'react-hooks-testing-library';
test('counter', async () => {
let count, increment, decrement;
renderHook(() => ({count, increment, decrement} = counterStore()));
expect(count).toBe(0);
act(() => increment());
expect(count).toBe(1);
act(() => decrement());
expect(count).toBe(0);
});