How to build simple React app (Part 2)

In previous part of How to build simple React app we had set up basic boiler-plate for our ToDo application. In this part we will:

  • start building our application logic,
  • introduce actions and reducers,
  • finish our todo page

Let's start coding!

Writting new components for handling todos

On start we will focus only on functionality, style will be added later. So for our todos we will create TodoList component, which will render Todo components for each todo it gets. So let's look at TodoList component.

// src/components/Home/TodoList/TodoList.jsx

import React from 'react';
import PropTypes from 'prop-types';

import Todo from './Todo/Todo';
import AddTodo from './AddTodo/AddTodo';


const TodoList = ({ todos, setTodoDone, deleteTodo, addTodo }) => (
  <div className="todos-holder">
    <h1>Todos go here!</h1>
    <AddTodo addTodo={addTodo} />
    <ul className="todo-list">
      {todos.map((todo) => <Todo key={`TODO#ID_${todo.id}`} todo={todo} setDone={setTodoDone} deleteTodo={deleteTodo} />)}
    </ul>
  </div>
);

TodoList.propTypes = {
  todos: PropTypes.arrayOf(PropTypes.shape({
    id: PropTypes.number.isRequired,
    task: PropTypes.string.isRequired,
    done: PropTypes.bool.isRequired
  })).isRequired,
  setTodoDone: PropTypes.func.isRequired,
  deleteTodo: PropTypes.func.isRequired,
  addTodo: PropTypes.func.isRequired
};

export default TodoList;    

Pretty straight forward component, written as dumb component (if you recall, in previous part I recommended writing all components as dumb on beginning). It has heading, AddTodo component, which we will take a look into in a moment, and one unordered list in which all todos are rendered, in form of Todo component.

