Introducing mlyn - new state management for React
Tags: javascript, react, solidjs, mlyn
Published: 2021-11-22 22:18:24

Impressed by fine-grained reactivity concept from solid-js, I've tried to build a library that brings it to react. Some react issues I was going to solve where:

* Provide possibility to re-render just those elements, which related data has changed.
* Enable easy 2-way binding, however maintaining unidirectional data flow.
* Remove necessity to overflow the code by explicitly mentioning all dependencies, as we currently do with `useEffect`, `useCallback` and `useMemo`.
* Issues with encapsulation and modularisation when using redux or context as state management (I ❤️ redux btw).

Now I'm going to present you main concepts of the library within a TodoMVC app example. You can find full source code [here](https://codesandbox.io/s/react-mlyn-todo-mvc-owecw). Note that example fits in less than 60 lines of code.

First of all let define our component:
```ts
export const App = seal(() => {
  // ...
});
```
`seal` is an import from `react-mlyn`, it's a wrapper of `React.memo`, which compare function always returns `true`. Which means, component should never re-render by incoming properties change (those are not supposed to ever change). All children re-renders will be triggered by *mlyn* reactivity system.
Now let define the state:
```ts
const state$ = useSubject({
  todos: [],
  newTitle: ""
});
```
`useSubject` is a react-hook, that will convert initial state to a `subject`. A subject in mlyn is a proxy object, which can we used in 4 different ways:
* you can read from it:
```ts
// will return actual state
state$();
```
* you can write to it:
```ts
// will set `newTitle` to `hello`
state$({
  ...state$(),
  newTitle: "hello",
}); 
```
* you can subscribe to it:
```ts
useMlynEffect(() => {
  // will log the `state$` value every time it's updated
  console.log(state$());
});
```
By reading `state$` inside of `useMlynEffect` hook we automatically set it as a dependency, which will re-run the hook every time `state$` has been updated.
* you can lens it:
```ts
state$.newTitle("hello");
state$.newTitle(); // hello
state$(); // { newTitle: "hello", todos: [] }
```
Every lens behave like a subject, but when updated bubbles an immutable update to the root subject. Also within lens you can subscribe to updates of just a portions of the state.

Now let go back to our TodoMVC app, let create a synchroniser of todos to the local storage:
```ts
// this hook accepts a subject and a string key for localstorage
const useSyncronize = (subject$, key) => {
  // if localStorage already contains info for that key,
  // let write it to `subject$` as initial state
  if (localStorage[key]) {
    const preloadedState = JSON.parse(localStorage[key]);
    subject$(preloadedState);
  }
  // create a subscription to `subject$` and write
  // write it to localStorage when updated
  useMlynEffect(() => {
    localStorage[key] = JSON.stringify(subject$()); 
  });
};
```
Invocation of this hook in the component code:
```ts
// create a lens to `state$.todos` and
// bind it to localStorage `todos` key.
useSyncronize(state$.todos, "todos");
```
Let create methods for adding / deleting todos:
```ts
const addItem = () => {
  state$({
    todos: [
      // remember to use `()` when reading from a subject.
      ...state$.todos(),
      {
        title: state$.newTitle(),
        createdAt: new Date().toISOString(),
        done: false
      }
    ],
    newTitle: ""
  });
};
```
This looks very similar to normal react update, but you don't need to wrap it with `useCallback` since with `mlyn` component is not going to be re-rendered.

```ts
const removeItem = (i) => {
  state$.todos([
    ...state$.todos().slice(0, i),
    ...state$.todos().slice(i + 1)
  ]);
};
```
Note that since here you need to update just `todos` you can directly write to `state$.todos` without taking care of rest of the state. This is very handy, when passing a lens as a property to a child.
And finally jsx:
```ts
return (
  <>
    <h3>Simple Todos Example</h3>
    <Mlyn.input
      type="text"
      placeholder="enter todo and click +"
      bindValue={state$.newTitle}
    />
    <button onClick={addItem}>+</button>
    <For
      each={state$.todos}
      getKey={({ createdAt }) => createdAt}
    >
      {(todo$, index$) => (
        <div>
          <Mlyn.input type="checkbox" bindChecked={todo$.done} />
          <Mlyn.input bindValue={todo$.title} />
          <button onClick={() => removeItem(index$())}>x</button>
        </div>
      )}
    </For>
  </>
);
```
Notice that for inputs we use special tag `Mlyn.input` it has some properties which enables subscriptions to mlyn reactivity. One of those is `bindValue`. When you pass `state$.newTitle` to it, it will both update the input when the `newTitle` is updated, and write to `newTitle` when input is changed. In short, this is 2-way binding.
```ts
<Mlyn.input
  type="text"
  placeholder="enter todo and click +"
  bindValue={state$.newTitle}
/>
```
Now let analyse how the `For` component, that is used to display collections works:
```ts
<For
  // pass subject which holds array to display
  each={state$.todos}
  // key extractor, it's used not only by react reconciliation,
  // but also by `For` component logic.
  getKey={({ createdAt }) => createdAt}
>
  {(todo$, index$) => (
    <div>
      <Mlyn.input type="checkbox" bindChecked={todo$.done} />
      <Mlyn.input bindValue={todo$.title} />
      <button onClick={() => removeItem(index$())}>x</button>
    </div>
  )}
</For>
```
The first parameter `$todo` of function child prop is still a 2-way lens. Which means, by updating it, you'll update `todos` array and in general entire state. So writing:
```ts
todo$.title("new value");
```
Is like writing something similar to bellow in plain react:
```ts
setState({
  ...state,
  todos: state.todos.map(item => {
    if (getKey(item) === getKey(todo)) {
      return { ...item, title: "new value" };
    }
    return item;
  }),
});
```
You probably noticed that one input is a checkbox toggle for boolean value:
```ts
<Mlyn.input type="checkbox" bindChecked={todo$.done} />
```
`bindChecked` is similar to `bindValue` but it creates 2-way binding for a boolean subject value to input `checked` field.