DEV Community

Tim Ermilov
Tim Ermilov

Posted on

Outstated - simple hooks-based state management for React

If you ever built React apps, you know that state management can be a pain.
Redux is great, but often is too much for small (and even some medium-sized) projects.
Unstated has simplified state management for smaller project using context and custom classes. It is quite awesome, but doesn't really work with new shiny React hooks.
So, I was wondering whether I could build something similar to Unstated but with all the hooks goodness to make it even nicer.

Introducing Outstated

Outstated is a simple hook-based state management solution for React.
It takes normal React hooks and allows you to share them to have a global state.
Oh, have I mentioned that it's just 474 bytes minzipped?

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')
);

How does it work?

Outstated is built on top of React hooks and context.
It is pretty much just a simple context wrapper around hooks that instantiates them once and allows accessing them where needed.

Outstated has three core parts:

Store

This is a place to store your state and the logic for updating it.
Stores are essentially React hooks (which means you can re-use them, use them outside of Outstated or even use other hooks within them).

Here's an example of a very simple store:

import {useState} from 'React';

const store = () => {
  const [state, setState] = useState('hello..');

  const greet = val => setState(`hello ${val}!`);

  return {state, greet};
};

Note that it use useState hook from React for managing state.
This means that Outstated doesn't have to manually manage anything related to state changes - React will handle this for us!
You do have to be careful not to mutate state directly or your components won't re-render though.

useStore

Next piece is a useStore hook.
It allows us to access the instantiated stores from anywhere in the tree.
useStore hook allows us to get store instances by using specific store constructor, e.g.:

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 of Outstated is <Provider> component.
It is responsible for initialization of global instances of given stores (this is required because React expects the number of hooks to be consistent across re-renders, so we cannot dynamically initialize them as needed).
It then uses context to pass initialized store instances to all the components down the tree.

render(
  <Provider stores={[counterStore]}>
    <Counter />
  </Provider>
);

Testing

Because Outstated stores are just hooks, we can construct them in
tests and assert different things about them very easily.
Here's a basic example using awesome react-testing-library:

import {act, testHook} from 'react-testing-library';

test('counter', async () => {
  let count, increment, decrement;
  testHook(() => ({count, increment, decrement} = counterStore()));

  expect(count).toBe(0);

  act(() => increment());
  expect(count).toBe(1);

  act(() => decrement());
  expect(count).toBe(0);
});

Links

You can find Outstated on GitHub.
Any feedback is appreciated :)

Top comments (0)