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})}
/>;
};
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} />;
};
That's it, the engine will update the state in the same way as on the plain react example above.
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$());
});
};
Now you can easily bind user name to it:
useSyncronize(user$.name, "userName");
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);
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:
When a history entry is selected, this entry is written back to the state:
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:
For more examples on 2-way binding and mlyn
visit react-mlyn repo.
Top comments (0)