Getting started with ES6 and React — by building a Minimal Todo App

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

  1. Setting up the Project Folder
  2. Setting up Webpack
  3. Setting up Babel
  4. Writing our first React Component
  5. Setting up a data interface to store and manipulate our todos
  6. 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.

  1. We are telling Webpack to start bundling everything it finds, using the file ./src/index.js as an entry point…
  2. …and then store the bundled output into the bundle.js file located in the ./dist directory;
  3. 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 a package.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. 😃