React from scratch Part 3
Jakob Klamser
Posted on August 31, 2020
In the third and final part of the series "React from scratch" we will implement a statemanagement by adding Redux to our application.
Prerequisites
Part 3 will start where we left off in part 2. If you didn't already, go ahead and finish part 1 and part 2 or just clone part 2 from my repo and start from there.
New dependencies
Let's get started by adding redux and some more new dependencies to our project.
$ npm i redux react-redux redux-thunk
- redux: A javascript library for state containers.
- react-redux: React bindings for redux.
- redux-thunk: Thunk middleware for redux.
If you want to know more about thunk I recommend reading this.
Global Store
Our goal is to create a, so called, store which holds all the post data of our application.
This store will also provide access to methods for handling with the store data, e.g. adding or removing posts from the store.
After we created that store we want to get rid of the state inside our Todo-Component.
So first let's start with creating new folders:
$ mkdir store
$ mkdir actions
$ mkdir reducers
Next up we will create a new file inside the store folder:
$ cd store/
$ touch configureStore.js
This file will contain all the configuration of our global store, such as adding the thunk middleware.
The content should look like this:
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers';
const initialState = {};
const middleware = [thunk];
const store = createStore(
rootReducer,
initialState,
compose(
applyMiddleware(...middleware),
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
)
);
export default store;
With the createStore method we can create a Redux Store. This store needs a reducer, the initial state of the application and the so called enhancers.
The enhancers are middleware that add functionality to our store. In our case we enable the Redux Developer Tools and added redux-thunk.
Now we need to pass this store to our application. This is done by using a Provider from the react-redux library we installed earlier.
We need to provide the store to our main component. So let's open up our Root.js and implement it:
import React from 'react';
import Routes from '../Routes';
import { Provider } from 'react-redux';
const Root = ({ store }) => (
<Provider store={store}>
<Routes />
</Provider>
);
export default Root;
Our Root Component can accept the store now, we just need to pass it in. We do this in our index.js file:
import React, { StrictMode } from 'react';
import ReactDOM from 'react-dom';
import store from './store/configureStore';
import Root from './containers/Root';
import './index.scss';
ReactDOM.render(
<StrictMode>
<Root store={store} />
</StrictMode>,
document.querySelector('#root'),
);
Reducers
For this code to work we need to create the rootReducer next:
$ cd ../reducers/
$ touch index.js
$ touch todoReducer.js
Let's open the index.js and add the following:
import { combineReducers } from 'redux';
import todoReducer from './todoReducer';
export default combineReducers({
todo: todoReducer,
});
The method combineReducers takes an object with all the reducers of your application.
From that it creates a single reducer which we can pass to our store, like we did in the configureStore.js.
Now we switch into the todoReducer to give it some life!
import { ADD_TODO, DELETE_TODO } from '../actions/types';
const initialState = {
todos: localStorage.getItem('todos') ?
JSON.parse(localStorage.getItem('todos')) : [],
error: null,
}
export default (state = initialState, action) => {
switch (action.type) {
case ADD_TODO:
const newTodos = [action.payload, ...state.todos]
localStorage.setItem('todos', JSON.stringify(newTodos))
return {
...state,
todos: newTodos,
};
case DELETE_TODO:
return {
...state,
todos: state.todos.filter(todo => todo._id !== action.payload)
};
default:
return state;
}
}
The reducer has the initial state for all the todos. We store all the todos in the localStorage of the browser.
We do that, so we can still have access to our todos after reloading the page.
The switch case creates a new state depending on the action called and the current state.
It does not modify the current state because redux implements the concept of immutability.
Actions
Next up we create the actions that the reducer listens to.
$ cd ../actions/
$ touch types.js
$ touch todoActions.js
First of we create the constants in types.js, that we already used in our todoReducer.
export const ADD_TODO = 'ADD_TODO';
export const DELETE_TODO = 'DELETE_TODO';
Now we start implementing our two actions ADD and DELETE in todoActions.js:
import { ADD_TODO, DELETE_TODO } from './types';
export const addTodo = (todo) => (dispatch) => {
dispatch({
type: ADD_TODO,
payload: todo,
});
};
If we would store our todos in some kind of backend we could send it there via axios.js or some other framework.
But for now we just dispatch the todo we pass in and the type ADD_TODO to our todoReducer.
Right below the addTodo we implement the deleteTodo like that:
export const deleteTodo = (id) => (dispatch) => {
dispatch({
type: DELETE_TODO,
payload: id,
})
};
It works nearly the same as the addTodo action, except we don't pass in the whole todo.
We just pass in the id of the todo we want to delete.
Connect Redux with React
At this point in time, our Todo App still works the same as before. To utilize the Redux Store we need to connect it to our Todo component.
import React, { Component, Fragment } from 'react'
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { addTodo, deleteTodo } from '../actions/todoActions';
import TodoListContainer from '../containers/TodoList';
import NewTodoContainer from '../containers/NewTodo';
export class Todo extends Component {
constructor(props) {
super(props);
this.state = {
showNewTodo: false,
title: '',
text: '',
};
}
static propTypes = {
todos: PropTypes.array.isRequired,
addTodo: PropTypes.func.isRequired,
deleteTodo: PropTypes.func.isRequired,
};
toggleNewTodo() {
this.setState({
showNewTodo: !this.state.showNewTodo
});
}
onChange(event) {
this.setState({ [event.target.name]: event.target.value });
}
onSubmit(event) {
event.preventDefault();
const { text, title } = this.state;
const newTodo = { id: this.props.todos.length + 1, title, text };
this.props.addTodo(newTodo);
this.setState({
showNewTodo: false,
title: '',
text: '',
});
}
render() {
const { showNewTodo } = this.state;
return (
<Fragment>
<div className="container-fluid">
<div className="col w-25 mt-4 mx-auto">
{ showNewTodo ?
(<Fragment>
<button className="mb-2 w-100 btn btn-danger" onClick={this.toggleNewTodo.bind(this)}>Cancel</button>
<NewTodoContainer
onChange={this.onChange.bind(this)}
onSubmit={this.onSubmit.bind(this)} />
</Fragment>)
: (<button className="mb-2 w-100 btn btn-success" onClick={this.toggleNewTodo.bind(this)}>Add Todo</button>)
}
<TodoListContainer
todos={this.props.todos}
/>
</div>
</div>
</Fragment>
);
}
}
const mapStateToProps = (state) => ({
todos: state.todo.todos,
});
const mapDispatchToProps = (dispatch) => ({
addTodo(todo) {
dispatch(addTodo(todo));
},
deleteTodo(id) {
dispatch(deleteTodo(id));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(Todo);
There is a lot of stuff happening here at once so let's break it down from top to bottom:
- We imported the connect function from react-redux. Then we imported the PropTypes and the two new actions we implemented before, addTodo and deleteTodo.
- The constructor needed some cleanup so we removed all the todos because we keep them in our store from now on.
- We added static propTypes to ensure that the todos from the store and the two actions we imported earlier get the right types and are required for this component.
- In the onSubmit method we create a newTodo that we pass on to our addTodo action by accessing it via this.props. We did remove the todos from the setState method because the component-state does not contain the list of todos anymore.
- Right below the component-class we added two arrow functions. mapStateToProps gets the redux state passed in and return our todos by adding it to this.props of our component. mapDispatchToProps maps our actions, addTodo and deleteTodo, to this.props of our component.
- Finally we used the react-redux method connect to map the two arrow functions to our Todo component, so that we can access all the state and actions via this.props.
Conclusion
That's it for part 3 of this series.
We implemented a redux store, reducer and actions into our application. After we did that we connected this new feature to our Todo-component by using react-redux.
By doing all this we got a quick introduction to localStorage and can now write and read data from the browsers localStorage.
As you may have noticed we did not use the deleteTodo method anywhere. I did this on purpose so that you can add the delete functionality to this component by yourself.
I hope you enjoyed it!
If you got any question just contact me via Twitter.
All the code for this multipart series can be found in this GitHub-Repository.
Posted on August 31, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.