Redux is a popular state management library for React applications. It enforces a strict unidirectional data flow and provides a central store for your application’s state.

Redux is a predictable state container for JavaScript apps

Core Concepts

Redux is based on three core principles:

  1. Store: A single, immutable source of truth for your application’s state.
  2. Actions: Plain JavaScript objects that describe a change in the application’s state.
  3. Reducers: Functions that specify how the state changes in response to actions.

Redux API

Redux, for all of its power, has a relatively small API footprint, it is really small and has few things attached to it:

  • ➡️ Apply Middleware
  • ➡️ Compose
  • ➡️ Combine Reducers
  • ➡️ Bind action creator
  • ➡️ Create Store
    • dispatch: ƒ dispatch(action)
    • getState: ƒ getState()
    • replaceReducer: ƒ replaceReducer(nextReducer)
    • subscribe: ƒ subscribe(listener)

Yup, just five functions. We'll discuss each of this in due time. We'll start with one of the simple utility methods that come along with Redux.

A reducer is simply a function that takes in a state, and the things that happened and only one thing comes out of it, a new state

Compose

Functional programing concept!😍

The compose function takes multiple functions as arguments and returns a new function that represents the composition of these functions. In Redux, it’s commonly used to apply multiple store enhancers or middleware to the store creation process.

Info compose() isn't exactly Redux-specific. It's just a helper function. compose takes a series of functions as arguments and returns a new function that applies each those functions from right-to-left

Let’s say that we had a bunch of functions that each take a string and return a modified string.

const makeLouder = (string) => string.toUpperCase()
const repeatThreeTimes = (string) => string.repeat(3)
const embolden = (string) => string.bold()

(Yea, I just learned that some of those methods exist too.)

We could call them all like this:

embolden(repeatThreeTimes(makeLouder("hello")))

But, what if I wanted to pass this combined function around as an argument to another function or method? I’d have to do something like this:

const makeLouderAndBoldAndRepeatThreeTimes = (string) =>
  embolden(repeatThreeTimes(makeLouder(string)))

This is tedious. compose gives us a simple way to compose functions out of other functions.

const makeLouderAndBoldAndRepeatThreeTimes =
redux.compose(embolden,repeatThreeTimes,makeLouder)
You'll see a similiar utility in other libraries like Lodash and Ramda.

This is used as a helper when creating enhancers, which we’ll talk about in the a more advanced workshop. For now, I’m just fulfilling my promises of demystifying the core API.

Create Store

edux is a friendly state management library. In this industry, we tend to call the place where we keep our state the “store.” Redux’s createStore() method will … create … a … store

One does not just create a Redux store, however.

let store = redux.createStore()

This will blow up, but at least we get a helpful error message.

Error: Expected the reducer to be a function.

  • So, there are two things to take away from this:
    • We need to provide an argument that is a reducer and a reducer is a function.
    • A function that takes a reducer and creates you main state returns you functions to interact with your store

Reducer

A reducer is a function where the first argument is the current state of the world and the second is something that happened. Somewhere inside of the function, we figure out what the new state of the world ought to be based on whatever happened.

That's the job of the reducer. It looks at the new thing that happened and it looks at the current state of the world and returns a new state of the world.

Here’s the crazy thing: it’s just a function. It takes two arguments: the thing that just happened and the current (soon to be previous) state of the world. It returns one thing: the new state of the world.

const initialState = { value: 0 }
 
const reducer = (state = initialState, action) => {
  return state
}

An action is just an object. The only requirement is that is has a type property. Sure, something happened, but what type of thing happened?

A Word on Conventions Around Action Types

There are a few different patterns for naming your action types. For a long time, it was convention to use SCREAMING_SNAKE_CASE. As we’ll see in a little bit, we frequently alias our action type names to constants in JavaScript, we we can’t have spaces. Using SCREAMING_SNAKE_CASE also helps separates our constant action type names from other variables in our application.

Updating State Based on Actions

Let’s say an increment action comes in. Well, we should probably increment the value, right?

const initialState = { value: 0 }
 
