When you are a junior developer, learning Redux can be something really overwhelming. This leads to avoiding it at all cost and using alternatives like MobX, ContextAPI, or simple React Hooks.
It's not something bad actually, but if you take a look at job offers, you can see Redux is almost always a requirement. In this article, I'll try to explain Redux in the simplest way I can.
Let's do it!
My first steps with Redux
Honestly speaking, I was also postponing the moment in time to learn Redux. Most of the time I was using MobX, when React Hooks and ContextAPI didn't exist.
And one day… I just sat down to the code and decided to face Redux!
Well, it was not easy in the beginning, but believe me—this technology is awesome! If you're a frontend developer, this is a must for you. Funny thing is that it’s really hard to understand at first, but at some point, it just clicks and everything becomes clear!
Setup
Like always, I'll start with creating a new application using create-react-app. But won’t start with Redux. I want to begin with simple React Hooks to set up some easy case that we will migrate to Redux then.
Also, I'd recommend deleting all the stuff related to tests, service workers, etc. You can check the setup commit on GitHub here: https://github.com/norbertsuski/redux-tutorial/commit/805191ab3f834f40d53cabb61427d81472f80969.
As always, a link to the repository is at the end of the article.
Simple state
Let's create a simple state in App.jsx file.
import React, { useState } from 'react';
import './App.css';
const App = () => {
const [todos, setTodos] = useState([]);
return (
<div className="App">
<header className="App-header">
<p>List of TODOs:</p>
<ul>
{todos.map(({ id, title }) => (
<li key={id}>{title}</li>
))}
</ul>
</header>
</div>
);
};
export default App;
When you run this code, nothing special will happen on the screen. Simple 'List of TODOs' text will show up, but nothing more. This is because our list of TODOs is empty.
Let's add a simple form to handle adding new TODO.
For the sake of simplicity, we can agree that the visual part of this tutorial is not the most important thing and I’ll just make it as simple as possible.
Our App.jsx file should look like this and be able to add new positions to the list:
import React, { useState } from 'react';
import './App.css';
const App = () => {
const [todos, setTodos] = useState([]);
const [todoTitle, setTodoTitle] = useState('');
const handleSubmit = (event) => {
event.preventDefault();
setTodos((prevTodos) => [...prevTodos, { title: todoTitle, id: prevTodos.length }]);
setTodoTitle('');
};
const handleOnChange = (event) => {
setTodoTitle(event.target.value);
};
return (
<div className="App">
<header className="App-header">Learn Redux</header>
<main>
<form onSubmit={handleSubmit}>
<input value={todoTitle} type="text" name="title" onChange={handleOnChange} />
<button type="submit">Submit</button>
</form>
<p>List of TODOs:</p>
<ul>
{todos.map(({ id, title }) => (
<li key={id}>{title}</li>
))}
</ul>
</main>
</div>
);
};
export default App;
This example is a simple case scenario with two states - for the list of TODOs and for handling title value changes. We can assume that todoTitle state is used internally and doesn't need to be exposed outside of this component.
But we can imagine a situation when TODO list is used by multiple components passing it via properties or handling its change, extending it from many different places. That can become a challenge as the application keeps growing. This is a situation when state management libraries come into play.
Let's try to refactor it to use Redux.
Redux setup
Now it's time to install all required Redux dependencies.
npm install react-redux redux
npm install redux-devtools --save-dev
Let's start with some theory first. I'm assuming that you are at least familiar with React Hooks already.
The code we created before is an example of 'one-way data flow': a state that describes current condition of the app, where UI is rendered based on the state and actions that the user performs are updating the state, which causes UI to re-render again.
Flow in Redux is almost the same, but the names of some elements are differing a little bit or have a few more functions.
Let's try to recreate the flow we did already with Redux. You'll get familiar with three basic building blocks: store, reducers, and actions.
Creating a store
What is a store? The store holds all your application states. This is a single source of truth, a container for the state, which is then injected into the application and every building block of the application can have a ‘connection' to it. Good design practice is to have only one store per application. Let's create a new folder called store and index.js file inside.
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import App from './App';
import store from './store';
import './index.css';
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root'),
);
Now, when you open your browser you should see a beautiful error similar to this one:
This is because we need to pass a reducer to the createStore function. Let's create one now.
Creating reducer
What is a reducer? Actually, it’s just a function that is responsible for changing the state of an application. What's important here—reducer cannot modify the existing (passed) state. It has to make immutable updates by copying the existing state and making changes to the copied values.
Reducer can't have any side effects and can't do asynchronous logic like calling backend services. The new state is always calculated based on arguments passed to the reducer. Normally, an application can have multiple reducers, but in our case, we will do just one.
Let's create reducer.js file in our store folder with the following content:
const initialState = {
todos: [],
};
const reducer = (state = initialState, action) => {
switch (action.type) {
default:
return state;
}
};
export default reducer;
The first part here is really simple—it's just an object with the initial state of our application, which is an empty list of todos. Just like in useState hook.
Then we have a reducer function that has two parameters: current state that will be used inside the reducer to calculate new state and action object, which is describing how the state should be changed.
I'll describe the actions in the next chapter. Now we should modify our index.js file in the store folder.
import { createStore } from 'redux';
import reducer from './reducer';
const store = createStore(reducer);
export default store;
Et voila! The application is working again! Now let's jump into actions and make Redux store work!
Lights, camera, action!
The last part of our Redux implementation is actions. Action is a plain JavaScript object that has a type field and describes what happened in the application.
Type is a simple string field used by the reducer to determine which action actually happened.
Do you already know what can be the action in our case? That's correct - setTodos is something that can be converted into a Redux action. What else do we need here apart from the type field? Of course - we need to pass actual todo to action.
Let's create actions.js file in store folder with the following content:
export const addTodo = (todo) => ({
type: 'todos/addTodo',
payload: { todo },
});
Wait! You said that action is a pure JavaScript object and I see a function here! — you might say. And yes, you would be right.
I packed an action object into a function that returns an actual action—this convention is called action creator and it's used very often. You will see why in a moment.
What you can see here is a function that gets todo object as a parameter and returns a simple object with type field and payload field—it's a good practice to pass data we want to use through payload field.
Now let's try to connect our application with Redux store.
Connecting pieces together
To connect our application with Redux store we need to use the function provided by react-redux package called... connect. Let's modify our App.jsx file content:
import React, { useState } from 'react';
import {
arrayOf, number, string, shape,
} from 'prop-types';
import { connect } from 'react-redux';
import './App.css';
const App = ({ todos }) => {
const [todoTitle, setTodoTitle] = useState('');
const handleSubmit = (event) => {
event.preventDefault();
// setTodos((prevTodos) => [...prevTodos, { title: todoTitle, id: prevTodos.length }]);
setTodoTitle('');
};
const handleOnChange = (event) => {
setTodoTitle(event.target.value);
};
return (
<div className="App">
<header className="App-header">Learn Redux</header>
<main>
<form onSubmit={handleSubmit}>
<input value={todoTitle} type="text" name="title" onChange={handleOnChange} />
<button type="submit">Submit</button>
</form>
<p>List of TODOs:</p>
<ul>
{todos.map(({ id, title }) => (
<li key={id}>{title}</li>
))}
</ul>
</main>
</div>
);
};
App.propTypes = {
todos: arrayOf(shape({
id: number,
title: string,
})).isRequired,
};
const mapStateToProps = (state) => ({
todos: state.todos,
});
export default connect(mapStateToProps)(App);
Phew, a lot of things have happened here!
First of all, I imported connect function from react-redux and used it in the last line of the code, and passed mapStateToProps function. This function is another convention in Redux used to map application state into props passed to component.
As you can see there, I only get todos from state object and pass it to todos field. This field can be read as a component property then (see App function parameter).
If you still aren’t sure that this is working, we can modify initialState object in reducer to verify it:
const initialState = {
todos: [{ id: 1, title: 'test1' }],
};
And this is what we have on UI:
Redux dispatched action @@INIT on our reducer (which you should never react to in reducer!). This initialized our state with values from initialState object (default case in switch) and then we mapped the state into props using mapStateToProps function passed to connect function.
Using dev tools with Redux
Before we jump into modifying our state, let’s talk about dev tools that will help us control what is happening inside Redux store.
In the Chrome browser, there is a very helpful tool called... Redux DevTools. You can install it as a Chrome extension. To make it work we need to modify our store a little:
// change this line:
const store = createStore(reducer);
// into this:
const store = createStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
Thanks to this, you will be able to see what is happening with the application state. As you can see on the screen below, there is one action @@INIT called and on the right side—the current state of our application. You can experiment with this tool later when we’ll be handling state modifications.
Dispatching actions
To modify our state (add a new todo in this case), we need to do something called action dispatch. This will pass the action to the reducer which will take care of our state modification.
Let's try to do this in App.jsx file.
import { addTodo } from './store/actions';
import './App.css';
const App = ({ todos, dispatch }) => {
const [todoTitle, setTodoTitle] = useState('');
const handleSubmit = (event) => {
event.preventDefault();
dispatch(addTodo({ title: todoTitle, id: todos.length }));
setTodoTitle('');
};
...
What changed here? I did the following things:
- imported addTodo function from actions.js file,
- destructured dispatch function from components props (it's available because we used connect function, remember?),
- used dispatch function with addTodo action creator, and passed new todo to this.
When you look at dev tools you will see this after clicking Submit button:
We successfully dispatched todos/addTodo action!
But nothing happened on the UI... this is because our reducer is not reacting to this kind of action. We need to adapt it for this now.
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'todos/addTodo':
return { ...state, todos: [...state.todos, action.payload.todo] };
default:
return state;
}
};
Now, our reducer is able to do some logic on dispatched action (which means that new todo is added). Also, the UI displayed a modified todo list:
Simplifying action dispatcher
You’ve probably noticed that dispatching action looks a little bit odd, but it can be simplified.
We’ll use almost the same mechanism as mapDispatchToProps. We need to do the following changes in App.jsx file:
import * as todoActions from './store/actions';
...
const mapDispatchToProps = {
addTodo: todoActions.addTodo,
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
And now we can use addTodo function like this:
const App = ({ todos, addTodo }) => {
const [todoTitle, setTodoTitle] = useState('');
const handleSubmit = (event) => {
event.preventDefault();
addTodo({ title: todoTitle, id: todos.length });
setTodoTitle('');
};
...
Thanks to this, we no longer need to use dispatch manually and our application is still working as before.
Summary
Today you’ve learned:
- how to install Redux, write store, action (and action creator), and reducer;
- how to connect the application with Redux store and dispatch actions;
- how to use dev tools to see what is happening inside.
However, this is very basic knowledge and a very simple code. There is a lot of things that can be improved, like: move types to consts, generating id on the reducer side, disabling dev tools for production code, etc.
I intend to create a series of articles about good practices in Redux, so stay tuned!
Link to repo: https://github.com/norbertsuski/redux-tutorial.git