TL;DR: Actually, the idea is to create an article that is not too long at all (TL;DR stands for Too Long; Didn't Read 😊). However, to summarize in a few words, the idea here is to show through practical, short examples how Redux works and what are its main pieces. To find the final code that we are going to create in this article, please check this GitHub repository.
What is Redux
Mostly used with React, Redux is a storage facility that helps JavaScript applications to manage state. Note that I started the introductory sentence with "Mostly used". What I mean is that we do not have to use Redux with React. We don't even need a browser to use Redux. We can use it to control the state of a Node.js backend application, for example.
The biggest advantage of Redux is that this facility works as the single source of truth for our data. That is, whenever we want to know the state of our application, we have to look into a single place, the Redux Store. Another advantage of Redux, as we will see in the next section, is that to manage the state, we have only to deal with simple objects and pure functions. Redux does not rely on fancy, extensive APIs. It is quite the opposite actually.
"Redux is as simple as it gets."
Tweet This
Learning Redux
To learn how to properly use Redux, we have to understand three basic concepts of this library. The first one is called store. When using Redux to manage our state, we let it keep an updated version of this state in the store. This is the main purpose of this piece of Redux. The store exists to hold (store) the current state of our data and to become the single source of truth.
The second concept is called reducer. Reducer is nothing but a pure function that gets our app's current state and generates a new state based on an action. Actions are the third concept that we are interested in. To define an action to be applied to our state, we simply create an object with a type
and any arbitrary number (0..N
) of properties.
For example, we can have as the current state a simple JavaScript object that contains a person’s name. To change this state (object), we use a reducer that, based on an action, updates the person with arbitrary data. The following code snippet illustrates these concepts.
const BIRTHDAY_UPDATE = 'BIRTHDAY_UPDATE';
const initialState = {name: 'Bruno Krebs'};
function reducer(state, action) {
const {birthday} = action;
switch (action.type) {
case BIRTHDAY_UPDATE:
return {
...state,
birthday
};
default:
return state;
}
}
const updateAction = {
type: BIRTHDAY_UPDATE,
birthday: new Date(1984, 10, 20)
};
const newState = reducer(initialState, updateAction);
console.assert(initialState.birthday === undefined, 'Initial state must not be changed');
console.assert(
newState.birthday !== undefined &&
newState.birthday.getTime() === new Date(1984, 10, 20).getTime(),
'New state must contain 1984/10/20 as the birthday'
);
Note that the spread operator (
…state
) used in the example above is a feature recently introduced to JavaScript and might not be available on all environments. For example, it is not available on Node.js prior to version8.2.1
. Therefore, we must have a Node.js newer than8.2.1
or we need to run this in a browser compatible with this feature.
In the snippet exhibited, we can see that we use the initialState
to generate a newState
. This new state is the product of calling the reducer
function with the updateAction
object and the initialState
. After passing the state and the action to the reducer, we get a new state where we can still find the name of the person, and the new birthday
property correctly applied.
Although simple, the code snippet used above shows another concept that is quite important when using Redux. The state does not change. What happens is that we have to generate a new state (or return the same) when using Redux. The pros of creating new states instead of updating the current one is that, by following this paradigm, we enable traceability in our application's state. By enabling traceability, we also enable other great features like the possibility to time travel.
After understanding these three concepts (well, four with state immutability), we are ready to start using Redux in practice. In the following sections, we are going to create a small Node.js script that uses Redux to manage state.
Using Redux
You might have noticed that we haven't used Redux in the previous section. A great characteristic of Redux is that it relies on simple concepts and structures. As we will see, introducing Redux to manage states in apps is easy. The Redux library itself is quite small, have a great performance, and is really intuitive.
To keep things organized, let's create a new Node.js project, and add actions and reducers to it. In a terminal, let's issue the following commands:
# create a dir to our project
mkdir redux-node
# change working directory to it
cd redux-node
# initialize the directory as a NPM project
npm init -y
# create the source folder
mkdir src
# create files for the main app, actions, and reducers
touch src/index.js src/actions.js src/reducers.js
These commands will give us a brand new project with the basic structure that we will need. To makes our lives easier and before proceeding with the next steps, let's open this project on an IDE (like WebStorm or Visual Studio Code).
Creating Redux Actions
Now, let's open the src/actions.js
file and add the following action creators and action types to it:
// action types
export const ADD_EXPENSE = 'ADD_EXPENSE';
export const REMOVE_EXPENSE = 'REMOVE_EXPENSE';
// action creators
export const addExpense = expense => ({
type: ADD_EXPENSE,
expense
});
export const removeExpense = expense => ({
type: REMOVE_EXPENSE,
expense
});
These action creators are quite simple. They simply returns objects that contain type
, to indicate if it is a removal or an addition, and an expense
as the payload. We won't invest time creating automated tests to these action creators, as they are trivial.
Creating Redux Reducers
We are going to add the business logic of our tutorial app in the reducer that we are going to create in this section. This reducer will have a switch
statement that, based on an action, will trigger the proper function to generate the new state. Let's open the src/reducers.js
file and add the following reducer definition to it:
import {ADD_EXPENSE, REMOVE_EXPENSE} from './actions';
export default expenses;
export const initialState = {
expenses: [],
balance: 0
};
function expenses(state = initialState, action = {}) {
switch (action.type) {
case ADD_EXPENSE:
return addExpense(state, action.expense);
case REMOVE_EXPENSE:
return removeExpense(state, action.expense);
default:
return state;
}
}
function addExpense(state, expense) {
return Object.assign({}, state, {
expenses: [...state.expenses, expense],
balance: state.balance + expense.amount
});
}
function removeExpense(state, expense) {
const persistedExpense = state.expenses.find(item => item.id === expense.id);
return Object.assign({}, state, {
expenses: state.expenses.filter(item => item.id !== expense.id),
balance: state.balance - persistedExpense.amount
});
}
To decide exactly what function to call (addExpense
or removeExpense
), the reducer created by this file (expenses
) compares the action.type
with both ADD_EXPENSE
and REMOVE_EXPENSE
constants. After identifying the correct type, it triggers the proper function passing the current state
of the application and the expense
in question.
Testing Redux Reducers with Jest
It is easy to create an automated test to validate the behavior of this reducer. As reducers are pure functions, we don't need to mock anything. We just need to generate some samples of expenses and actions, trigger our reducer with them, and check the generated output. Let's install the jest
test runner and babel
to help us testing the reducer.
npm i -D jest babel-jest babel-preset-es2015
After that, let's create a file called .babelrc
in the project root (i.e., alongside with the package.json
file), and configure Babel to use the es2015
preset:
{
"presets": ["es2015"]
}
Also, let's update the scripts
property in the package.json
file so we can easily run jest
:
{
// ...
"scripts": {
"test": "jest",
"test:watch": "npm test -- --watch"
},
// ...
}
With these scripts in place, we can create the test suite that will validate the expenses
reducer. Let's create a file called reducers.test.js
alongside with reducers.js
and define two tests in a new test suite, as follows:
import {addExpense, removeExpense} from './actions';
import expenses, {initialState} from './reducers';
describe('reducers', () => {
it('should be able to add expenses', () => {
const stateStep1 = expenses(initialState, addExpense({
id: 1,
amount: 20
}));
expect(stateStep1.expenses.length).toEqual(1);
expect(stateStep1.balance).toEqual(20);
const stateStep2 = expenses(stateStep1, addExpense({
id: 2,
amount: 10
}));
expect(stateStep2.expenses.length).toEqual(2);
expect(stateStep2.balance).toEqual(30);
});
it('should be able to remove expenses', () => {
const stateStep1 = expenses(initialState, addExpense({
id: 1,
amount: 55
}));
expect(stateStep1.expenses.length).toEqual(1);
expect(stateStep1.balance).toEqual(55);
const stateStep2 = expenses(stateStep1, addExpense({
id: 2,
amount: 36
}));
expect(stateStep2.expenses.length).toEqual(2);
expect(stateStep2.balance).toEqual(91);
const stateStep3 = expenses(stateStep2, removeExpense({
id: 1
}));
expect(stateStep3.expenses.length).toEqual(1);
expect(stateStep3.balance).toEqual(36);
});
it('should return the default state', () => {
expect(expenses()).toEqual(initialState);
});
});
The test suite and its tests are a little bit verbose, but they are easy to understand. We start by importing the addExpense
and removeExpense
action creators. After that, we import the expenses
reducer from its source alongside with the initialState
. Lastly, we use the describe
function to define the test suite and the it
function to create three tests.
The first two tests are pretty similar. Therefore, let's analyze the first one to understand how they work. The first step executed by this test calls the expenses
reducer passing to it the initialState
and the addExpense
action creator. As the parameter of this action creator, we pass an expense with id = 1
and amount = 20
. We then check if the result of the expenses
execution, the stateStep1
, contains a single expense and if the balance
is equal 20. After that, we execute a similar process that validates if the expenses
reducer accepts a new expense and updates the balance
accordingly. The difference in the second test is that, after adding two expenses, we use the reducer to remove an expense.
Let's run the npm test
command to verify our implementation. If we followed the steps above correctly, we should get an output similar this:
> redux-node@1.0.0 test /Users/brunokrebs/git/tmp/redux-node
> jest
PASS src/reducers.test.js
reducers
✓ should be able to add expenses (3ms)
✓ should be able to remove expenses (1ms)
✓ should return the default state
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 0.834s, estimated 1s
Ran all test suites.
Defining a Redux Store
So far, we haven't used the central piece of Redux, the Redux Store. We have only defined two functions to create Redux Actions and a Redux Reducer. Actually, we haven't even installed and used Redux anyhow. So, the first thing we need to do next is to install Redux. To do so, let's issue the following command in our terminal:
# from the project root
npm i redux
Now, it's time to create a Redux Store and put our reducer and our action creators to work. As we want to use modern JavaScript code, let's install babel-cli
and a plugin:
npm i -D babel-cli
The plugin simply guarantees that we can use the spread operator with Babel. After installing both dependencies, let's open the index.js
file and add the following code:
import {createStore} from 'redux';
import {addExpense, removeExpense} from './actions';
import expenses from './reducers';
const store = createStore(expenses);
store.dispatch(addExpense({
id: 1,
amount: 45
}));
store.dispatch(addExpense({
id: 2,
amount: 20
}));
store.dispatch(addExpense({
id: 3,
amount: 30
}));
store.dispatch(removeExpense({
id: 2
}));
console.assert(store.getState().balance === 75);
console.assert(store.getState().expenses.length === 2);
Pretty simple, right? To create a Redux Store, all we had to do was to import the createStore
function from Redux and call it passing our reducer. Interacting with the store was not hard either. After importing the action creators, we simply called the dispatch
function of the store, passing to it actions created by our action creators (addExpense
and removeExpense
).
In the end, to verify that the store ended up in the correct state, we added two console.assert
calls. The first one showed that the balance
is indeed 75, and the second one guaranteed that we finished with two expenses in the last state.
To run our code, we need to use the babel-node
command provided by Babel. To easily run this command, let's edit the package.json
file and add the following record to the script
property:
{
// ...
"scripts": {
"start": "babel-node src/",
// ...
},
// ...
}
After that, we can simply issue npm start
and we will see Babel run our code and assert that we get the expected state.
"I just finished a Redux tutorial."
Tweet This
Aside: Auth0 Authentication with JavaScript
At Auth0, we make heavy use of full-stack JavaScript to help our customers to manage user identities, including password resets, creating, provisioning, blocking, and deleting users. Therefore, it must come as no surprise that using our identity management platform on JavaScript web apps is a piece of cake.
Auth0 offers a free tier to get started with modern authentication. Check it out, or sign up for a free Auth0 account here!
Then, go to the Applications section of the Auth0 Dashboard and click on "Create Application". On the dialog shown, set the name of your application and select Single Page Web Applications as the application type:
After the application has been created, click on "Settings" and take note of the domain and client id assigned to your application. In addition, set the Allowed Callback URLs and Allowed Logout URLs fields to the URL of the page that will handle login and logout responses from Auth0. In the current example, the URL of the page that will contain the code you are going to write (e.g. http://localhost:8080
).
Now, in your JavaScript project, install the auth0-spa-js
library like so:
npm install @auth0/auth0-spa-js
Then, implement the following in your JavaScript app:
import createAuth0Client from '@auth0/auth0-spa-js';
let auth0Client;
async function createClient() {
return await createAuth0Client({
domain: 'YOUR_DOMAIN',
client_id: 'YOUR_CLIENT_ID',
});
}
async function login() {
await auth0Client.loginWithRedirect();
}
function logout() {
auth0Client.logout();
}
async function handleRedirectCallback() {
const isAuthenticated = await auth0Client.isAuthenticated();
if (!isAuthenticated) {
const query = window.location.search;
if (query.includes('code=') && query.includes('state=')) {
await auth0Client.handleRedirectCallback();
window.history.replaceState({}, document.title, '/');
}
}
await updateUI();
}
async function updateUI() {
const isAuthenticated = await auth0Client.isAuthenticated();
const btnLogin = document.getElementById('btn-login');
const btnLogout = document.getElementById('btn-logout');
btnLogin.addEventListener('click', login);
btnLogout.addEventListener('click', logout);
btnLogin.style.display = isAuthenticated ? 'none' : 'block';
btnLogout.style.display = isAuthenticated ? 'block' : 'none';
if (isAuthenticated) {
const username = document.getElementById('username');
const user = await auth0Client.getUser();
username.innerText = user.name;
}
}
window.addEventListener('load', async () => {
auth0Client = await createClient();
await handleRedirectCallback();
});
Replace the
YOUR_DOMAIN
andYOUR_CLIENT_ID
placeholders with the actual values for the domain and client id you found in your Auth0 Dashboard.
Then, create your UI with the following markup:
<p>Welcome <span id="username"></span></p>
<button type="submit" id="btn-login">Sign In</button>
<button type="submit" id="btn-logout" style="display:none;">Sign Out</button>
Your application is ready to authenticate with Auth0!
Check out the Auth0 SPA SDK documentation to learn more about authentication and authorization with JavaScript and Auth0.
Conclusion
As we can see, Redux is an easy technology to reason about. Although not hard, correctly understanding its three main pieces (the store, reducers, and actions) is important before we move to other topics, like integrating with front-end frameworks. However, once we learn Redux's concepts, we can integrate it with, for example, React to get a great foundation for modern Single Page Apps.
By the way, in our blog we have an article that shows how to properly secure React and Redux Apps with JWTs. Take a look at it if you are going to use these technologies in your next project.