const reducer = (state = initialState, action) => {
  if (action.type === "INCREMENT") {
    return { value: state.value + 1 }
  }
  return state
}

Alright, there are a few things here. You’ll notice that we’re creating a new object rather than mutating the existing one. This is helpful because it allows anything depending on this state to figure out that we have a new state of the world. We also want to make sure we return the existing state in the event that an action we don’t care about comes through the reducer.

We can make the above a little bit better too!

const initialState = { value: 0 }
 
const INCREMENT = "INCREMENT"
 
const incrementCounter = { type: INCREMENT }
 
const reducer = (state = initialState, action) => {
  if (action.type === INCREMENT) {
    return { value: state.value + 1 }
  }
 
  return state
}

You’ll notice we made a constant called INCREMENT. This is what I was talking about before. The main reason that we took this approach is because we needed to make sure that we didn’t accidentally misspell the action type—either when we created the action or in the reducer. At least now, our code will blow up. This sure beats silently failing.

We’ll also typically use functions to create our actions since they might need more information.

const initialState = { value: 0 }
 
const INCREMENT = "INCREMENT"
const ADD = "ADD"
 
// action creator
const increment = () => ({ type: INCREMENT })
const add = (number) => ({ type: ADD, payload: number })
 
const reducer = (state = initialState, action) => {
  if (action.type === INCREMENT) {
    return { value: state.value + 1 }
  }
 
  if (action.type === ADD) {
    return { value: state.value + action.payload }
  }
 
  return state
}

Calling it a payload is also just a convention. You’ll probably notice that nothing in the last few code samples have literally nothing to do with Redux. It’s all just JavaScript.

We now have an initial state of the world and some logic about how it should change in the event that a limited set of things happen.

const store = redux.createStore(reducer)

Store API

Alright, so we made a store. Redux stores also have a relatively small API surface area. one store for the entire application

  • 💡 It has the following responsibilities
    • ➡️ Holds application state
    • ➡️ Allows access to state via getState()
    • ➡️ Allow sate to be updated via Dispatch(action)
    • ➡️ Register listeners via subscribe(listener )
    • ➡️ Handle unregistering of listeners via the function returned by subscribe(listener)

Redux Store Methods

replaceReducer

Takes one reducer and replace it with another one

The Redux store exposes a replaceReducer function, which replaces the current active root reducer function with a new root reducer function. Calling it will swap the internal reducer function reference, and dispatch an action to help any newly-added slice reducers initialize themselves:

getState()

This simply gets a state

dispatch: ƒ dispatch(action)

Sends actions into the reducer to do mutations to the current state of the application

We made some initial state. We made the outlined some things that could happen to that state. We even defined what should happen to the state when those actions happen.

So, how do we get actions into that reducer to modify the state? Well, we dispatch them.

store.dispatch(action)

Our action creators are functions that return actions, so might see something like this:

store.dispatch(increment())

Once an action is dispatched, the reducer takes care of the rest.

const store = createStore(reducer) // pass in our reducer
 
console.log(store.getState()) // { value: 0 }
store.dispatch(increment())
console.log(store.getState()) // { value: 1 }

The system works. You probably understand most of Redux at this point. Seriously.

subscribe: ƒ subscribe(listener)

Subscribes to the store and when something changes, then do domething!

If we think about this in terms of React, we’re probably not expecting that our components call store.getState() all the time. Rather, we would think that our store would know that its state has changed and pass different props to the components.

That’s a good thought acutally. In the last section, we noticed that the store has a subscribe method. This method takes a function dictates what should happen whenever the state in the store is updated.

subscribe also returns a function that you can call to unsubscribe.

const subscriber = () => console.log('Subscriber!' store.getState().value);
 
const unsubscribe = store.subscribe(subscriber);
 
store.dispatch(increment()); // "Subscriber! 1"
store.dispatch(add(4)); // "Subscriber! 5"
 
unsubscribe();
 
store.dispatch(add(1000)); // (Silence)
  • getState: ƒ getState()
    • get the state of our store
  • replaceReducer: ƒ replaceReducer(nextReducer)
    • Swaps out the reducer

