How to Build Simple React App (Part 2)
In the previous part of How to build simple React app, we had set up a basic boilerplate 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
At the 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 a dumb component (if you recall, in the previous part I recommended writing all components as dumb at 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 the form of Todo
component.
The new part here is the usage of prop-types. Prop-types gives us the possibility of type-checking. Its main idea is to define the types of props the component will receive, which gives you more clarity when writing the component, and more verbosity when debugging (for example if something marked as required is not set, you will see a console error for that, or if something is sent, but the 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 an array of objects having a shape as described, and marked that array as required. The shape of each todo is described by id number required the value, the task as a required string, and the done required boolean flag. addTodo, setTodoDone, and deleteTodo are props defined as functions and all required.
Don't worry for now about 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.
The 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 an internal state. Generally, component internal state should be avoided because it makes testing harder, and 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, has its internal state storing task text (which is read from the input field), and two custom methods (functions) changeText
and submitTask
. The changeText
method is triggered by any change event inside the input field, while submitTask
is triggered only by the Add Todo button click. Both methods are simple ones, changeText
just sets an internal state task to a received text, and submitTask
restarts text inside the internal state, and submits current text (from the internal state) through only the prop component received, addTodo
. The interesting thing here is the order of actions, it first restarts the text, and then submits text which is inside the state, but it still works as it is supposed to. How? The component's setState
method is an async method, which means that it won't change state immediately, but in the next process tick, so we can do something like that. You should probably reverse the order of these 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 attributes 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 a function and passed through props.
The next thing we want to take a look at 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}
<button className="done-button" onClick={() => setDone(todo.id, !todo.done)}>{todo.done ? "Activate" : "Set Done"}</button>
<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, and how things will look like 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 meanings.
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 a 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 the component to the 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 components to wrap. The first function passed to connect is mapStateToProps
, the 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. The second function passed to connect is mapDispatchToProps
, the 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 an object containing "function name - function" pairs (that functions are actually actions) which will also be passed as props to the wrapped component.
This is a pretty important part, it is the link between simple components, and the application state, a part that actually connects all parts of redux as a functional whole. One more handy thing connect
does 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" to.
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 the 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 returns 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 to action I will say "action object", which is a pretty common notation. Each action object has to have a 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 the 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 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 the 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 the top. First, we defined the initial state using an immutable Record. That ensures that the 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 the 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 the action object which caused the 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 the received action object. Before I mentioned that each action object has to have a 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 the 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 the reducer use (import) is the constants.js
file, which we haven't shown yet. It is just a simple collection of constants, for a simpler connection between them (recognition of action objects inside the 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. The 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 the current state todos, and check if the given todo id matches one sent through action object (in payload.id
), when it matches, we replace that todo object with a new object, by copying all key-value pairs from the old object (using spread operator), and overriding done key with vthe alue passed through action object. And in the end, newly created a list we set as new state todos
.
The 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 filters current state todos to extract todo with given id (payload.id
). The filtered list is then set as todos
key in the 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 the current state todos into a new list and add a new object, with auto-generated id, the task from payload.task
and default false for done. Generation of id
is done through the 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 the given list, finds the 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 the last id, so the minimal id will be 1.
Connecting all the parts together
Ok, so, we defined our actions, our reducer, and all the 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 the whole reducer will be registered under todoReducer
inside the 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 the new reducer inside the 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 functions, we don't want to provide custom data or functions to it, here we need it independent. Next, we want to actually render the component, so we insert
<div>
<TodoList />
</div>
just bellow ending </p>
tag in default render method. And that is it. Now if you start the application you shouldn't get any warnings or errors, and on localhost:3000 you should see something like
You can play around with options, it will all work. Each time when you restart the 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 the first part you can read it here. In the 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.