Two-way binding will make your React code better.

vaukalak

vaukalak

Posted on January 14, 2022

Two-way binding will make your React code better.

Two-way binding allows to create synchronisation between 2 entities, for example application data and view. React out of the box, provides an api to get one-way binding. When we want to mutate the state, we need explicitly call updating callback:

const UserName = ({ name, onChange }) => {
  return <input onChange={onChange} value={name} />;
};

const App = () => {
  const [user, setUser] = useState({ name: "" });
  return <UserName
    name={name}
    onChange={(e) => setUser({ name: e.target.value})}
  />;
};
Enter fullscreen mode Exit fullscreen mode

This is done to provide owner to child update experience. So when the root state of the app gets updated, changes will propagate to children. This makes application data flow clear and predictable, however increases the amount of code to write.
In order to make two-way binding match with react update philosophy, I've built the library called mlyn. The main paradigm is that every piece of the state is readable and writable. However when you write to it, the change will bubble to the root of the state, and the root state will be updated:

// trailing $ suggests that the value is observable
const UserName = ({ name$ }) => {
  return <Mlyn.Input bindValue={name$} />;
};

const App = () => {
  const user$ = useSubject({ name: "" });
  return <UserName name$={user$.name} />;
};
Enter fullscreen mode Exit fullscreen mode

That's it, the engine will update the state in the same way as on the plain react example above.
User name update

However two-way binding is not limited to the communication with UI. You can easily bind your value to the local storage. Let say you have a hook that accepts a portion of mlyn state, and target local storage key:

const useSyncronize = (subject$, key) => {
  useEffect(() => {
    // if state exists write that to the state.
    if (localStorage[key]) { 
      subject$(JSON.parse(localStorage[key]));
    }
  }, []);
  useMlynEffect(() => {
    // whenever state is updated, update localStorage
    localStorage[key] = JSON.stringify(subject$());
  });
};
Enter fullscreen mode Exit fullscreen mode

Now you can easily bind user name to it:

useSyncronize(user$.name, "userName");
Enter fullscreen mode Exit fullscreen mode

Note that you don't need to create/pass any callbacks to update the value, it just works.
Another use-case is when you want to make changes of the state undoable / re-doable. Once again, just pass this state to the appropriate history management hook.

const history = useHistory(state$.name);
Enter fullscreen mode Exit fullscreen mode

The history object will contain the api to jump to any step of the state. However, it's a bit customized 2-way binding. Whenever state gets updated, the value is pushed to the history:
Snapshots are pushed to the history
When a history entry is selected, this entry is written back to the state:
Snapshot to state flow

And note again that we don't write custom boilerplate for the state update, just connecting the dots.

Check this code sandbox with history management for a TodoMVC app:
TodoMVC with history

For more examples on 2-way binding and mlyn visit react-mlyn repo.

💖 💪 🙅 🚩
vaukalak
vaukalak

Posted on January 14, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related