Using redux
Table of contents
In this post, we won’t explain why to use redux. We’ll just start using it and explain some terms along the way. We’ll start simple with vanilla JavaScript and enhance it as we go with React.
Let’s open a CodeSandbox with plain JavaScript and add redux as a dependency. You can use this one if you like.
Redux, seems complicated and an overkill for simple applications. And that’s true if you ask me. It’s an overkill for simple applications but not really complicated. Just uses some fancy terminology that can deter you from using it. So let’s try to change this by quickly introducing you to some terms.
Redux saves the application state in a store. The state is a simple JavaScript object. Here we’ll create an application that tracks the things we have to do (TODO’s). In this case, the state could be the following:
const state = {
todos: [],
};
If we want to change the state, we use a reducer. A reducer is just a function like the following:
const todoReducer = (state, action) => state;
It takes the application state and an action as arguments. Then calculates the new state and returns it back (to the store, behind the scenes). In our case it doesn’t calculate anything, just returns the state back.
We mentioned the term action. An action, just like the state, is a plain JavaScript object. That object describes how we want to change the state. In this case, we want to add a new todo:
const buyMilkAction = {
type: "ADD_TODO",
stuffToAdd: "buy milk",
};
As you can see, the action has a type and optionally a field with some stuff relevant to the action we want to perform. And that’s it for now.
To summarize: We have a store that contains our state (an object). In order to change that state, we use a function that’s called a reducer. The reducer changes the state according to an action (has a type). Lets now try to actually use them and fill some gaps.
Using redux with plain JavaScript
We start in our index.js file by creating the store: (Remember, I’m using this codesandbox)
import { createStore } from "redux";
const store = createStore();
The store needs a reducer to change the state. So we’ll paste the one we created before and pass it to the store as a parameter:
import { createStore } from "redux";
const todoReducer = (state, action) => action;
const store = createStore(todoReducer);
Ok, but we haven’t initialized our state. Right now is undefined so let’s create an empty todo array. You’d think we’d do that in our store. But we actually update the state in the reducer. And that’s because only the reducer updates the store’s state. Not even the store itself. Keep that in mind for later. Let’s initialize the state inside the reducer:
import { createStore } from "redux";
const initialState = {
todos: [],
};
const todoReducer = (state = initialState, action) => action;
const store = createStore(todoReducer);
We didn’t do something special here. We just used a default parameter value (the =) which is an ES6 JavaScript feature. As we said before, we want to buy some milk. So in order to achieve that, we’ll paste the buyMilk action we created before:
import { createStore } from "redux";
const initialState = {
todos: [],
};
const todoReducer = (state = initialState, action) => action;
const store = createStore(todoReducer);
const buyMilkAction = {
type: "ADD_TODO",
stuffToAdd: "buy milk",
};
Now, we want to send that action to the store and update our state. The store’s reducer will intercept that action on behalf of the store (think of the reducer as the store’s butler). When we want to send an action we use a fancy word called dispatch. So let’s “dispatch” the buy milk action to the store:
import { createStore } from "redux";
const initialState = {
todos: [],
};
const todoReducer = (state = initialState, action) => action;
const store = createStore(todoReducer);
const buyMilkAction = {
type: "ADD_TODO",
stuffToAdd: "buy milk",
};
store.dispatch(buyMilkAction);
The reducer right now doesn’t do anything to our state. If we want to add the buy milk todo into the array, we’ll have to tell our reducer to do that:
import { createStore } from "redux";
const initialState = {
todos: [],
};
const todoReducer = (state = initialState, action) => {
if (action.type === "ADD_TODO")
return {
todos: [...state.todos, action.stuffToAdd],
};
return state;
};
const store = createStore(todoReducer);
const buyMilkAction = {
type: "ADD_TODO",
stuffToAdd: "buy milk",
};
store.dispatch(buyMilkAction);
Let’s explain what we happened here.
First of all, you’ll notice that we don’t just add the todo to the state directly with todos.push(newTodo)
for example. Instead, we create a new state object. Hold on to your seats, we’re about to use some fancy words again. That’s because the reducer should be a pure function. A pure function is a function that doesn ’t mutate (change) the arguments (the state in this case) but instead creates a new state object and returns it to the store. Why you ask. Because when you mutate the state, in let’s say 5 different places in your application, you’ll most certainly get to a point where something changes the state in a way you didn’t expect. This is called a bug and it’s not fun wasting time, trying to find and fix it. So in order to avoid introducing bugs to our application, we change the state in only one place (the reducer) and don’t directly mutate it. We just return a new one.
We also use array destructuring (the 3 dots ...
). This way we separate our array (destructure) into simple elements (strings in our case) and add them into the new array. At last, we add the new todo at the bottom of the array.
Now, if you want to see if this code actually works, you can call the getState method of the store (at the end of the index.js file) and log it at the console:
console.log(store.getState());
A more elegant way to check our state is to subscribe to the store for state updates. By subscribing, we ask the store to run a function when the state changes. In our case we’ll just print the state in the console:
import { createStore } from "redux";
const initialState = {
todos: [],
};
const todoReducer = (state = initialState, action) => {
if (action.type === "ADD_TODO")
return {
todos: [...state.todos, action.stuffToAdd],
};
return state;
};
const store = createStore(todoReducer);
store.subscribe(() => {
console.log("todos updated", store.getState());
});
const buyMilkAction = {
type: "ADD_TODO",
stuffToAdd: "buy milk",
};
store.dispatch(buyMilkAction);
Ok, that’s nice and all, but if we want to add more things in our todo list we’ll soon face a problem. We’ll start repeating ourselves. I don’t know about you, but when I hear that I’m about to repeat myself, alarms start echoing in my head that I’m doing something really wrong. And of course, I want to prevent myself from doing that. For example, if we want to not only buy milk but additionally learn redux and then go to the gym, we’ll have to create 2 new action objects:
import { createStore } from "redux";
// skipping stuff in the middle...
store.subscribe(() => {
console.log("todos updated", store.getState());
});
const buyMilkAction = {
type: "ADD_TODO",
stuffToAdd: "buy milk",
};
const learnReduxAction = {
type: "ADD_TODO",
stuffToAdd: "learn redux",
};
const goToTheGymAction = {
type: "ADD_TODO",
stuffToAdd: "go to the gym",
};
store.dispatch(buyMilkAction);
store.dispatch(learnReduxAction);
store.dispatch(goToTheGymAction);
Ugh… disgusting. 🤮 For that reason, it would be nice if we added a function that creates actions. But what should we call that type of function? Hmm… what about we call it an action creator! So let’s add to our code an action creator that creates (and returns) actions:
import { createStore } from "redux";
// skipping stuff in the middle...
store.subscribe(() => {
console.log("todos updated", store.getState());
});
const addTodo = (todo) => ({
type: "ADD_TODO",
stuffToAdd: todo,
});
store.dispatch(addTodo("buy milk"));
store.dispatch(addTodo("learn redux"));
store.dispatch(addTodo("go to the gym"));
That’s better! Now, we could continue explaining how to use redux with vanilla JavaScript. But to be honest, when I use redux I usually do it inside a React application. So what I suggest to do now is to integrate redux inside a React application and learn some more things along the way.
But… before we do that let’s recap what we’ve done so far. We created a store and passed a reducer responsible for our TODO’s to the store. We initialized the TODO’s to an empty array and then added a function that creates actions. So far we created only one type of action which is to add TODO‘s. We used that action creator to create some actions and then we dispatched those actions to our store. We also saw how we can subscribe to the store and get notified about the state changes.
Using redux in React
You could follow the tutorial by creating a new sandbox with React this time. But I want to complicate my life (and yours) by creating a new create-react-app project on the disk. I suppose you have node installed on your computer. We start by creating a folder for our project. Name it whatever you like, and then open it. We open a terminal inside that folder and we type:
npx create-react-app .
to create a new create-react-app application. When that’s done, you open the generated project with your favorite Visual Studio Code. I mean your favorite text editor. You can type npm start
to start the project and check if everything is installed correctly.
Now let’s install redux again by typing in the terminal:
npm i redux
Next, we’ll go to src/App.js file, and copy the stuff we wrote before. If we do that we end up with the following file:
import React from "react";
import { createStore } from "redux";
import "./App.css";
const initialState = {
todos: [],
};
const todoReducer = (state = initialState, action) => {
if (action.type === "ADD_TODO")
return {
todos: [...state.todos, action.stuffToAdd],
};
return state;
};
const store = createStore(todoReducer);
store.subscribe(() => console.log("todos updated", store.getState()));
const addTodo = (todo) => ({
type: "ADD_TODO",
stuffToAdd: todo,
});
const boundAddTodo = (todo) => store.dispatch(addTodo(todo));
boundAddTodo("buy milk");
boundAddTodo("learn redux");
boundAddTodo("go to the gym");
class App extends React.Component {
render() {
return (
<div className="App">
<header className="App-header">
<ul>
{store.getState().todos.map((todo, index) => (
<li key={index}>{todo}</li>
))}
</ul>
</header>
</div>
);
}
}
export default App;
Kinda ugly right? We can use redux inside this specific component but if we change components and try to use the store we’ll run into problems. For that reason, we’ll use a library that “glues” together React and redux. And, as you may have guessed, it’s called react-redux. We install it with:
npm i react-redux
This library uses the React’s Context API to pass down the store to our components. We start by importing a React component called Provider and wrap our App component in the src/index.js file. Here is a good place to additionally create our store. So we create the store, initialize the state, create the TODO reducer, pass it to the store and finally wrap our App component with the Provider:
import React from "react";
import ReactDOM from "react-dom";
import { createStore } from "redux";
import { Provider } from "react-redux";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
const initialState = {
todos: [],
};
const todoReducer = (state = initialState, action) => {
if (action.type === "ADD_TODO")
return {
todos: [...state.todos, action.stuffToAdd],
};
return state;
};
const store = createStore(todoReducer);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
serviceWorker.unregister();
Ok but I don’t like the fact that we create the reducer here. I think that the reducer should be on a separate file. Separation of Concerns is a thing so we should try to enforce that. So we create a file named todoReducer.js in src/reducers and paste there the TODO reducer:
const initialState = {
todos: [],
};
const todoReducer = (state = initialState, action) => {
if (action.type === "ADD_TODO")
return {
todos: [...state.todos, action.stuffToAdd],
};
return state;
};
export default todoReducer;
Then we can import it in our index.js:
import React from "react";
import ReactDOM from "react-dom";
import { createStore } from "redux";
import { Provider } from "react-redux";
import todoReducer from "./reducers/todoReducer";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
const store = createStore(todoReducer);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
serviceWorker.unregister();
In the same way we put the reducers in their own files, we’ll put now the action creators in their own files. So we create todoActions.js file in src/actions
folder and write the following:
export const addTodo = (todo) => ({
type: "ADD_TODO",
stuffToAdd: todo,
});
Can you see a difference between the reducer and the action files? In the first, we export with “export default” the TODO reducer because it’s only one. In contrast, in the action file, we export the action using the “export const”. That’s because we expect to have more than one action creators.
Now we want to remove the plain redux stuff from App.js component and start using the stuff from react-redux. If we want to get the application state to our App component, we’ll have to wrap it with a higher order component (also called HOC). If you don’t know what a HOC is, don’t worry, it doesn’t matter right now. It’s an “advanced” React pattern that adds functionality to a component. In this case, allows us to access the store’s state. We additionally import the action and we change the App.js to the following:
import React from "react";
import { connect } from "react-redux";
import { addTodo } from "./actions/todoActions";
import "./App.css";
class App extends React.Component {
render() {
return (
<div className="App">
<header className="App-header">
<ul>
{store.getState().todos.map((todo, index) => (
<li key={index}>{todo}</li>
))}
</ul>
</header>
</div>
);
}
}
export default connect()(App);
Right now, if you try to run the application, you’ll get an error that says store is not defined. And that’s something we should expect. If we want to get the state in our App component we’ll have to pass a function to connect that maps the store state to the App’s props. Sounds weird at first but it’s easier if you see the code. Consider the following code:
import React from "react";
import { connect } from "react-redux";
import "./App.css";
class App extends React.Component {
render() {
const { todos } = this.props;
return (
<div className="App">
<header className="App-header">
<ul>
{todos.map((todo, index) => (
<li key={index}>{todo}</li>
))}
</ul>
</header>
</div>
);
}
}
const mapStateToProps = (state) => ({ todos: state.todos });
export default connect(mapStateToProps)(App);
Of course we still see nothing. That’s because the initial todo list is empty if we don’t add our todos to the store. In vanilla JavaScript we did that by calling store.dispatch(actionCreator) method. A good way to do this with react-redux, is to pass a second function to connect. This time we want to map the dispatch to props. You can see a pattern here. We map things to props.
Mapping a dispatch to props sounds a bit weird also. So let’s break it down a little bit. What we actually want to achieve is, first of all, to get access to the store.dispatch method we used earlier. So, as we said, we do that by passing a second parameter to connect. That parameter is, like mapStateToProps, a function. That function returns an object that has a property (wow an object that has a property! 🤦). That object will be part of the component’s props. So the object property is a prop of the App component. Just like we did with mapStateToProps. We added a prop, our todos. That prop is not an array this time. It’s a function that takes a todo as an argument and dispatches an action with that todo. It’s a bit difficult to get your head around it at first, especially because I think I made a bad job explaining it, but it’ll make sense if you see the code and practice it a few times. In the following code, we add the TODO’s at the componentDidMount method.
import React from "react";
import { connect } from "react-redux";
import { addTodo } from "./actions/todoActions";
import "./App.css";
class App extends React.Component {
componentDidMount() {
const { addTodo } = this.props;
addTodo("buy milk");
addTodo("learn redux");
addTodo("go to the gym");
}
render() {
const { todos } = this.props;
return (
<div className="App">
<header className="App-header">
<ul>
{todos.map((todo, index) => (
<li key={index}>{todo}</li>
))}
</ul>
</header>
</div>
);
}
}
const mapStateToProps = (state) => ({ todos: state.todos });
const mapDispatchToProps = (dispatch) => ({
addTodo: (todo) => dispatch(addTodo(todo)),
});
export default connect(mapStateToProps, mapDispatchToProps)(App);
In a real application, we would probably add our TODO’s when we click a button that submits a form with a todo.
That’s it for now! Let’s recap one more time what we saw here. We saw that our state is a plain JavaScript object that lives in a store. When we want to change the state, we send (dispatch) an action to the store. The action is again a JavaScript object that describes how to change the state and optionally contains information about the state change. Now, that action is intercepted by a function which is called a reducer. The reducer takes the state and the action as arguments, calculates the new state and returns a new state object. Remember that the reducer should be a pure function. Meaning that it doesn’t mutate the state but instead returns a new state object. Also being a pure function means that we don’t perform side-effects (console.log, network requests etc.) inside the reducer, but this is a story for another time.
Additionally, we saw how to use redux with plain JavaScript and later with React. To use redux with React we’ll have to wrap our App component in a Provider component that takes our store as a prop. When we want to access the state we use a higher order component called connect. We pass to connect a function, as the first argument, that can access the state and maps the state, we are interested in, to a prop. If we want to change the state we pass a second function to connect. This time we want to access the dispatch method (maps the dispatch to a prop). So we create an additional prop that, this time is a function. When we call that function with a todo as an argument, it adds that todo in our state.
Before you go a little (big) disclaimer. In the beginning, I stated that “Redux is not complex, just uses some fancy terminology.” Well… I lied a bit there because I wanted to get you inspired and start using it. It can be complex when you start adding third-party store enhancers for asynchronous action creators (eg. the redux-thunk library, remember two paragraphs ago that I mentioned no side-effects in the reducer?). Or if you try to integrate redux with a back-end service like Google’s Firebase. And to be honest, even the integration of redux in React with react-redux we saw here, is not the most straightforward thing ever. So I suggest to take a look at the docs and go over the basics again and again. When you want to learn something, practice and repetition is maybe the most effective technique to achieve that.
Other things to read
Popular
- Reveal animations on scroll with react-spring
- Gatsby background image example
- Extremely fast loading with Gatsby and self-hosted fonts