Bind action creator

Let’s get back to Redux itself for a moment. So far, we’ve made functions that create actions (a.k.a. action creators) and we’ve passed them to dispatch.

Well, what if we wanted to wrap that whole process into one nice neat package.

const dispatchIncrement = () => store.dispatch(increment())
const dispatchAdd = (number) => store.dispatch(add(number))
 
dispatchIncrement()
dispatchAdd()

This could be tedious. What else could we do? We could use compose.

const dispatchIncrement = compose(store.dispatch, increment)
const dispatchAdd = compose(store.dispatch, add)

And if we wanted to a whole bunch, we could get fancy.

const [dispatchIncrement, dispatchAdd] = [increment, add].map((fn) => compose(store.dispatch, fn))

But, like, React likes its props in objects and then we’d have to get all fussy with Object.keys or Object.entries. I’m very lazy. We should just have an helper for that. Oh, wait, we do.

const actions = bindActionCreators(
  {
    increment,
    add,
  },
  store.dispatch,
)
 
actions.increment()
 
console.log(store.getState())

There is no rule saying that you have to use bindActionCreators. It’s there to help you.

Binds an action, then returns a new object with the same keys, but each action creator is wrapped in a dispatch call. This makes it easier to invoke the action creators directly, and the resulting functions automatically dispatch the actions.

import { bindActionCreators } from "redux"
import store from "./store" // Assume you have a Redux store
import { increment, decrement } from "./actions" // Assume you have action creators
 
// Manually binding action creators
const manuallyBoundActionCreators = {
  increment: () => store.dispatch(increment()),
  decrement: () => store.dispatch(decrement()),
}
 
// Using bindActionCreators
const boundActionCreators = bindActionCreators({ increment, decrement }, store.dispatch)
 
// Now you can directly call the action creators with the store's dispatch
boundActionCreators.increment()
boundActionCreators.decrement()

Combine Reducers

Let’s stay we have an application where we have some users and we have some tasks. We can assign users and we can assign tasks.

The state might look something like this.

const initialState = {
  users: [
    { id: 1, name: "Steve" },
    { id: 2, name: "Wes" },
  ],
  tasks: [
    { title: "File the TPS reports", assignedTo: 1 },
    { title: "Order more toner for the printer", assignedTo: null },
  ],
}

This is still way smaller than a real application, but it’s still becoming a pain. Let’s look at what the reducer might look like.

The kind of monolithic reducer you would have.

const ADD_USER = "ADD_USER"
const ADD_TASK = "ADD_TASK"
 
const addTask = (title) => ({ type: ADD_TASK, payload: { title } })
const addUser = (name) => ({ type: ADD_USER, payload: { name } })
 
const reducer = (state = initialState, action) => {
  if (action.type === ADD_USER) {
    return {
      ...state,
      users: [...state.users, action.payload],
    }
  }
 
  if (action.type === ADD_TASK) {
    return {
      ...state,
      tasks: [...state.tasks, action.payload],
    }
  }
}
 
const store = createStore(reducer, initialState)
 
store.dispatch(addTask("Record the statistics"))
store.dispatch(addUser("Marc"))
 
console.log(store.getState())

This is already getting out of control. It would be nice if we could split out the idea of managing our users and managing our tasks into two separate reducers and just sew everything back up later.

It turns out, we can! We just need to use combineReducers.

// User Reducer
const users = (state = initialState.users, action) => {
  if (action.type === ADD_USER) {
    return [...state, action.payload]
  }
  return state
}
 
// Task Reducer
const tasks = (state = initialState.tasks, action) => {
  if (action.type === ADD_TASK) {
    return [...state, action.payload]
  }
  return state
}
 
const reducer = redux.combineReducers({ users, tasks })
const store = createStore(reducer, initialState)

Fun Fact!: All actions flow through all of the reducers. So, if you want to update two pieces of state with the same action, you totally can.

Actions

Synchronous Actions