New part here is usage of prop-types. Prop-types gives us possibility of type checking. Its main idea is to define types of props component will receive, which gives you more clarity when writing component, and more verbosity when debugging (for example if something marked as required is not set, you will see console error for that, or if something is sent, but type doesn't match, you will also see console error). More about prop-types and rules for writing them you can find here. We defined "todos" as array of objects having shape as described, and marked that array as required. Shape of each todo is described by id number required value, task as required string, and done required boolean flag. addTodo, setTodoDone and deleteTodo are props defined as functions and all required.

Don't worry for now from where TodoList will get its props, we will get to that later, for now just note that we are assuming that those props are passed to component from somewhere.

Next component we obviously need is AddTodo component. Let's take a look at AddTodo implementation.

// src/components/Home/TodoList/AddTodo/AddTodo.jsx

import React, { Component } from 'react';
import PropTypes from 'prop-types';


class AddTodo extends Component {

  static propTypes = {
    addTodo: PropTypes.func.isRequired
  }

  constructor(props) {
    super(props);

    this.state = {
      task: ''
    };

    this.changeTaskText = this.changeTaskText.bind(this);
    this.submitTask = this.submitTask.bind(this);
  }

  changeTaskText(e: Event) {
    e.preventDefault();  // optional, not necessary in this case, but for consistency

    this.setState({ task: e.target.value });
  }

  submitTask(e: Event) {
    e.preventDefault();  // optional, not necessary in this case, but for consistency

    this.setState({ task: '' });
    this.props.addTodo(this.state.task);
  }

  render() {
    return (
    <div>
      <input type="text" onChange={this.changeTaskText} value={this.state.task} placeholder="Task text" />
      <button onClick={this.submitTask}>Add Todo</button>
    </div>
    );
  }
}

export default AddTodo;

This component is written in class form, because it uses internal state. Generally component internal state should be avoided, because it makes testing harder, separates component from global application state (which is main idea behind redux/flux), but here it is implemented this way, mainly to show one component written through class.

AddTodo component, as we already said, have its internal state storing task text (which is read from input field), and two custom methods (functions) changeText and submitTask. changeText method is triggered on any change event inside input field, while submitTask is triggered only on Add Todo button click. Both methods are simple ones, changeText just sets internal state task to received text, and submitTask restarts text inside internal state, and submits current text (from internal state) through only prop component received, addTodo. Interesting thing here is order of actions, it first restarts text, and then submits text which is inside state, but it still works as it is supposed to. How? Component's setState method is async method, which means that it won't change state immediately, but in next process tick, so we can do something like that. You should probably reverse order of this two lines, just for clarity, I just wanted to share that fun fact with you.

Prop types in this component (and in all class defined components) are defined as static attribute of class. AddTodo only have one prop (and it is required), addTodo function. In this case it gets that prop from TodoList component, but it may be extracted from somewhere else, doesn't matter, only thing that matters inside AddTodo is that addTodo is function and passed through props.

Next thing we want to take a look is Todo component.

// src/components/Home/TodoList/Todo/Todo.jsx

import React from 'react';
import PropTypes from 'prop-types';


const Todo = ({ todo, setDone, deleteTodo }) => (
  <li style={{ textDecoration: (todo.done ? "line-through" : "") }}>
    {todo.task}&nbsp;
    <button className="done-button" onClick={() => setDone(todo.id, !todo.done)}>{todo.done ? "Activate" : "Set Done"}</button>&nbsp;
    <button className="delete-button" onClick={() => deleteTodo(todo.id)}>Delete</button>
  </li>
);

Todo.propTypes = {
  todo: PropTypes.shape({
    id: PropTypes.number.isRequired,
    task: PropTypes.string.isRequired,
    done: PropTypes.bool.isRequired
  }).isRequired,
  setDone: PropTypes.func.isRequired,
  deleteTodo: PropTypes.func.isRequired
};

export default Todo;

This component is presentation of one Todo item. It is wrapped inside <li> tag, has todo's task text and two buttons, one for marking todo as done or undone (same button, same action, different parameter), and one for deleting todo. Both buttons trigger functions which are just delegating job to function given through props, with appropriate attributes (values). As far as prop-types are concerned, it has todo key (defined same as todo in TodoList component), setDone required function and deleteTodo required function.

Before we carry on with components let's talk a little bit about presentational and container components. There is this pattern which states that all react components are divided into two groups, presentational and container components. Presentational components are responsible for rendering content, how things will look like on screen. They are not responsible for fetching or mutating data, they just receive data through props, and create appropriate layout for that data. Usually they are written as dumb components, and they can hold other presentational or container components, doesn't matter. Unlike them container components, are responsible for data fetching and mutating. Their job is to provide data to presentational components, and to provide callbacks (hooks) for mutating data, most often the same to presentational components. There is one nice article describing this pattern here is link, just note that in that article dumb component is practically synonym for presentational component, while in this article dumb component has other meaning.

Having in mind what I just described about presentational and container components, you can see that all our components are presentational. Neither one of them is concerned about data fetching or mutating, they all just display data, and link callbacks (hooks) for mutation to user controls (buttons). There is no real source of data or mutation callbacks, it all comes from TodoList which gets it from props, but where does TodoList get them from?

TodoListContainer component, actions and reducers

Now we will create our first container component, which will handle fetching data (for now just from reducer - application state), and provide callbacks for mutation (modification).

// src/components/Home/TodoList/TodoListContainer.js

import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';

import { setTodoDone, deleteTodo, addTodo } from './actions/todoActions';
import TodoList from './TodoList';


const mapStateToProps = state => ({
  todos: state.todoReducer.todos
});

const mapDispatchToProps = dispatch => bindActionCreators({
  setTodoDone,
  deleteTodo,
  addTodo
}, dispatch)


export default connect(mapStateToProps, mapDispatchToProps)(TodoList);

Here we have few new concepts. First of all, as you may have noticed, real definition of component doesn't even exist here. In export default part we just wrapped our TodoList component in some function, and returned that. What is this actually? It's just a wrapper which subscribes component to global application state (reducer), and provides data (and functions) as a props to the wrapped component. So this is the part where real data is "injected" in our components.

connect function accepts two functions as parameters, and creates wrapper which then accepts component to wrap. First function passed to connect is mapStateToProps, function which gets state (global state, which is created by combineReducers in our src/reducers.js added to a store object in our src/index.js and injected in global wrapper <Provider> also in src/index.js) and returns object with keys (extracted from state) which will be passed as props to wrapped component. Second function passed to connect is mapDispatchToProps, function which gets dispatch (callback we will get back to this in Part 3 where we will take a look into creating async actions), and returns object containing "function name - function" pairs (that functions are actually actions) which will also be passed as props to wrapped component.

This is pretty important part, it is link between simple components, and application state, part that actually connects all parts of redux as a functional whole. One more handy thing connect do for us, is "subscribing" to a part of state we are passing to wrapped component, so any time that part of state is changed (only through reducers!), our wrapped component will receive new (changed) props. It is like we have some event listener, which listens for change events only for those parts of global state we "subscribed" on.

In our mapStateToProps we connected state.todoReducer.todos to a todos key. That is nice, but we need todoReducer, if you take a look in src/reducers.js it is just an empty object, we need to create todoReducer, with todos key. Also in mapDispatchToProps we are using bindActionCreators function (this will also be explained later, for now just think of it as a helper) to wrap our object containing actions. But we still need those actions in code. So let's start from our actions, and then take a look into our todoReducer.

// src/components/Home/TodoList/actions/todoActions.js

import * as types from '../constants';


export const setTodoDone = (id: Number, done: Boolean) => ({
  type: types.SET_TODO_DONE,
  payload: {
    id,
    done
  }
});

export const deleteTodo = (id: Number) => ({
  type: types.DELETE_TODO,
  payload: {
    id
  }
});

export const addTodo = (task: String) => ({
  type: types.ADD_TODO,
  payload: {
    task
  }
});

It is just a javascript file containing bunch of functions. Every function is returning some kind of object. That object is actually an action, and these functions are action creators. In this article, whenever I said actions I was referring to "action creators", and when I want to refer action I will say "action object", that is pretty common notation. Each action object has to have type key, representing identificator by which it will be recognized in reducers, other keys are optional. For consistency I like to all other data put inside payload key, so that each action object has same structure. Actions (action creators) can accept parameters how ever you want, because in the end, they are just a simple plain functions which will be called from somewhere in your code (components). These returned objects (action objects) are automatically dispatched in system (automatically thanks to bindActionCreators method, but more on that later), and main reducer (optionally combined from other reducers - in our case in src/reducers.js with function combineReducers) will get called with that action object as a second parameter. Let's now take a look into the our todoReducer.js

// src/components/Home/TodoList/reducers/todoReducer.js

import { Record } from 'immutable';
import * as types from '../constants';

import { getLastId } from '../../../../utils/todoUtils';


const TodoState = new Record({
  todos: [
    { id: 1, task: "This is todo 1", done: false },
    { id: 2, task: "This is todo 2", done: false },
    { id: 3, task: "This is todo 3", done: true }
  ]
});

const initialState = new TodoState();

const todoReducer = (state = initialState, action) => {
  switch(action.type) {
    case types.SET_TODO_DONE:
      return state.set('todos', state.todos.map((todo) => todo.id === action.payload.id ? { ...todo, done: action.payload.done } : todo));
    case types.DELETE_TODO:
      return state.set('todos', state.todos.filter((todo) => todo.id !== action.payload.id));
    case types.ADD_TODO:
      return state.set('todos', [ ...state.todos, { id: getLastId(state.todos) + 1, task: action.payload.task, done: false } ]);

    default:
      return state;
  }
}

export default todoReducer;

Let's start from top. First we defined initial state using immutable Record. That ensures that state won't be changed manually, only through public interface (set method), which is useful because any manual changes made to state won't be recognized, and "event" for state change won't be fired. We could do that with Object.assign, by making new instance of state each time we change something, immutable provides same result but with bunch of optimizations.

reducer is actually just a function, which gets current state as first parameter, and action object which caused invoking function (action creator created and dispatched that action object), as a second parameter. So everything that reducer is doing is actually just mutating state depending on received action object. Before I mentioned that each action object has to have type key, by that key reducer recognizes which action actually invoked change, and knows how to handle that concrete action. One more time, you can't modify state object manually, it is possible to do something like

state.todos.push({ 
  id: -1,
  task: 'Invalid modification of state',
  done: false
});

but don't! This type of change won't trigger "change event", so all components that are subscribed won't get signal that anything changed.

One common thing that both actions and reducer use (import) is constants.js file, which we haven't show yet. It is just a simple collection of constants, for simpler connection between them (recognition of action objects inside reducer).

// src/components/Home/TodoList/constants.js

export const SET_TODO_DONE = 'SET_TODO_DONE';
export const DELETE_TODO = 'DELETE_TODO';
export const ADD_TODO = 'ADD_TODO';

Let's now analyze each case in our reducer. First case is SET_TODO_DONE

// action object
{
  type: types.SET_TODO_DONE,
  payload: {
    id,
    done
  }
}

// reducer handler
case types.SET_TODO_DONE:
      return state.set('todos', state.todos.map((todo) => todo.id === action.payload.id ? { ...todo, done: action.payload.done } : todo));

So in reducer, we go through current state todos, and checking if given todo id matches one sent through action object (in payload.id), when it match, we replace that todo object with new object, by copying all key-value pairs from old object (using spread operator), and overriding done key with value passed through action object. And in the end, newly created list we set as new state todos.

Next case is DELETE_TODO

// action object
{
  type: types.DELETE_TODO,
  payload: {
    id
  }
}

// reducer handler
case types.DELETE_TODO:
      return state.set('todos', state.todos.filter((todo) => todo.id !== action.payload.id));

Simple handler, just filter current state todos to extract todo with given id (payload.id). Filtered list is then set as todos key in new state.

And last case is ADD_TODO

// action object
{
  type: types.ADD_TODO,
  payload: {
    task
  }
}

// reducer handler
case types.ADD_TODO:
      return state.set('todos', [ ...state.todos, { id: getLastId(state.todos) + 1, task: action.payload.task, done: false } ]);

Here action object has only task key in payload, that is because done is by default false, and id is auto-generated. Here we just copy all of current state todos into new list and add new object, with auto-generated id, task from payload.task and default false for done. Generation of id is done through helper function in our src/utils/todoUtils.

// src/utils/todoUtils.js

export const getLastId = (todoList: Array) => {
  let lastId = 0;
  todoList.map((todo) => lastId = (todo.id > lastId ? todo.id : lastId));

  return lastId;
}

For now it just contains that one function, which is pretty basic. Goes through given list, finds biggest id, and returns that. Default value is 0 so if no todos are sent, it will return 0, and in our generator we always add + 1 on last id, so minimal id will be 1.

Connecting all parts together

Ok, so, we defined our actions, our reducer, and all components that we need, now it's time to include them somewhere in our application. In our TodoListContainer we referenced todos from reducer with state.todoReducer.todos, and in our reducer we only have todos key, so that means that whole reducer will be registered under todoReducer inside global one. That would be simple enough.

// src/reducers.js

...
import todoReducer from './components/Home/TodoList/reducers/todoReducer';
...

const appReducer = combineReducers({
  // here will go real reducers
  todoReducer
});
...

In our main reducer creator we just imported our reducer, and inserted it inside appReducer under the name (key) todoReducer. That will give us access to all data from new reducer inside global applications state.

And last thing we need to do to make this work (show on our screen) is to actually render our TodoList.

// src/components/Home/Home.jsx

...
import TodoList from './TodoList/TodoListContainer';
...

First we need to import our component inside Home, because that is where we want to render our list. Note that we imported from TodoListContainer and not TodoList, why is that? Because we need component which have data and function, we don't want to provide custom data or functions to it, here we need it independent. Next we want to actually render component, so we insert

<div>
  <TodoList />
</div>

just bellow ending </p> tag in default render method. And that is it. Now if you start application you shouldn't get any warnings or errors, and on localhost:3000 you should see something like

react-tutorial-3

You can play around with options, it will all work. Each time when you restart browser tab, it will go to this initial data set (because we haven't connected our reducers to some persistent data, but only to our initial state).

Conclusion

That is all for this part. It has many information, go through this part more times if you need to, it is important to get all the concepts described here, because everything else is built on them. If you haven't read first part you can read it here. In next part we will focus on async actions, and connecting application with RESTful API (that is why we need async actions). See you in part 3.