Redux for React: A Simple Introduction

Prionto Abdullah
12 min readJan 5, 2021

👋 Hey! I’m learning about Redux today. I’ve found that the best way for me to learn is to simply write it out as I go about doing it. It’s useful for me, and hopefully for you too

What is Redux?

Redux promotes itself as a predictable state container for JavaScript apps.

I don’t know about you, but that means nothing to me. I know what state is. I know what a container is. Why I’d need a library to contain my state? 🤷‍♂️ No idea. But hey, everyone says Redux is cool and I don’t want to be a loser.

So what does it actually mean? Well, when we think about how we normally manage state in React, you’re probably doing one of these things:

  1. Component drilling”, where you pass objects down down down from a parent component to some far-off child. As an example, if you want to show the user’s avatar you might be passing a user object down from your App to your Page to your Navbar to your UserMenu to your Avatar. Or something worse.
  2. Free-for-all”, where you’ve grown tired of this prop passing hell and each component sort of manages its own state, possibly through some intermediary data store you created. This is where business and presentation logic start mixing, and after some time you want to set literal fire to your entire codebase because of the 💩 you’ve created.
  3. Children props”, a somewhat reasonable middle road where you use the children property to pass JSX and component lists around your app, from high to low, to prevent having to drill down props too excessively.

None of these solutions are great once your app becomes bigger than “small”.

Before you dive in

As I’ve personally experienced in learning about Redux and putting together this post, it should be mentioned that Redux has a bit of a learning curve.

Beyond learning how Redux and its concepts work, you’ll may have to go knee-deep in immutable state patterns, and you’ll probably have to add a few extra libraries in the mix to make it all tick.

I think it’s a good learning investment, but be weary of over-engineering. :) Especially for smaller projects, you have to wonder if Redux and its direct and indirect dependencies might be a bit overkill.

How does Redux make life better?

💉 Prop injection, no more drilling

Redux wraps itself around your components, and allows you to inject properties straight into them from your application’s store a single, central data repository that holds all your application state.

If we take a that UserAvatar as an example, let’s consider what that component would look like with Redux:

const mapStateToProps = state => ({
user: state.user
});const UserAvatar = connect(mapStateToProps)(({ user }) => (
<img src={user.avatar} alt={user.name}>
));

The magic happens when we use connect from Redux — this allows us to pull the state from the store, and extract the properties we need, without drilling.

😇 Pure components

Whenever you use connect, it automatically makes those components pure: they only re-render when their state changes in the Redux store.

This effectively means you won’t have any unnecessary re-renders. Normally you’d have to implement shouldComponentUpdate yourself to control this effectively, but now you get it “for free” with Redux.

It’s pretty neat to be able to have independent, reusable and predictable components that don’t rely on anything except state input.

✅ Plus a few other benefits

Having a central data store that hosts our entire state has a few advantages:

  • Independent components are extremely predictable and easy to test.
  • Having your state centralized is neat: you can persist or save it, you can restore it, you can manipulate it, etc — all in one central place.
  • It makes code sharing and server side rendering easier too: all you need to do is share the state, and the client and server should render identically.
  • It makes testing and debugging a lot easier. You can even time travel! To get an idea of how powerful this concept is, check out the Redux dev tools.

Redux dev tools: it’s never been easier to analyse and debug your application state.

Redux: concepts, basics and concerns

Let’s think about putting together a todo-list application. Our state would consist of a list of tasks, some of which may have been completed. A user might want to add, remove and complete items, which would modify state.

How do we get that to work, what are actions and reducers, and how do they interact with our store and the state it holds?

📕 Actions 101

So, as you might expect, an action could be something like “add a todo”. In Redux each action is simply represented as a simple JavaScript object:

{
type: "ADD_TODO",
whatever: "add your own payload"
}

That type property is required for each Redux action. It’s convention to write those values in UPPER_CASE style. The rest of the object is yours to fill with whatever payload you want.

👉 So, how do we use actions, and how do we fire them? To simplify things and ensure everything behaves consistently, you’d usually write each action as a separate function (known as an “action creator”) — for example:

function addTodo(text) {
return {
type: "ADD_TODO",
text
};
}

The actual dispatching doesn’t happen there! Instead, from your component code, where the actual invocation happens, you’d do something like this:

store.dispatch(addTodo("My first todo!"));

So: we dispatch actions by invoking dispatch() on our store, and passing an action object to it, which usually comes from an action creator.

Now we know how to send actions, but we haven’t actually implemented their logic at all. That’s where reducers come in to play, so let’s dive a bit deeper.

📕 Intermission: The Three Principles

Now we have some idea of how the data flows through a Redux application, it’s important to mention the three major principles it adheres to:

  1. 1️⃣ There is one single source of truth:
    All application state only comes from one place: your store.
  2. 🔒 State is read-only:
    We only read state, never change it. Actions may result in a new state.
  3. 😇 Changes are made through pure functions:
    We never mutate the state directly, but reducers can return a new state.

📕 Reducers 101