Actions are plain JavaScript objects that describe events or user interactions in your application. They represent what happened and the data associated with the event. An action object typically includes two properties:

  • type:
    • ➡️ A string that describes the type of action. It’s a unique identifier for the action.
  • payload:
    • ➡️ An optional property that carries data related to the action. This can be any valid JavaScript value like an object, string, or number.

Here’s an example of an action object:

const incrementAction = {
  type: "INCREMENT",
  payload: 1,
}

Actions serve as a way to communicate changes in your application, and they are dispatched to the Redux store, where reducers respond to them

Async Actions

Asynchronous API calls to fetch data from an endpoint and use the data in your application

Reducers

specify how the app’s state changes in response to actions sent to the store function that accepts state and action as arguments and returns the next state of the application

(previousState,action) => newState

Hook Redux in react 💗

We’re starting from base principles. So, we’re going to have to do this from the ground up. Where to begin?

Well, an interesting thing to think about is the basic state and the actions that can occur in a given feature—or, an entire application in this case.

We have…

  • ✅ A count.
  • ✅ The ability to increment that count.
  • ✅ The ability to decrement that count.
  • ✅ The ability to set that count to zero.

If we’re being intellectually honest, we could handle this in a bunch of ways.

  • 💡 We could make actions for these three specific use cases (e.g. INCREMENTDECREMENT, and SET).
  • 💡 We could make a super generalized called SET where we set it a given number every time.
  • 💡 We could split the difference and have INCREMENT and DECREMENT and the have an action type that handles the edge cases.

Info A general rule: Your actions should say what happened and not really have any opinions about what that means. Let your reducers figure that all out on your behalf.

I like the first option. So, what would that look like?

We can make a file called actions.js.

export const INCREMENT = "INCREMENT"
export const DECREMENT = "DECREMENT"
export const SET = "SET"
 
// action creator
export const increment = () => ({ type: INCREMENT })
export const decrement = () => ({ type: DECREMENT })
export const set = (value) => ({ type: SET, payload: value })

That seems fine. What would a reducer.js look like?

import { DECREMENT, INCREMENT, SET } from "./actions"
 
export const initialState = { count: 0 }
 
export const reducer = (state = initialState, action) => {
  if (action.type === INCREMENT) {
    return { count: state.count + 1 }
  }
 
  if (action.type === DECREMENT) {
    return { count: state.count - 1 }
  }
 
  if (action.type === SET) {
    return { count: parseInt(action.payload, 10) }
  }
 
  return state
}

Okay, so we’ve looked at Redux and we’re working with some assumption that you’ve seen React before. But, it turns out that there is a bit of a layer in between and it’s a library called React Redux.

Luckily, it’s another fairly small library as well.

The first and arguably most important part—is a component called Provider. This is a component that takes the store and threads it through your React application using the Context API.

First, let’s go ahead and create our store in store.js.

mport { createStore } from "redux";
import { reducer } from "./reducer";
 
export const store = createStore(reducer);

Now, in index.js, we’ll hook it up to React.

import React from 'react';
import ReactDOM from 'react-dom';
 
import { Provider } from 'react-redux'; /* 🌝 */
 
import Application from './Application';
import { store } from './store'; /* 🌝 */
 
import './index.scss';
 
ReactDOM.render(
  <Provider store={store}> {/* 🌝 */}
    <React.StrictMode>
      <Application />
    </React.StrictMode>
  </Provider>, {/* 🌝 */}
  document.getElementById('root')
);

With that—all of your components now have access to the Redux store—from a certain point of view.

Adding the Redux Dev Tools

First and foremost, install the Redux Developer Tools in your browser if you haven’t already.

We’re not totally there yet, we still need to hook our application up to the tools.

The extension adds some properties onto the global window object. One of these is an enhancer that we can use.

import { createStore } from "redux"
import { reducer } from "./reducer"
 
const enhancer = window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
 
export const store = createStore(reducer, enhancer)

We can pass this to our store when we create it in store.js.

Now, we have some insight into the current state of the store and the actions that are firing through the application.

Hooking Up State

Basics of useSelector

