This article requires a basic understanding of Hooks.

React state management is implemented in two ways. One is a Flux architecture, such as Redux, which shares global state through Context. The other is reactive, such as Mobx, which uses observable and HOC to share state.

After Hooks came out, the previous state solution with props was a bit too cumbersome. Given the complexity of state management before, I wrote a state management tool Piex Store the other day, which is fully object-oriented, based on Hooks, without using Context to share state.

Based on its core principles, this article will step through a simplest state management tool with a core code of just 13 lines.

Custom Hook

With the creation of the Hooks API, Function Components now have their own state and can customize Custom Hooks, which gives us more possibilities to manipulate the state of the Component. Here is a simple custom Hooks:

const useCounter = (a)= > {
    const [count, setCount] = useState(0);
    
    const increment = useCallback((a)= > {
        setCount(count + 1); 
    }, [count]);
    
    const decrement = useCallback((a)= > {
        setCount(count - 1); 
    }, [count]);
    
    return {count, increment, decrement};
}
Copy the code

To use it, it needs to be in an FC:

const Counter = (a)= > {
  const {count, increment, decrement} = useCounter();

  return (
    <article>
      <p>{count}</p>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </article>)}Copy the code

We have implemented a simple Custom hooks that reuse the logic that operates on count.

But there’s a problem, you can run this example, if we use two Counter components, their counts are not related, there’s no relationship between them. If we had a way to share the count, every time we changed the state, we could synchronize to the state everywhere we used it, wouldn’t that be state sharing!

Control component update

One of the problems with sharing state is synchronizing it to all components and updating the page. This can be done easily with useState. Let’s look at an example:

const App = (a)= > {
  const [,setState] = useState(Math.random());

  setTimeout((a)= >{
    setState(Math.random());
  }, 200);

  return (
    <p>{Date.now()}</p>)};Copy the code

Click here to see it in action.

You can see that instead of taking the first parameter of useState, you just use the second parameter to update the status, actually displaying different timestamps on the page every 200ms.

In fact, useState is not some dark magic, the specific implementation principle can see my article. Basically, every time we setState, if the argument is not equal to the previous state, React will rerun the function component and update the page by DOM Diff the returned value. So the key is setState. If we know setState, You know when to update your components.

State sharing

We know from the above example that useState returns the second value of the array, here called setState, which controls the rendering of the component.

If there was a way to create a useState for all components that use the shared state, store the second parameter, and run all setStates each time the shared state is updated, wouldn’t that update the state of all components? Wouldn’t that be a shared state?

Here’s a simple example:

let _count = 0;
let _setters = [];

const useCounter = (a)= > {
    const [, setCount] = useState(_count);

    _setters.push(setCount);
    
    const increment = useCallback((a)= > {
      _count++;
      _setters.forEach(setState= > setState(_count));
    }, [_count]);
    
    const decrement = useCallback((a)= > {
      _count--;
      _setters.forEach(setState= > setState(_count));
    }, [_count]);
    
    return {count:_count, increment, decrement};
}

const Counter = (a)= > {
  const {count, increment, decrement} = useCounter();

  return (
    <article>
      <p>{count}</p>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </article>)}const App = (a)= > {
  return (
    <div>
      <Counter />
      <Counter />
    </div>)}Copy the code

Click here to see how it works.

This example is based on the first custom Hook example. You can see that when you click a button, the count values of both Counter components on the page change.

That’s because I saved the state of useCounter in a global _count variable, and I collected the second argument of useState in a global _setters array, and every time I did Increment or Decrement, Change the _count value first, and then trigger setState in setters, so all Counter components will be updated. And the _count variable is returned renamed to count, so each component’s setState is triggered by _count to get the latest value and display it on the page.

In this way, we have achieved a simple counter state sharing, but each time it is too troublesome to write their own, can design a simple and easy to use, immediately can use the general tool.

Universal tool

Designing a common tool requires looking at application scenarios and common models, and object oriented is a good choice. For more details see the Piex Store core concept, let’s see how to implement:

export abstract class Store {
  state = {};
  setters = [];

  setState(newState) {
    this.state = newState;
    this.setters.forEach(setState= > setState(this.state)); }}export function useStore(store) {
  let [, setState] = useState(store.state);
  store.setters.push(setState);

  return store;
}
Copy the code

Since JS does not support inheritance, this code is implemented with TS, and the blank lines are removed. It takes only 13 lines of code to implement a state management:

  • stateCorresponding to the example above_count, stores the global state;
  • settersCorresponding to the example above_settersTo collectsetState;
  • setStateMethod to update a component;
  • useStoreIs used to collect dependencies;

How do you use it? As follows:

class CounterStore extends Store {
  state = {
    count: 0,
  }

  increment() {
    this.setState({
      count: this.state.count + 1,
    })
  }

  decrement() {
    this.setState({
      count: this.state.count + 1,}}}const counterStore = new CounterStore();

const Counter = (a)= > {
  const store = useStore(counterStore);

  return (
    <article>
      <p>{store.state.count}</p>
      <button onClick={store.increment}>Increment</button>
      <button onClick={store.decrement}>Decrement</button>
    </article>)}const App = (a)= > {
  return (
    <div>
      <Counter />
      <Counter />
    </div>)}Copy the code

In this way, all places that use counterStore can share the variables of Counterstore. state, and object methods can be used to update state and synchronize it to other components.

The last

Of course, this is just a simple demo, a lot of things are not done, such as setters collection setState dependencies are released when the component is uninstalled; Full status updates are cumbersome, only partial updates are fine, data change detection, and so on.

These are certainly not 13 lines of code to implement, if you are interested in Piex Store, a state management tool based on the principles above, perfect support for TS type inferenation, follow the React design philosophy, support middleware, you can use Redux DevTools to observe state changes.

As the Piex Store is still in its infancy, there are many areas that need to be improved. Please give us your support.