React, one-way data flow revealed
We are familiar with two-way data flow i.e. changes in model updates our view and vice versa, this is how we used to think when designing the architecture of our application. Some of us may even ask, how is one-way data flow even possible, events happened in the view should update the model as well, so that all the information will be in sync. To understand one-way data flow especially for developers coming from MVC background, we need to discuss some powerful features in React that make the impossible possible.
If I would ask anyone about one thing they know in React, I guess it will be "The Virtual DOM", because it sounds so powerful, so marketable and revolutionary - yes, it truly is. In React, what we code is actually building up the virtual DOM and we don't deal directly in HTML. Our job is to build up the virtual DOM, and let React compare it with the browser DOM, find the most efficient way to update the browser DOM by using the diffing algorithm, and magically render our web page. One thing to emphasize here, building up the virtual DOM is fast, and this opens up a whole new way of architecting React application which is known as Flux.
Flux is an idea, Redux is its implementation.
Before we go any further, it is worthwhile to mention that Flux is just an idea, an architecture pattern, you can implement the entire concept your own way, or use the well-known library, Redux. The idea behind Flux is to let React re-render whenever there are changes to the model, React doesn't need to know what has changed, it just needs a trigger for rerendering. Let's stop and think for a second, since React together with its diffing algorithm able to update the browser DOM in an optimized way, we can now safely rerender once we have done the necessary update of our data and do not have to worry about expensive repaints, and this is the essence in the one-way data flow.
And just like what I said earlier, we can build the entire one-way data flow using just plain vanilla Javascript without using any external library. Let's discuss this based on a simple example.
import React, { useState } from "react"; export default function() { const [value, setValue] = useState(""); const handleChange = event => { setValue(event.target.value); }; return ( <div> <input type="text" onChange?={handleChange} /> <p>{value}</p> </div> ); }
This simple React component simply output what we typed in a new paragraph, so how can we make this work using Flux architecture? Let's rewrite the whole thing from scratch using a different approach.
Flux has the concept of single Dispatcher and Event Emitter, where Dispatcher is used to dispatch information to the subscribers and Event Emitter let you listen to event and emit new event.
eventemitter.js
export default class EventEmitter { constructor() { this.events = {}; } on(eventName, callback) { this.events[eventName] = this.events[eventName] || []; this.events[eventName].push(callback); } emit(eventName, value) { if (this.events[eventName]) { this.events[eventName].forEach(cb => { cb(value); }); } }
}
The idea of Event Emitter is straight forward, listening to an event using on() method and emit event using emit() method, and this will invoke all functions related to the event.
dispatcher.js
class Dispatcher { constructor() { this.callbacks = []; } register(callback) { this.callbacks.push(callback); } dispatch(action) { this.callbacks.forEach(cb => { cb(action); }); } } export default new Dispatcher();
dispatcher works the same way, registering functions using register() method, dispatch action to all registered functions using dispatch() method.
store.js
import EventEmitter from "../eventemitter"; const Store = new EventEmitter(); Store.value = ""; Store.changeValue = function(value) { this.value = value; this.emit("change", value); }; Store.addEventListener = function(eventName, callback) { this.on(eventName, callback); };
export default Store;
To have a single source of truth, we only create one store and Store needs to be EventEmitter because it needs to tell React (emit "change" event) to rerender once its internal data has changed. Look closely at the changeValue() method attached to the Store, it emits an event after setting a new value.
reducer.js
import Store from "../store"; export function TextReducer(action) { switch (action.type) { case "UPDATE_TEXT": { Store.changeValue(action.payload); } } }
The idea here is to receive the appropriate action before updating our store. And the next thing we need to do is to register this method to the dispatcher, so that whenever the dispatcher dispatches new action, TextReducer() method is able to receive it and act on it based on the action's type.
actions.js
import Dispatcher from "../dispatcher"; export function updateText(text) { Dispatcher.dispatch({ type: "UPDATE_TEXT", payload: text }); }
We would like to dispatch an action of type "UPDATE_TEXT" and have that automatically reach our TextReducer() so that we can change the value in the store. The very last step involves triggering React to rerender when we updated our store value.
App.js
import React, { useState, useEffect } from "react"; import Store from "../store"; import Dispatcher from "../dispatcher"; import { TextReducer } from "../reducers"; import * as Actions from "../actions"; export default function() { const [value, setValue] = useState(Store.value); Dispatcher.register(TextReducer); const handleChange = event => { Actions.updateText(event.target.value); }; useEffect(() => { Store.addEventListener("change", function(val) { setValue(val); }); }, []); return ( <div> <input type="text" onChange?={handleChange} /> <p>{value}</p> </div> );
}
To finish the one-way data flow, let React listens to the "change" event and rerender. When will the store emit a "change" event? It is only when changeValue() method invoked, results in the updated store.
That's it, this achieves the same functionality but with greater complexity, but does it worth it? Definitely Yes for larger project where changes in one place requires notification to other places in your code, this greatly helps in maintainability of your code because data can be tracked easily with one-way data flow, even new developer can understand your code easily.
You might have noticed some terminologies I used which is the same in Redux. Yes, whatever you used in Redux can be built using plain vanilla Javascript, so do not memorize, understand and it will make you think differently and become a better developer.
Principle Software Engineer / Dot Net / fullstack developer / Educator
5 年Congrats??