useSelector is a hook provided by the React-Redux library. Its primary purpose is to extract data from the Redux store in a React component. As you delve into web development and React in particular, mastering this hook will significantly enhance your state management capabilities. Alright, so we have Redux connected to React in spirit, but the component isn’t really using anything here.

import { useSelector } from "react-redux"
 
const MyComponent = () => {
  const selectedData = useSelector((state) => state.yourReducer.yourData)
  // Your code here
}

Key Features

  • Selector Function

The first argument of useSelector is a selector function. This function takes the entire Redux state as its argument and returns the specific data from the state that the component needs. It allows you to precisely pinpoint the slice of the store that is relevant to your component.

  • Reactivity

One of the powerful aspects of useSelector is its reactivity. When the selected part of the Redux store changes, the component will automatically re-render. This simplifies the process of managing and updating the UI based on changes in the application state.

  • Memoization

Under the hood, useSelector uses a memoization technique to ensure that the selector function is only re-invoked when the relevant parts of the state tree have changed. This optimization enhances performance by preventing unnecessary renders.

Best Practices and Gotchas

  • Keep Selectors Simple

While the selector function can be complex, it’s generally a good practice to keep it as simple as possible. Complex selectors can lead to performance issues, as they might get re-invoked more frequently than necessary.

  • Reselect for Memoized Selectors

For larger applications or scenarios where performance is critical, consider using the Reselect library to create memoized selectors. This can significantly improve the performance of your selectors, especially if they involve complex calculations or transformations.

  • Avoid Inline Arrow Functions

When using useSelector, avoid defining inline arrow functions within the component’s render. This can lead to unnecessary re-renders, impacting performance. Instead, define your selector functions outside the component.

useSelector

We’re going to get more into selectors in a bit, but—at a high level—they do what they say they do on the tin: they select pieces from our state tree.

import { useSelector } from 'react-redux'; /* 🌝 */
 
export const Counter = () => {
  const incident = 'Incident';
  const count = useSelector((state) => state.count); {/* 🌝 */}
 
  return (
    // …
  );
};
 
export default Counter;

It’s zero by default in our state, so our UI hasn’t changed much.

Hooking Up Dispatch

Okay, asking our users to install the developer tools and dispatch actions by hands seems not great. Maybe we should get those buttons working?🤣

In order to do that, we’re going to need them to start dispatching some actions.

const dispatch = useDispatch()

This is store.dispatch from our store.

Let’s import our action creators.

import { decrement, increment, set } from "./actions"

And now we can dispatch those action creators.

<button onClick={() => dispatch(increment())}>Increment</button>
<button onClick={() => dispatch(set(0))}>Reset</button>
<button onClick={() => dispatch(decrement())}>Decrement</button>

Opinion on Using subscribe in React

While subscribe offers a lower-level way to listen to store changes, I would recommend leveraging React-Redux hooks like useSelector and useEffect in most cases. These hooks provide a more declarative and React-friendly approach to handling state changes within components. The use of subscribe can be reserved for specific scenarios where fine-grained control over store changes is necessary. striking the right balance between advanced techniques and best practices will be key to building robust and maintainable React applications.

Binding Actions

We saw in our introduction that we can create functions that implicitly know about or are bound to—dispatch.

At a simple version of this might look as follows:

const actions = bindActionCreators({ increment, decrement, set }, dispatch)

And now we could put them into the component.

<button onClick={() => actions.increment()}>Increment</button>
<button onClick={() => actions.set(0)}>Reset</button>
<button onClick={() => actions.decrement()}>Decrement</button>

You could even create a hook if you wanted to do this on the regular.

In a new file called set-actions.js:

import { bindActionCreators } from "redux"
import { useDispatch } from "react-redux"
import { useMemo } from "react"
 
export function useActions(actions) {
  const dispatch = useDispatch()
  return useMemo(() => {
    return bindActionCreators(actions, dispatch)
  }, [actions, dispatch])
}

And now we can use this whenever.

const actions = useActions({ increment, decrement, set })

Using the Connect API

Redux toolkit