At it’s core, a reducer is just a function that processes an action: it takes two parameters: the current state and the action object. It can either generate a new state, or retain the current state.

For our example, here’s what a reducer that implements the ADD_TODO action might look like:

function todoApp(state, action) {
switch (action.type) {
case "ADD_TODO":
return Object.assign({}, state, {
todos: [
...state.todos,
{
text: action.text,
completed: false
}
]
});
default:
return state;
}
}

This is a simple example. If we receive a new todo, we create a new state with that todo added into it. We do this by cloning the previous state onto a new object, instead of changing the existing state.

Note how we use Object.assign() here: by passing an empty object, {}, as the first parameter, we are initializing a brand new object and merging the previous state into it with some modifications. This ensures we’re not modifying the previous state object, which is immutable.

You’ll also notice that if we don’t recognize the action type, we simply return back the current state and leave it as-is.

🤵 Executive summary: The store holds and controls the state, the reducer can create a new state, based on objects that describe actions.

✋ Hold up: a giant switch statement with verbose code?

If you take these examples and build on them, you’re left with a giant and, over time, growing, switch statement in your reducer that holds every single operation that modifies state. It’s easy to see how that can get out of hand.

Moreover, every single operation that creates a new state, requires you to write rather verbose code, which is not ideal:

// The code you'd want to write, ideally:
state.todos.push(todo);
state.todos[123].checked = true;// The "pure" code you end up writing in Redux reducers:
return Object.assign({}, state, {
todos: [...state.todos, todo]
});return {
...state,
todos: {
...state.todos,
123: {
...state.todos[123],
checked: true
}
}
};// ...many times over, in a giant switch statement.

So, how do we cope with that? Redux has a nice page in their docs called Reducing Boilerplate. It’s a page that works really hard to convince you that their design choices make sense, and offers some solutions as well.

The major takeaways for me:

  • Use constants for things like action types (type less; reduce mistakes).
  • Action creators make sense for the sake of consistency and reliability, but you can unleash your creativity in how you write or generate them.
  • The same is true for the giant switch statement: it’s pretty easy to write a little bit of helper code that splits it into independent functions.
  • You can split and merge reducers to help organize code.
  • Middleware like redux-thunk is pretty much essential for doing async operations in reducers (like hitting an API).
  • Middleware like redux-act makes it easier to write action creators and reducers, and pushes them together, which also helps reduce boilerplate.
  • Libraries like Immer make working with immutable state a LOT easier — more on that later.

I’ll be mentioning some of these techniques and libraries later as we start putting things together, and improving it step-by-step. Let’s get to the code!

🥪 Putting it all together

🌯 Store, Provider, and wrapping your App

The Provider class wraps around your application, which lets underlying components access your store via connect. Here’s what that looks like:

// src/index.jsimport React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";import App from "./app.js";
import store from "./store.js";
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);

🗄 Creating a Store and adding Reducers

In the example above, I referred to store.js. The most simple example of that file would look like a little something like this:

// src/store.jsimport { createStore } from "redux";
import { todoReducer } from "./reducer.js";
const store = createStore(todoReducer);export default store;

We’re using createStore from Redux to create a blank store with a reducer.

That reducer will end up processing all of our action dispatches. We refer to it as the “root reducer” — we only ever pass one reducer directly to the store.

In the real world, with bigger applications, you’d be splitting reducers out and then merging them together into a single root reducer:

rootReducer = combineReducers({
todo: todoReducer,
account: accountReducer
});

👉 Here’s a big thing to note: when you split your reducers, you also split your state into those same namespaces. For the example above, the state would be split into todo and account keys, and the associated reducer would only receive/create that specific part of the state.

➖ Adding the Reducer

Let’s add our reducer in, and add an “initial state” as well:

// src/reducer.jsconst initialState = {
todos: []
};
function todoReducer(state = initialState, action) {
switch (action.type) {
case "ADD_TODO":
return Object.assign({}, state, {
todos: [
...state.todos,
{
text: action.text,
completed: false
}
]
});
default:
return state;
}
}
export default todoReducer;

This is the same as the example as earlier, with that ugly switch statement. We can do better, though! And we need to add “delete” and “toggle” too. So:

✨ Reducing Boilerplate + coping with Immutable Patterns

Let’s follow along with the boilerplate reduction recommendations. We’re going to take care of a couple of things:

  1. We’re going to add constants for our action names.
  2. We’re going to implement complete and delete actions.
  3. We’re going to implement createReducer (as recommended in the docs) so that we can use individual functions instead of a switch statement.
  4. We’re going to clean up todoReducer to follow along with this.

Let’s create a new file to hold all the action constants we want to implement:

// src/actionTypes.jsexport const ActionTypes = {
AddTodo: "ADD_TODO",
CompleteTodo: "COMPLETE_TODO",
DeleteTodo: "DELETE_TODO"
};

We need to implement “complete” and “delete” as well, which introduces a new challenge: how do we approach this with the immutable state?

We can find some useful hints in the docs under Immutable Update Patterns, but the most useful hint of all seems to be a library called Immer, which sounds great: through Immer, we can essentially write “normal” code and forget about most of the usual immutable state concerns. Yay! 🥂🎉

