DEV Community

Cover image for resso, world's simplest React state manager
南小北
南小北

Posted on

resso, world's simplest React state manager

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

resso is a brand-new state manager for React, aims to provide the world's easiest way to use.

resso also implements on-demand updating. If the unused data changed, it will never trigger the component updating.

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

import resso from 'resso';

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

function App() {
  const { count } = store; deconstruct first, then use
  return (
    <>
      {count}
      <button onClick={() => store.count++}>+<button>
    <>
  );
}
Enter fullscreen mode Exit fullscreen mode

Only one API resso, just wrap the store object, nothing else.

To update, just re-assign the key of the store.

2. How does React state manager works?

Suppose there is a store, 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();

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

// update
store = { ...store, count: 1 };
listeners.forEach((setState) => setState(store));
Enter fullscreen mode Exit fullscreen mode

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

How to monitor the store data changing? A public update function (such as Redux's dispatch) can be provided, which is updated if called. You can also use the proxy's setter to listen.

Yes, almost all state managers work this way, 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 the update performance?

All setState in listeners is called every time the store is updated, which can cause performance issues.

For example, when updating count, theoretically only A is expected to update, at this time B and C are also updated, but they do not use count at all.

How to update on demand? You can use a selector (such as Redux's useSelector, or zustand's implementation):

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

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

The same way for other components, subscribing new setA to listeners can achieve "on-demand update" of components.

Above functions can also be implemented using proxy's getter, and the data "used" by the component can be known through the getter.

4. How is resso implemented internally?

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

resso uses a new idea, which is actually more in line with the primitive concept of Hooks:

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

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

// update
store = { ...store, count: 1 };
listenerMap.count.forEach((setCount) => setCount(store.count));
Enter fullscreen mode Exit fullscreen mode

Use useState to inject each data from store used in the component, while maintaining a list of updaters for each key in the store.

The number of setStates collected in each component corresponds to the store data used. Instead of just collecting one setState for the component updating.

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

To update a certain data is to call the updater list of this data, not the updater list of the component. Primitive the entire store.

5. How the API of resso is designed?

The secret to designing an API: write the usage you want first, and then figure out how to implement it. What comes out of this must be the most intuitive.

resso also thought about the following API design 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
Enter fullscreen mode Exit fullscreen mode

This is the standard usage of Hooks, with the disadvantage of adding an extra API useStore. And using snap when getting, using store when setting, makes people split. This is definitely not the "simplest" way.

2. Similar to valtio/macro

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

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

This is also achievable and is the standard usage of Hooks. At this time, the main body of get and set is unified, but it is still necessary to add a useStore API. This thing is only for calling Hooks, what if the user forgets to write it?

And in practice, it is found that when using store in each component, you have to import two things, store and useStore, which is definitely not as simple as importing only one store, especially when it is used in many components, will be very troublesome.

3. In order to import only one store

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

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

This is the last hope of a "legal" use of Hooks, just importing a store, but it still looks weird and unacceptable anyway.

If you try to design this API, you will find that if you want to directly update the store (requires import store), and want to deconstruct store data from Hooks (need to import one more Hook, get and set are from different sources), no matter what, the design will looks awkward.

For the ultimate simplicity, for the easiest way to use, resso finally embarked on this API design:

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

const { count } = store; // get
store.count++; // set
Enter fullscreen mode Exit fullscreen mode

6. How to use resso?

Get store

Because the store data is injected into the component using useState, it needs to be destructure first (destructure means calling useState), destructure at the top level of the component (Hooks rules, cannot be written after if), and then use, otherwise there will be a React warning.

Set store

Assignment to the first level of the store data will trigger the update, and only the assignment of the first level will trigger the update.

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

store.obj.num = 10; // ❌ does not trigger update (valtio supports this way)
Enter fullscreen mode Exit fullscreen mode

resso does not support the writing method like valtio, mainly due to the following considerations:

  1. It is need to traverse all the data deeply to proxy, and when updating the data, it needs to be proxy first, which will cause a certain performance loss. (resso only proxy store once at the initialization.)
  2. Because all data are proxy, it is not friendly when printed in the Chrome console, which is a big problem. (resso does not because only the store is a proxy, and generally prints the data in the store.)
  3. If the sub-data is destructured, such as obj, obj.num = 10 can also trigger the update, which will cause the data source to be opaque, and it is uncertain whether it comes from the store and whether the assignment triggers the update. (The main body of resso is always from the store, and the source is clear.)

7. Simple. Not chaos

The above is the design concept of resso and some implementations of a React state manager.

At the end of the day, the React state manager is a tool, React is a tool, JS is a tool, programming is a tool, and the job itself is a tool.

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

So, why not make it simpler?

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

So, why not make it simpler?

Chic. Not geek.

iMac

Simplicity is everything.

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

Latest comments (0)