resso, the simplest React state manager in the world

Posted by Teach on Thu, 17 Feb 2022 15:09:46 +0100

1. resso, React state management has never been so simple

resso is a new React state manager. Its purpose is to provide the simplest way to use it in the world.

At the same time, resso also realizes on-demand update. If the data not used by the component changes, the component update will never be triggered.

GitHub: https://github.com/nanxiaobei/resso

import resso from 'resso';

const store = resso({ count: 0, text: 'hello' });

function App() {
  const { count } = store; // Deconstruction before use
  return (
    <>
      {count}
      <button onClick={() => store.count++}>+</button>
    </>
  );
}

There is only one API, resso. Just wrap the store object. There's nothing else.

To update, re assign the key of the store.

2. How does react state manager work?

Suppose there is a store that is injected into different components:

let store = {
  count: 0,
  text: 'hello',
};

// Component A
const { count } = store;
const [, setA] = useState();

// Component B
const { text } = store;
const [, setB] = useState();

// Component C
const { text } = store;
const [, setC] = useState();

// initialization
const listeners = [setA, setB, setC];

// to update
store = { ...store, count: 1 };
listeners.forEach((setState) => setState(store));

Put the setState of each component into an array. When updating the store, call the listeners again, so that the update of all components can be triggered.

How to monitor changes in store data? You can provide a public update function (such as Redux's dispatch). If it is called, it is an update. You can also use the proxy setter to listen.

Yes, almost all state managers work like this. It's that simple. For example, the source code of Redux: https://github.com/reduxjs/redux/blob/master/src/createStore.ts#L265-L268

3. How to optimize update performance?

Every time the store is updated, all setstates in listeners will be called, which will cause performance problems.

For example, when updating count, in theory, you only want A to update, and at this time, B and C also update, but they don't use count at all.

How to update on demand? You can use selectors (for example, the useSelector of Redux or the implementation of zustand):

// Component A
const { count } = store;
const [, rawSetA] = useState();

const selector = (store) => store.count;
const setA = (newStore) => {
  if (count !== selector(newStore)) {
    rawSetA(newStore);
  }
};

Similarly, for other components, subscribe to the new setA into listeners to realize the "on-demand update" of components.

The above functions can also be realized by using the getter of proxy to know the data "used" by the component.

4. How is resso implemented internally?

In the above implementation, a setState is collected in each component. When updating the store, determine whether to update the components through data comparison.

resso uses a new idea, which is actually more in line with Hooks' metadata concept:

let store = {
  count: 0,
  text: 'hello',
};

// Component A
const [count, setACount] = useState(store.count);

// Component B
const [text, setBText] = useState(store.text);

// Component C
const [text, setCText] = useState(store.text);

// initialization
const listenerMap = {
  count: [setACount],
  text: [setBText, setCText],
};

// to update
store = { ...store, count: 1 };
listenerMap.count.forEach((setCount) => setCount(store.count));

Use useState to inject each store data used in the component, and maintain an updated list for each key in the store at the same time.

The number of setstates collected in each component corresponds to the used store data one by one. Instead of just collecting a setState for component updates.

When updating, there is no need to compare the data, because the update unit is based on the "data" level, not the "component" level.

To update a data is to call the update list of the data, not the update list of the component. Metadata the entire store.

5. How is resso's API designed?

The secret of designing API is to write out the most desired usage first, and then think about the implementation method. What you do in this way must be the most intuitive.

resso also thought about the following API designs at the beginning:

1. Similar to valtio

const store = resso({ count: 0, text: 'hello' });

const snap = useStore(store);
const { count, text } = snap; // get
store.count++; // set

This is the standard Hooks usage. The disadvantage is that you have to add an API useStore. Moreover, the use of snap in get and store in set can split people. This is certainly not the "simplest" design.

2. Similar to valtio/macro

const store = resso({ count: 0, text: 'hello' });

useStore(store);
const { count, text } = store; // get
store.count++; // set

This is also achievable and is the standard Hooks usage. At this time, the get and set bodies are unified, but we still need to add a useStore API. This thing is just to call Hooks. What if the user forgets to write?

Moreover, it is found in practice that when using store in each component, you have to import two things, store and useStore. This is certainly not as concise as importing only one store, especially when there are many places to use.

3. To import only one store

const store = resso({ count: 0, text: 'hello' });

store.useStore();
const { count, text } = store; // get
store.count++; // set

This is the last hope of using Hooks "legally". Only one store is import ed, but it still looks strange and unacceptable.

If you try to design this API, you will find that if you want to update the store directly (you need to import the store) and deconstruct the store data through Hooks (you need to import more Hooks and different sources of get and set), this design will look awkward anyway.

For ultimate simplicity and simplest use, resso finally embarked on such API design:

const store = resso({ count: 0, text: 'hello' });

const { count } = store; // get
store.count++; // set

6. Usage of resso

Get store

Because the store data is injected into the component as useState, it needs to be deconstructed first (deconstruction means calling useState), deconstructed at the top level of the component (i.e. Hooks rules, which cannot be written after if), and then used. Otherwise, there will be React warning.

Set store

Assigning a value to the first layer data of the store will trigger the update, and only the assignment of the first layer data will trigger the update.

store.obj = { ...store.obj, num: 10 }; // ✅  Trigger update

store.obj.num = 10; // ❌  Do not trigger updates (please note that valtio supports this writing)

resso does not support the way valtio is written. The main considerations are as follows:

  1. It is necessary to deeply traverse all data for proxy, and when updating data, it is also necessary to proxy first, which will lead to certain performance loss. (resso only initializes the proxy store once.)
  2. Because all data is proxy, it is unfriendly to display when printing in Chrome console, which is a big problem. (resso won't have this problem, because only the store is a proxy, but it usually prints the data in the store.)
  3. If the sub data is deconstructed, such as obj, obj Num = 10 can also trigger the update, which will make the data source opaque. It is uncertain whether it comes from the store and whether the assignment triggers the update. (the subject of resso update is always store, and the source is clear.)

7. Make simple, not chaos

The above is the design concept of resso and some implementation methods of React state manager.

In the final analysis, React state manager is a tool, React is a tool, JS is a tool, programming is a tool, and the work itself is also a tool.

The purpose of tools is to create and create works that act on the real world, not the tools themselves.

So why not make it easier?

JQuery is to simplify the development of native JS, React is to simplify the development of jQuery, the development is to simplify the process of the real world, the Internet is to simplify people's communication path, work path and consumption path, the significance of development is simplification, the significance of the Internet is simplification, and the value of the Internet also lies in simplification.

So why not make it easier?

Chic. Not geek.

Simplicity is everything.

try try resso: https://github.com/nanxiaobei/resso

Topics: React redux hooks