Problem
React is a very powerful library for building interactive JavaScript applications. One of its strongest qualities is that it forces you to build your app in a uni-directional data flow. This is much different from other frameworks' two-way data binding. With React, every piece of your application is controlled from a single source, it's owner. The owner is defined by the Component
that is passing props
to its owned component.
React component interaction comes in two forms: flow of data from parent to child component and flow of data from child to parent. To achieve parent to child data flow we use props
when create child component in the parent component. To achieve child to parent data flow in React, we use handlers passed in to the child component via the parent component as props
. The parent knows that such activity could occur in it's child so it sets up a handler for when the activity occurs.
As more nested components are added, this logic can grow out of hand and can make it difficult to keep track of events passed through the component hierarchy.
Also, if you decide to add new features, this inconvenience will quickly become a maintenance nightmare:
Application > ComponentA > ComponentB > ComponentC > ComponentD > EndComponent
To prevent this from happening, we're going to solve two problems:
We'll rethink of how data flows inside our application with the help of Flux. We'll be building our application with the help of ES6 and Babel.
Flux coming
Flux is the application architecture from Facebook that complements React. It's not a framework or a library, but rather a solution to a common problem; how to build scalable client-side applications.
With the Flux architecture, we can rethink how data flows inside our application. Flux makes sure that all our data flows only in a single direction. This helps us reason about how our application works, regardless of how small or large it is. With Flux, we can add a new functionality without exploding our application's complexity.
With Flux, we separate the concerns of our application into four logical entities:
Actions are objects that we create when our application's state changes. For example, when our application receives a new data, we create a new action. An action object has a type
property that identifies what action it is and any other properties that our application needs to transition to a new state. Here is an example of an action object:
let action = { type: "CREATE_TODO", title: "Go shopping" };
As you can see, this is an action of type
CREATE_TODO, and it has the title
property, which is a new Todo
object that our application has received. You can guess in which case this action is created by looking at its type
. For each new Todo
that our application receives, it creates a CREATE_TODO action.
Where does this action go? What part of our application gets this action? Actions are dispatched to stores.
Stores are responsible for managing your application's data. They provide methods for accessing that data, but not for changing it. If you want to change data in stores, you have to create and dispatch an action.
The dispatcher is responsible for dispatching all the actions to all stores:
This is how our data flow looks like:
You can see that the dispatcher plays a role of a central element in our data flow. All actions are dispatched by it. Stores register with it. All the actions are dispatched synchronously. You can't dispatch an action in the middle of the previous action dispatch. No action can skip the dispatcher in the Flux architecture.
Project structure is following
I'm going to use settings and index.html
from previous tutorial Hello world in ReactJS and Node.js as background.
Difference is entry
key in webpack.config.js
... entry: [ './components/app.js' ], ...
Also I'm going to use react-icons and FontAwesome for flavor.
npm i react-icons --save-dev
Result
Following is components/app.js
import React from 'react'; import {render} from 'react-dom'; import {Todos} from './Todos' class App extends React.Component { render() { return ( <div> <h1>My todo</h1> <hr/> <Todos/> </div> ); } } render(<App />, document.getElementById('root'));
Following is components/Todos.js
import React from 'react'; import TodoStore from '../stores/TodoStore'; import * as TodoActions from '../actions/TodoActions'; import {FaCheckCircleO,FaCircleO} from 'react-icons/lib/fa'; export class Todo extends React.Component { render() { return ( <li>{this.props.completed ? <FaCheckCircleO/> : <FaCircleO/>} {this.props.title}</li> ) } } export class Todos extends React.Component { constructor() { super(); this.state = { title: "", todos: TodoStore.getAll() } } componentWillMount() { TodoStore.on("change", () => { this.setState({todos: TodoStore.getAll()}); }) } createTodo(e) { e.preventDefault(); TodoActions.createTodo(this.state.title); this.setState({title: ""}); } handleChange(event) { this.setState({title: event.target.value}); } render() { const {todos} = this.state; const TodoComponents = todos.map((todo) => { return <Todo key={todo.id} {...todo}/>; }); return ( <div> <ul>{TodoComponents}</ul> New todo: <input type="text" ref="titleTask" value={this.state.title} onChange={this.handleChange.bind(this)} /> <button onClick={this.createTodo.bind(this)}>Add!</button> </div> ) } }
Creating a dispatcher
Now let's implement Flux data flow. We'll start by creating a dispatcher first. Facebook offers us its implementation of a dispatcher that we can reuse. Let's take advantage of this.
Navigate to your project directory and run the following command
npm i flux --save-dev
The flux
module comes with a Dispatcher
function that we'll be reusing.
Next, create a new file called dispatcher.js
in our project's root directory.
import {Dispatcher} from "flux"; export default new Dispatcher;
First, we import Dispatcher
provided by Facebook, then create, and export a new instance of it. Now we can use this instance in our application.
Next, we need a convenient way of creating and dispatching actions. For each action, let's create a function that creates and dispatches that action. These functions will be our action creators.
Creating an action
Let's create a new folder called actions
in our project's root directory. Then, create the TodoActions.js
file in it:
import dispatcher from "../dispatcher"; export function createTodo(title) { dispatcher.dispatch({ type: "CREATE_TODO", title }); }
Our action will need a dispatcher
to dispatch the actions. Then, we create our first action createTodo()
.
The createTodo()
function takes the title
as an argument, and creates the action object with a type
property set to CREATE_TODO
.
Finally, the createTodo()
action creator dispatches our action object by calling the dispatch()
method on the dispatcher
object. The dispatch()
method dispatches the action object to all the stores registered with the dispatcher
.
So far, we've created dispatcher
and action createTodo
. Next, let's create our first store.
Creating a store
As we learned earlier, stores manage data in your Flux architecture. They provide that data to the React components. We're going to create a simple store that manages a new Todo
that our application receives from simple form.
Create new folder called stores
in our project's root directory. Then, create the TodoStore.js
file in it:
import {EventEmitter} from "events"; import dispatcher from "../dispatcher"; class TodoStore extends EventEmitter { constructor() { super(); this.todos = [ { id: 1, title: "Go shopping", completed: false }, { id: 2, title: "Go for a walk", completed: true } ] } createTodo(title) { const id = Date.now(); this.todos.push({ id, title, complete: false }); this.emit("change"); } getAll() { return this.todos; } handleAction(action) { switch (action.type){ case "CREATE_TODO": { this.createTodo(action.title) } } } } const todoStore = new TodoStore; dispatcher.register(todoStore.handleAction.bind(todoStore)); export default todoStore;
The TodoStore.js
file implements a simple store. We can break it into four logical parts:
TodoStore
object with methods.handleAction(action)
and registering a store with a dispatcher
.We import the EventEmitter
class to be able to add and remove event listeners from our store. TodoStore
manages a simple Todo
object that we initially set to simple list.
Stores are in full control of managing their data. They only allow other parts in our application to read that data, but never write to it directly. Only actions should mutate data in the stores.
The third logical part of the TodoStore.js
file is creating an action handler and registering the store with a dispatcher
.
When the store changes its data, it needs to tell everyone who is interested in the data change. For this, it calls emit()
function that emits the change
event and triggers all the event listeners created by other parts in our application.
Outcome
Revisit the Flux architecture and take a look at a bigger picture of how it works: