Introduction
Redux is a predictable state container for JavaScript apps - as stated in Redux official documentation. When I started learning Redux, I found it difficult to comprehend the concept and implementation. I skimmed through many articles on the net but could not find a satisfactory answer. Most of the articles either explain creating apps with Redux or converting React apps with no basic code to Redux. I could not find something that explained how a working React app can be modified to use Redux with the same functionality.
In this article, I have made an attempt to do exactly that. I will first explain how to create a React app that has minimal functionality of creating and deleting a list of books. This app is similar to a much-publicized ‘todo’ app. You can use it to create a Todo app also by just renaming the component and variable names.
Create a simple React App
With this introduction, let us dive into hands-on. Open your code editor and type ‘npx create-react-app react-redux’. You may change the name of the app from react-redux to whatever you like. Once the app creation process completes, run ‘npm start’ at your console and confirm that the app is working fine with a gently rotating React logo.
Now delete all the files except those shown in the list below and create blank files which are needed:
Create a directory ‘components’ under ‘src’ directory and create three blank files Book.js, BookForm.js, and BookList.js under the ‘components’ directory. Once you have the files and folders, as shown above, replace the contents of the files App.js and Index.js from the code given below, and fill the other three blank files with the code given below :
index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(
<App />,
document.getElementById("root")
);
App.js
import React from "react";
import BookForm from "./components/BookForm";
import BookList from "./components/BookList";
class App extends React.Component {
state = {
books: []
};
addBook = book => {
this.setState(state => ({
books: [book, ...state.books]
}));
};
removeBook = id => {
this.setState({
books: this.state.books.filter(book => book.id !== id)
});
};
render() {
return (
<div>
<h1> React to Redux</h1>
<BookForm onSubmit={this.addBook} />
<BookList books={this.state.books} removeBook={this.removeBook} />
</div>
);
}
}
export default App;
Book.js
import React from "react";
const Book = props => {
const { book } = props;
return (
<div>
<div>
{book.title} <button onClick={props.onDelete}>Delete</button>
</div>
</div>
);
};
export default Book;
BookForm.js
import React from "react";
class BookForm extends React.Component {
constructor() {
super();
this.state = { title: "" };
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
const { name, value } = event.target;
this.setState({
[name]: value
});
}
handleSubmit(event) {
event.preventDefault();
if (this.state.title)
this.props.onSubmit({
id: Date.now(),
title: this.state.title
});
this.setState({
title: ""
});
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<input
type="text"
value={this.state.title}
name="title"
placeholder="Book Title"
onChange={this.handleChange}
/>
<input type="submit" value="Submit" />
</form>
);
}
}
export default BookForm;
BookList.js
import React from "react";
import Book from "./Book";
const BookList = props => {
return (
<div>
{props.books.map(book => (
<ul key={book.id}>
<Book book={book} onDelete={() => props.removeBook(book.id)} />
</ul>
))}
</div>
);
};
export default BookList;
Migrating the app to Redux
Now that we have created a React app that displays a form, accepts books, displays the list and deletes any, we have a fully functional React app. Next, we will refactor the same app and see how we can use Redux to maintain the data of the app. Please remember, the purpose of this article is not to teach React but to teach how to understand and use Redux in a React application.
Before proceeding further, please remember we use Redux to maintains the ‘state’ or ‘data’ of a React application. In this article we are going to do exactly that, moving the state of React app into Redux.
Flow of data in a Redux App
We shall try to understand the basics of Redux from the diagram above. On top right there is a box ‘React App’ in which the data flows from top component to bottom and it is held in ‘state’ of the root component.
If you look at the bottom right diagram which shows ‘Redux App’, the data is not stored in the components but it is stored in a single place called ‘Store’. The data received by the components from the external interface is sent to Store using ‘mapDispatchToProps’, and the ‘connect’ function connects the component sending the data and the Store.
The data being sent to the ‘Store’ passes through two stages viz.
‘Actions’ and ‘Reducers’. The ‘Actions’ is an object that specifies what type of action is needed to be done on the data and what are the other parameters for that action. Then this data is passed on to ‘Reducer’. Reducer is a function which performs the required action, i.e. either storing the data in the required format or creating a new set of data and replacing the old data or fetching data from the Store. This is how data is modified because in React state is not mutated.
Now we shall briefly look at the new terms used above. The store has the following responsibilities:
But these things are handled internally and we will not generally use those methods explicitly.
Store is the place within the app where the data is stored. It is similar to ‘State’ in React Apps. Store is not accessible to the components of the App directly. Components have to connect to the Store making use of two methods.
The first one is mapDispatchToProps which sends data from the components to Store and the second one is mapStateToProps which receives data from the Store into the component as prop. But these two functions need connect() which connects Store and components. When you use connect, it is followed by the name of the component being connected in parentheses.
There are two more things, viz. Action and Reducer. Action is an object which specifies the inputs needed to an operation of fetching or sending data to Store. Reducer is a method which uses Action details and performs the required operation on the Store.
The components of the app are segregated into ‘components’ and ‘containers’ folders. We keep presentation components i.e. components that have a user interface under components folder and the components that deal with the Store are kept under containers folder. In addition to these, we create a folder called ‘actions’ and the action objects are placed in this folder.
We also create ‘reducers’ folder and keep the reducer objects under this folder. This is done so that Redux can pick up the needed components from these locations without specifying where to look for them.
The Redux App
Before we start refactoring our code, it is advised to make a copy of the working React app you have created just now.
To begin with, let us add redux to our app by running the following commands from the terminal:
npm install redux
npm install react-redux
First, we segregate our components depending on the functions they do and keep them separately in separate folders. For this purpose, let us create four folders with the following names. I am also specifying the file names which go under each of these folders. Only index.js is kept in the root folder.
index.js
components
App.js
Book.js
containers
BookForm.js
BookList.js
actions
index.js
reducers
book.js
index.js
Now move App.js and Book.js to the folder Components and move BookForm.js and BookList.js to the containers folder. We don't have any files under actions and reducers folders which we shall be adding shortly.
Follow the code below and wherever it is commented '//', delete those lines and wherever it is mentioned '//add' at the end of the line, add those lines to your React App. Alternatively, you may copy the entire React App code or Redux App code from my GitHub, the link for which is given at the end of this article. I shall give a brief explanation of the changes we are making to each of the files after the code listing of each file
index.js
import React from "react";
import ReactDOM from "react-dom";
// import App from “./App”;
Import App from “./components/App"; //add
import { createStore } from "redux"; //add
import { Provider } from "react-redux"; //add
import reducer from "./reducers"; //add
const store = createStore(reducer); //add
ReactDOM.render(
<Provider store={store}> //add
<App /> // remove ‘,’
</Provider>, //add
document.getElementById("root")
);
In the above file, we can see that the location of the ‘App’ component is moved from the root folder to ‘components’ folder. Then we imported two Redux components ‘createStore’ and ‘Provider’. CreateStore initializes the Store. The <Provider /> makes the Redux store available to any nested components that have been wrapped in the connect() function. The ‘reducer’ is going to be defined in a separate file which is being imported here.
actions/index.js
export const addBook = title => ({
type: "ADD_BOOK",
id: Date.now(),
title
});
export const removeBook = id => ({
type: "REMOVE_BOOK",
id
});
The index.js file under actions folder defines the various actions that are required in the app. Each action is defined by providing some attributes like ‘type of the action’ and the inputs needed to perform that action. For our app, we have defined two action objects ‘ADD_BOOK’ and ‘REMOVE_BOOK’ giving other information like ‘id’ and ‘title’.
These action objects would be used by the reducer methods defined in the files under the ‘reducer’ folder.
reducers/books.js
const books = (state = [], action) => {
switch (action.type) {
case "ADD_BOOK":
return [
...state,
{
id: action.id,
title: action.title
}
];
case "REMOVE_BOOK":
return state.filter(book => book.id !== action.id);
default:
return state;
}
};
export default books;
The file ‘books.js’ defines the methods ‘ADD_BOOK’ and ‘REMOVE_BOOK’ which add and remove books from the store. We can have more than one file defining the reducers for various actions required in the app, but then we need to use another reducer ‘index.js’ which combines them into one. For demo purposes, I have also defined ‘index.js’ which combines other reducers though we have only one other reducer, i.e. books.js.
reducers/index.js
import { combineReducers } from "redux";
import books from "./books";
export default combineReducers({
books
});
The file ‘index.js’ under the ‘reducers’ folder combines other reducers defined in other files, into one, and makes it available to the store.
So far, we have added the above three files and are going to modify other files as discussed below.
components/App.js
import React from "react";
// import BookForm from "./components/BookForm";
// import BookList from "./components/BookList";
import BookForm from "../containers/BookForm"; //add
import BookList from "../containers/BookList"; //add
class App extends React.Component {
// state = {
// books: []
// };
// addBook = book => {
// this.setState(state => ({
// books: [book, ...state.books]
// }));
// };
// removeBook = id => {
// this.setState({
// books: this.state.books.filter(book => book.id !== id)
// });
// };
render() {
return (
<div>
<h1> React to Redux</h1>
<BookForm onSubmit={this.addBook} />
<BookList books={this.state.books} removeBook={this.removeBook} />
</div>
);
}
}
export default App;
In the App.js file above we changed the top two lines just because we have changed the locations of those files. Then we have removed ‘declaring the state’, and methods for ‘adding’ and ‘deleting’ the books. Because in Redux, state is maintained by Store and add and delete methods are handled by the reducers defined in ‘reducers/books.js’ file.
components/Book.js
import React from "react";
const Book = props => {
// const { book } = props;
const { book, removeBook } = props; //add
const handleRemoveBook = () => removeBook(book.id); //add
return (
<div>
<div>
// {book.title} <button onClick={props.onDelete}>Delete</button>
{book.title} <button onClick={handleRemoveBook}>Delete</button>//add
</div>
</div>
);
};
export default Book;
In the above file ‘Book.js’ there is not much change except for the syntax
containers/BookForm.js
import React from "react";
import { connect } from "react-redux"; //add
import { addBook } from "../actions"; //add
class BookForm extends React.Component {
constructor() {
super();
this.state = { title: "" };
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
const { name, value } = event.target;
this.setState({
[name]: value
});
}
handleSubmit(event) {
event.preventDefault();
// this.props.onSubmit({
// id: Date.now(),
// title: this.state.title
// });
const { title } = this.state; //add
const { addBook } = this.props; //add
if (title) { //add
addBook(title); //add
this.setState({
title: ""
});
}
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<input
type="text"
value={this.state.title}
name="title"
placeholder="Book Title"
onChange={this.handleChange}
/>
<input type="submit" value="Submit" />
</form>
);
}
}
// export default BookForm
export default connect(null, { addBook })(BookForm); /add
In this file, we are importing ‘connect’ from ‘react-redux’ and ‘addBook’ from actions/index.js. Accordingly, we are deleting the OnSubmit method and in its place using the ‘addBook’ method which is defined in actions/books.js file. We are passing the ‘title’ which was initially stored in the ‘state’ of the component, to ‘addBook’ and passing this ‘addBook’ as 'mapDispatchToState' method which was used by the ‘connect’ function that is connecting the Store and BookForm components.
containers/BookList.js
import React from "react";
// import Book from "./Book";
import Book from "../components/Book"; //add
import { connect } from "react-redux"; //add
import { removeBook } from "../actions"; //add
function mapStateToProps(state) { //add
const { books } = state; //add
return { books }; //add
} //add
const mapDispatchToProps = dispatch => ({ //add
removeBook: id => dispatch(removeBook(id)) //add
}); //add
// const BookList = props => {
// return (
// <div>
// {props.books.map(book => (
// <ul key={book.id.toString()}>
// <Book book={book} onDelete={() => props.removeBook(book.id)} />
// </ul>
// ))}
// </div>
// );
// };
const BookList = ({ books, removeBook }) => { //add
return ( //add
<div> //add
{books.map(book => ( //add
<ul key={book.id}> //add
<Book book={book} removeBook={removeBook} /> //add
</ul> //add
))} //add
</div> //add
); //add
}; //add
// export default BookList;
export default connect(mapStateToProps, //add
mapDispatchToProps)(BookList); //add
Finally, we have replaced almost all the code in BookList.js. On the top of the file we are importing ‘connect’ and ‘removeBook’. In this component we are both receiving data from Store as a book list to display on the screen and sending data to Store by way of ‘removeBook’ if the ‘remove’ button is clicked for a book.
Thus, we have defined both the methods ‘mapStateToProps’ and ‘mapDispatchToProps’ on top of the file and used ‘connect’ at the bottom of the file passing both these methods and connecting Store and BookList components.
Once your files are ready, run ‘npm start’ and you can see the working of your app exactly like before when you did not use Redux.
Conclusion
Redux is more powerful and is very useful in large applications. Here, we tried just to understand how Redux functions. This knowledge should be useful when you start developing larger applications.
I am not a pro but a learner like you and I thought I should share the understanding I gained with the readers so that it would save some time for you. I welcome suggestions from anyone to improve the content.
Happy coding :)
React app : https://github.com/IBTechRaj/redux-article-react
Redux app : https://github.com/IBTechRaj/redux-article-redux