So — adding all that in, this is what the fully implemented reducer looks like:

// src/reducer.jsimport produce from "immer";
import {ActionTypes} from "./actionTypes";
const initialState = {
todos: {}
};
// createReducer as suggested in "reducing boilerplate"
function createReducer(handlers) {
return function reducer(state = initialState, action) {
if (handlers.hasOwnProperty(action.type)) {
// let's integrate immer at this level for ease!
let test = produce(state, draft => {
const handler = handlers[action.type];
return handler(draft, action)
});
console.log(initialState, test);
return test;
} else {
return state
}
}
}
// In the real world, your server would probably assign the ID
// For the sake of this example, we'll auto-increment a counter
let idMaker = 0;
export const todoReducer = createReducer({
[ActionTypes.AddTodo]: (state, action) => {
const text = action.text.trim();
const nextId = idMaker++;
state.todos[nextId] = {
id: nextId,
text: text,
checked: false
};
},
[ActionTypes.CompleteTodo]: (state, action) => {
state.todos[action.id].checked = true;
},
[ActionTypes.DeleteTodo]: (state, action) => {
delete state.todos[action.id];
}
});

We’ve made a few neat improvements to the reducer file, in summary:

  • All the actions (add, complete, delete) are now implemented.
  • Thanks to Immer, the code is easy to write, read and maintain. 👌
  • No more ugly switch statement, but neatly separated functions.

💻 The UI & Basic Action Generators

Okay, with that out of the way, let’s finally put Redux to work!

We’re going to set up up a really simple UI for our todo-app, and wire in the actions that make it all come together. Here’s what I came up with:

🙏 Please hold your applause for this beautiful UI design.

It’s neat how simple the code is. To keep things simple, I’ve merged it all into a single source file. In the real world you’d probably want to split this up:

// src/app.jsimport React from 'react';
import {connect} from 'react-redux';
import {ActionTypes} from './actionTypes.js';// Our action generators:
const addTodo = (text) => ({
type: ActionTypes.AddTodo,
text
});
const deleteTodo = (id) => ({
type: ActionTypes.DeleteTodo,
id
});
const completeTodo = (id) => ({
type: ActionTypes.CompleteTodo,
id
});// Our Todo-item presentation component:
class Todo extends React.Component {
render() {
return (
<div>
<input type={"checkbox"} id={this.props.id} checked={this.props.checked}
onClick={this.props.onComplete}/>
<label htmlFor={this.props.id}>
<strong>{this.props.text}&nbsp;</strong>
</label>
<button onClick={this.props.onDelete}>Del</button>
<hr/>
</div>
);
}
}// The main container component:
class App extends React.Component {
handleKeyPress(e) {
if (e.key === "Enter") {
const text = e.target.value;
if (text) {
this.props.dispatch(addTodo(text));
e.target.value = "";
}
}
}
handleTodoDelete(todo) {
this.props.dispatch(deleteTodo(todo.id));
}
handleTodoCheck(todo) {
if (!todo.checked) {
this.props.dispatch(completeTodo(todo.id));
}
}
render() {
return (
<div>
<h1>✔ TodoApp</h1>
<hr/>
<input placeholder={"Add new todo"} onKeyPress={(e) => this.handleKeyPress(e)}
style={{width: "300px", height: "30px"}} autoFocus={true}/>
<hr/>
{Object.values(this.props.todos).map((todo, i) => (
<Todo key={i} {...todo} onDelete={() => this.handleTodoDelete(todo)}
onComplete={() => this.handleTodoCheck(todo)}
/>
))}
</div>
);
};
}
const mapStateToProps = (state) => ({
todos: state.todos
});
export default connect(mapStateToProps)(App);

So let’s take a look at some of the things happening in this app:

  • In our main export for the App class, we use the connect function to wrap the component in Redux magic.
  • The mapStateToProps function filters the relevant props that we want to extract from the store, and pass to the App component. In our case, we’re just taking out the list of todos. If that list changes, it will re-render.
  • You never access the store directly, instead connect gives us access to props.dispatch for passing actions through to the reducer.

And there we are: a super simple todo app powered by React and Redux! 🙌

🤩 Adding the DevTools

Let’s grab the Redux DevTools and enable it by adding the hook in the createStore call.

const store = createStore(
todoReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);

In case you’re wondering, we’re passing something called an enhancer to the store initializer, which can add “[..] third-party capabilities such as middleware, time travel, persistence, etc”.

In our case it helps the dev tools hook into Redux. We can see every action that we have dispatched, and even jump in time between these points:

🌌 Time travelling in Redux!

I think that’s a good start!

Now we have our basic app, the tools to debug it with ease, and a stack that saves us from the hell of switch statements and complicated immutable state patterns. I think this is a very solid base to build upon.

Next steps

Thanks for reading!

--

--

Prionto Abdullah

Become a world’s no 1 full-stack web developer. That’s why I am learning and mastering web development. I will not stop until I become the Web Development Hero.