Getting started with ES6 and React — by building a Minimal Todo App
Building a minimal todo app to explore the concepts of ES6 and React; that can further be used as a seed app to explore the React ecosystem more, sounds like a decent idea, no?
Let's do it! 👍
The app we are building tries to imitate the implementation of this vanilla JS app that I built a long time ago. Feel free to check it out in action here.
True to the name in the title, for now, we will keep the app down and dirty, (read: no styles) and just worry about the implementation.
Although this tutorial doesn’t cover the styling of the app; the end product has some basic styling added to it. Here is the repo with the full code; and here is the app in action.
Thanks unto @alkshendra for the added zing (read: styles, ahoy!). 😊
Along the way of this story, if you don't understand any part of the code, or if you have a suggestion to better any part of the code, please post a comment, and I will try to update the story, and code accordingly. 👍
Before we dive into the story, here is an overview of what we will be covering in it
- Setting up the Project Folder
- Setting up Webpack
- Setting up Babel
- Writing our first React Component
- Setting up a data interface to store and manipulate our todos
- Wiring the TodoDataInterface with the React view
Without further ado let's get started.
1. Setting up the Project Folder
Fire up the terminal, make a new directory for the app, and initialise an npm project
$ mkdir minimal-todo-react
$ cd minimal-todo-react
$ npm init -y
This would generate a package.json
file which will also later house all of our dependencies. Now let us create a dist
directory, eventually containing everything that is needed to serve our app.
For now, the contents would just be an index.html
file along with a bundle.js
file containing all the modules we’ll come to use.
From the root directory of the project, run the following commands in the terminal
$ mkdir dist
$ cd dist
$ touch index.html
./dist/index.html
<!DOCTYPE html>
<html>
<head>
<title>Minimal Todo App built with React</title>
</head>
<body>
<div id="app"></div>
<script src="bundle.js"></script>
</body>
</html>
The bundle.js
file will be generated using Webpack
, a module bundler, which will bundle all of our source files into a single JavaScript file. We will also use webpack-dev-server
to serve the content locally.
2. Setting up Webpack
Before diving into the specifics, let's setup webpack
and webpack-dev-server
From the project’s root directory
$ npm install --save-dev webpack webpack-dev-server
This is the directory structure as of now.
├── minimal_todo_react
├── dist
│ └── index.html
├── node_modules
└── package.json
Let's change the package.json
file to change the “start” script to start the webpack-dev-server
./package.json
...
"scripts": {
"start": "webpack-dev-server --progress --colors --config ./webpack.config.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
...
Apart from a couple of other configuration options, we are indicating that the file webpack.config.js
be used as the config file. So, let's go ahead and create it.
From the project’s root directory
$ touch webpack.config.js
./webpack.config.js
module.exports = {
entry: [
'./src/index.js'
],
output: {
path: __dirname + '/dist',
publicPath: '/',
filename: 'bundle.js'
},
devServer: {
contentBase: './dist',
}
};
As we can see above, the Webpack configuration is basically a plain old JavaScrip object. Here is what it means from top to bottom.
- We are telling Webpack to start bundling everything it finds, using the file
./src/index.js
as an entry point… - …and then store the bundled output into the
bundle.js
file located in the./dist
directory; - and for the dev server, use the
./dist
directory to serve the content from.
Let's create the “entry point”…
From the project’s root directory
$ mkdir src
$ cd src
$ touch index.js
./src/index.js
console.log("Webpack is working");
From the project’s root directory
$ npm start
When we run the above command, Webpack will bundle up the contents of the ./src/index.js
file, and creates a bundle.js
file.
By default, webpack-dev-server
starts on localhost:8080
. So, go to http://localhost:8080/
to check if you can see the above console log. If you do, our initial Webpack setup is successful.
Additionally, we can choose to add a “build” script in our package.json file to generate the bundle.js
in the ./dist
folder, and then we can use it along with the other contents in the ./dist
folder to host our app anywhere we want.
./package.json
...
"scripts": {
"start": "webpack-dev-server --progress --colors --config ./webpack.config.js",
"build": "webpack --config ./webpack.config.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
...
This is the directory structure so far.
├── minimal_todo_react
├── dist
│ └── index.html
├── src
│ └── index.js
├── node_modules
├── package.json
└── webpack.config.js
3. Setting up Babel
Since we will be working with ES6, JSX code (React); we would need a compiler to convert all of it into ES5 code. Babel is touted as a tool that helps you to write next-generation JavaScript; which is exactly what we need.
Let's install the required tools, before looking at the purpose behind each of them.
From the project’s root directory
$ npm install --save-dev babel-core babel-loader babel-preset-es2015 babel-preset-react babel-preset-stage-2
- babel-core: This is the babel compiler core
- babel-loader: Since we are using Webpack to bundle up all the files; we need a tool, to convert our ES6 code in to ES5 before the bundling process; and that is what
babel-loader
is for. - babel-preset-es2015 & babel-preset-react: We’re installing the ES2015, and React presets to transpile the ES6, and JSX code — that we would be writing ahead — respectively into ES5 code.
- babel-preset-stage-2: The stage-2 preset consists of the plugins transform-class-properties, and transform-object-rest-spread that would help us do variable assignments outside of the constructor function in ES6 classes, and do rest and spread operations, respectively.
To give Babel info on all the presets, and plugins we are using… let's update our package.json
file. We also need to specify Webpack to run the babel-loader
on our JS files, so it can make use of Babel to transform ES6 and JSX code, before bundling.
Babel will look for a
.babelrc
in the current directory of the file being transpiled. If one does not exist, it will travel up the directory tree until it finds either a.babelrc
, or apackage.json
with a"babel": {}
hash within.
./package.json
...
"author": "",
"license": "ISC",
"babel": {
"presets": [
"es2015",
"react",
"stage-2"
]
},
"devDependencies": {
...
./webpack.config.js
...
entry: [
'./src/index.js'
],
module: {
loaders: [{
test: /\.jsx?$/,
exclude: /node_modules/,
loader: 'babel'
}]
},
resolve: {
extensions: ['', '.js', '.jsx']
},
output: {
...
In the Webpack config file, we are adding the babel-loader
(we can exclude the ‘-loader’ part), and instructing Webpack to transform all the files containing the .jsx
extension; excluding the ones that are present in the node_modules
directory.
We’ve also added a resolve.extensions
property so that we don’t have to specify extensions while importing files.
Before we go on to write our first React component, let's install the react packages, along with lodash. Lodash is a JavaScript utility library that will later come handy.
From the project’s root directory
$ npm install --save react react-dom lodash
With the above step, we have setup everything we need to get started with the code. Finally!
4. Writing our first React Component
Let's go ahead and edit the ./src/index.js
file and render our first react component onto the DOM.
./src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
class TodoApp extends React.Component {
render() {
return (
<h2>Down and Dirty TodoApp built with React</h2>
);
}
}
ReactDOM.render(<TodoApp />, document.getElementById('app'));
Putting it simply, every React component has at the least a render
function, that returns a React element or an element tree, based on how we define our component. In the above case, it is just returning a simple React element with the type h2
.
Even though it resembles HTML, the line inside the render()
function is JSX
. Babel’s react preset will convert it into a React.createElement()
call.
With the ReactDOM.render()
function, we are telling React to render everything returned by the ToDoApp
component, inside the div
element with an id
equal to "app"
.
From the project’s root directory
$ npm start
If everything has worked perfectly, we should see the above <h2>
tag rendered on our page. 👏🏼
5. Setting up a TodoDataInterface
Let's setup an interface for storing, and manipulating our “Todo” data.
From the project’s ./src
directory
$ mkdir lib
$ cd lib
$ touch Todo.js
./src/lib/Todo.js
// Helper function for generating unique IDs
function guidGenerator() {
function S4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
return S4()+S4()+'-'+S4()+'-'+S4()+'-'+S4()+'-'+S4()+S4()+S4();
}
export default class Todo {
constructor(descriptionText, isDone, id) {
this.descriptionText = descriptionText || '';
this.isDone = isDone || false;
this.id = id || guidGenerator();
}
}
We are exporting the Todo
class as the default object, so we could later import it with an import statement as such: import Todo from ./Todo.js
Had we not used the default
keyword, we would have to change our import statement to: import { Todo } from ./Todo.js
.
ES6 gives us the power to export multiple objects from a file. If we had one more module being exported from the Todo.js
file, we would simply do this: import { Todo, OneMoreClass } from ./Todo.js
Each Todo
object when instantiated, (new Todo()
) will have all these properties: descriptionText
, isDone
, and id
; all of which are optional; and will have the specified default values if we don’t specify them while instantiation.
We are using a helper function guidGenerator()
, to generate unique IDs for our Todo
objects.
Now let's create an interface to interact and manipulate an array of the above Todo
objects.
From the project’s ./src/lib
directory
$ touch TodoDataInterface.js
./src/lib/TodoDataInterface.js
import Todo from './Todo';
import { findIndex } from 'lodash';
export default class TodoDataInterface {
constructor() {
this.todos = [];
}
addTodo(descriptionText) {
const newTodo = new Todo(descriptionText);
this.todos.push(newTodo);
return newTodo;
}
archiveToggleTodo(todoId) {
const todoIndex = findIndex(this.todos, (todo) => todo.id === todoId);
if (todoIndex > -1) {
this.todos[todoIndex].isDone = !this.todos[todoIndex].isDone
}
}
removeTodo(todoId) {
const todoIndex = findIndex(this.todos, (todo) => todo.id === todoId);
if (todoIndex > -1) {
this.todos.splice(todoIndex, 1);
}
}
getAllTodos() {
return this.todos.map(todo => todo);
}
}
When a new TodoDataInterface
(const dataInterface = new TodoDataInterface()
) is instantiated, we have an access to the todos
array (dataInterface.todos
), which as earlier mentioned will be our store point for our Todo
objects; and along with it all the methods to manipulate it (dataInterface.addTodo("Finish writing the ES6 + React article")
). Simple!
6. Wiring the TodoDataInterface with the React view
./src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import TodoDataInterface from './lib/TodoDataInterface';
import TodoApp from './components/TodoApp';
const todoDataInterface = new TodoDataInterface();
ReactDOM.render(
<TodoApp dataInterface={todoDataInterface}/>,
document.getElementById('app')
);
Note that I’ve moved the TodoApp
component into a components
folder, which also consists of a couple of other components that are used in the TodoApp
component.
In the above file, we have instantiated a new TodoDataInterface
object, and it is being passed into the TodoApp
component as a “prop”. We’d be able to access it with this.props.dataInterface
inside the TodoApp
component.
Here is the directory structure of our app so far.
├── minimal_todo_react
├── dist
│ └── index.html
├── src
│ ├── components
│ ├── SingleTodo.js
│ ├── TodoApp.js
│ └── VisibleTodoList.js
│ ├── lib
│ ├── Todo.js
│ └── TodoDataInterface.js
│ └── index.js
├── node_modules
├── package.json
└── webpack.config.js
If you haven’t already, make a components
directory, and create individual files for all of the components inside it.
From the project’s ./src
directory
$ mkdir components
$ cd components
$ touch SingleTodo.js
$ touch VisibleTodoList.js
$ touch TodoApp.js
Let's take a look at the code for all the components. SingleTodo
component returns a simple element tree with a li
type React element at the top.
./src/components/SingleTodo.js
import React from 'react';
export default class SingleTodo extends React.Component {
render() {
return (
<li>
<input
data-id={this.props.todoId}
checked={this.props.isDone}
onChange={this.props.archiveToggleTodo}
type="checkbox"
/>
<label>{this.props.text}{this.props.isDone? " - DONE": ""}</label>
<button
data-id={this.props.todoId}
onClick={this.props.removeTodo}>
Delete
</button>
</li>
);
}
}
Notice that all the data, and functions for handling events (this.props.archiveToggleTodo
for the onChange
on the checkbox
input, and this.props.removeTodo
for the onClick
on the “Delete” button) are being passed down from a parent, the VisibleTodoList
component, as props.
Speaking of the VisibleTodoList
component, let's take a look at its code.
./src/components/VisibleTodoList.js
import React from 'react';
import SingleTodo from './SingleTodo';
export default class VisibleTodoList extends React.Component {
render() {
return (
<div>
// visibilityFilter could be either of the following values:
// "ALL_TODOS", "LEFT_TODOS", or "COMPLETED_TODOS"
<h3>{this.props.visibilityFilter.replace("_", " ")}</h3>
{this.props.visibleTodos.length > 0?
(
<ul>
{this.props.visibleTodos.map(
(todo) =>
<SingleTodo
key={todo.id}
todoId={todo.id}
text={todo.descriptionText}
isDone={todo.isDone}
archiveToggleTodo={this.props.archiveToggleTodo}
removeTodo={this.props.removeTodo}
/>
)}
</ul>
):
(
"No Todos to show"
)
}
</div>
);
}
}
We see that it is returning SingleTodo
components by looping over the visibleTodos
array that it is getting as a prop from its parent, the TodoApp
component.
Notice that the event handler functions we have seen in the SingleTodo
component, that have been passed down to it as props; are being passed down as props for the VisibleTodoList
component too (archiveToggleTodo
and removeTodo
).
Coming down to the main component in play, TodoApp
, here is the code for it.
./src/components/TodoApp.js
import React from 'react';
import VisibleTodoList from './VisibleTodoList';
export default class TodoApp extends React.Component {
constructor(props) {
super(props);
this.visibilityFilters = ["ALL_TODOS", "LEFT_TODOS", "COMPLETED_TODOS"]
this.state = {
todos: this.props.dataInterface.getAllTodos(),
visibilityFilter: "ALL_TODOS"
};
}
addTodo = () => {
if (this._todoInputField.value) {
this.props.dataInterface.addTodo(this._todoInputField.value);
this.setState({todos: this.props.dataInterface.getAllTodos()});
this._todoInputField.value = '';
}
}
archiveToggleTodo = e => {
this.props.dataInterface.archiveToggleTodo(e.target.dataset.id);
this.setState({todos: this.props.dataInterface.getAllTodos()});
}
removeTodo = e => {
this.props.dataInterface.removeTodo(e.target.dataset.id);
this.setState({todos: this.props.dataInterface.getAllTodos()});
}
changeVisibilityFilter = e => {
this.setState({visibilityFilter: e.target.dataset.id});
}
visibleTodos = () => {
switch (this.state.visibilityFilter) {
case "ALL_TODOS":
return this.state.todos;
case "LEFT_TODOS":
return this.state.todos.filter(todo => todo.isDone === false);
case "COMPLETED_TODOS":
return this.state.todos.filter(todo => todo.isDone === true);
default:
return this.state.todos;
}
}
render() {
let visibleTodos = this.visibleTodos();
return (
<div>
<h2> Down and Dirty TodoApp built with React </h2>
<input
type="text"
placeholder="What do you want todo?"
ref={(c => this._todoInputField = c)}
/>
<button onClick={this.addTodo}>Add Todo</button>
<VisibleTodoList
visibleTodos={visibleTodos}
visibilityFilter = {this.state.visibilityFilter}
archiveToggleTodo={this.archiveToggleTodo}
removeTodo={this.removeTodo}
/>
<div>
SHOW:
{
this.visibilityFilters.map(
visibilityFilter =>
<button
key={visibilityFilter}
onClick={this.changeVisibilityFilter}
data-id={visibilityFilter}>
{visibilityFilter.replace("_", " ")}
</button>
)
}
</div>
</div>
);
}
}
There is a lot going on here, but I will try to explain it as concisely as possible. This version of the TodoApp
component has state, which is being defined in the constructor()
.
Along with a todos
array, the state also handles a visibilityFilter
string that has one of the following values: "ALL_TODOS"
, "LEFT_TODOS"
, or "COMPLETED_TODOS"
… pretty self-explanatory!
To put it crudely, whenever the state of our component changes, the render()
function of that component is called. If the render()
function contains child React components, their render()
functions will be called too.
So, when I click the “Add Todo” button, its handler function addTodo
is fired. Notice how that function is updating the todos
array via the dataInterface
prop, and then setting the state with an updated list of todos
it gets from the appropriate function in the dataInterface
prop.
All other functions for manipulating a specific Todo
object, are being passed down as props to the VisibleTodoList
component which in turn passes it down to the SingleTodo
component.
That’s it. One way data flow right from parent to the child, and maintaining minimal state, are a couple of core React ideas.
If you are still unclear somewhere, it is A-okay; especially if you are new to React, and ES6. Read this article which explains “Thinking in React” in a better detail.
You can also comment here if you have any questions, and I’ll do my best to answer them.
If you have made it until here reading it all, kudos! Stay tuned for the future installments. Next up would be adding a store management system. See you soon, in the next story. 😃