Introducing mlyn - new state management for React

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. Note that example fits in less than 60 lines of code.
    First of all let define our component:
    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:
    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:
  • // will return actual state
    state$();
  • you can write to it:
  • // will set `newTitle` to `hello`
    state$({
      ...state$(),
      newTitle: "hello",
    });
  • you can subscribe to it:
  • 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:
  • 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:
    // 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:
    // create a lens to `state$.todos` and
    // bind it to localStorage `todos` key.
    useSyncronize(state$.todos, "todos");
    Let create methods for adding / deleting todos:
    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.
    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:
    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.
    <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:
    <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:
    todo$.title("new value");
    Is like writing something similar to bellow in plain react:
    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:
    <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.

    30

    This website collects cookies to deliver better user experience

    Introducing mlyn - new state management for React