DEV Community

Nesha Zoric
Nesha Zoric

Posted on

Building a Simple React App - Part 2

In the 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!

Writing new components for handling todos

On start we will focus only on functionality, a style will be added later. So for our todos, we will create a 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 straightforward component, written as dumb component (if you recall, in previous part I recommended writing all components as dumb on the beginning). It has a 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 the usage of prop-types. Prop-types gives us a 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 a shape as described, and marked that array as required. Shape of each todo is described by id number required the value, the task as a 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 the 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 a component from global application state (which is the 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. The changeText method is triggered by any change event inside input field, while submitTask is triggered only by Add Todo button click. Both methods are simple ones, changeText just sets an internal state task to received text, and submitTask restarts text inside internal state, and submits current text (from the internal state) through only prop component received, addTodo. The interesting thing here is the order of actions, it first restarts text, and then submits text which is inside the state, but it still works as it is supposed to. How? Component's setState method is an 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 a static attribute of the class. AddTodo only has 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, the 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 the 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 the job to function given through props, with appropriate attributes (values). As far as prop-types are concerned, it has todo key (defined same as the 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 an 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 the link, just note that in that article dumb component is practically the synonym for the 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 the 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, the real definition of the 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 props to the wrapped component. So this is the part where real data is "injected" into our components.

connect function accepts two functions as parameters and creates a 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 a pretty important part, it is the link between simple components, and application state, a 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 the state we are passing to the wrapped component, so any time that part of the 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 the 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 with our actions, and then take a look at 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 a 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 identification 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 the same structure. Actions (action creators) can accept parameters however 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 the system (automatically thanks to the 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 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 thr top. First, we defined initial state using immutable Record. That ensures that state won't be changed manually, only through the 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 a new instance of state each time we change something, immutable provides the same result but with a bunch of optimizations.

reducer is actually just a function, which gets the current state as the 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 the signal that anything changed.

One common thing that both actions and reducer use (import) is the 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 the 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. The 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 the 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 the 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 a component which has 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.

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 much 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 the application with RESTful API (that is why we need async actions). See you in part 3.

Originally published at Kolosek blog.

Top comments (0)