Intro

Redux is a popular 3rd party library AND pattern for state management for React, but is actually framework agnostic, meaning it interacts with any UI framework. It promotes a SingleSourceOfTruth within the app, to avoid isolated silos of state.

Like many of my explorations of new tech and tools, I’m following the official guides (linked above but also in References) to build a starter project to learn about.

What’s it Do?

It manages and updates the global application’s state using a singular data flow. Events triggered by the UI called “Actions” describe the changes that occurred, then special functions called “Reducers” update the state accordingly. This singular data store ensures predictability.

Why Use It?

Having a reliable and global State makes apps predictable, easier to debug, expand, and understand how and when your app’s data changes throughout the run time.

When?

  • If you have LARGE amounts of app state.
  • App state’s updated frequently.
  • The logic to update the state’s complex…
  • When a project’s codebase starts growing and is worked on by a team of people.

I'll be using the Redux Toolkit in this guide but also taking a peek at React-Redux, there are other options depending on the project. But we're only learning here.

How’s it differ from the Context API?

The Context API is a DependencyInjection tool that makes values available to the component tree but it doesn’t handle mutating state across the app.

Concepts

State Management

They start off with a simple counter component.

function Counter() {
  // State: a counter value
  const [counter, setCounter] = useState(0)
 
  // Action: code that causes an update to the state when something happens
  const increment = () => {
    setCounter(prevCounter => prevCounter + 1)
  }
 
  // View: the UI definition
  return (
    <div>
      Value: {counter} <button onClick={increment}>Increment</button>
    </div>
  )
}

Breakdown:

  • The State is the counter variable
  • The Action is increment which is triggered when an event occurs, clicking the button.
  • The View (UI) is the returned JSX. This is all well and dandy when it’s a simple and self-contained component, even if it’s only a few comprising an app, not so much when things grow and need to share state…

Separating the three parts of state management lead to independence between views and states, increasing the maintainability of the project.

Immutability

JavaScript objects and arrays are mutable by default, and this sometimes results in unpredictable behavior.

However, Redux’s Store is immutable for stability’s sake. So to update the Store, we make copies that are updated with the new changes and then those replace the store.

This can be done in vanilla using JavaScript Spread Operator (…) for objects and array methods.

const obj = {
  a: {
    // To safely update obj.a.c, we have to copy each piece
    c: 3
  },
  b: 2
}
 
const obj2 = {
  // copy obj
  ...obj,
  // overwrite a
  a: {
    // copy obj.a
    ...obj.a,
    // overwrite c
    c: 42
  }
}
 
const arr = ['a', 'b']
// Create a new copy of arr, with "c" appended to the end
const arr2 = arr.concat('c')
 
// or, we can make a copy of the original array:
const arr3 = arr.slice()
// and mutate the copy:
arr3.push('c')

Redux Terminology

Actions

A plain ol’ JavaScript object with type and payload fields. Conceptually, Actions are events describing the changes in the app.

  • The type field should be a string that gives this action a descriptive name with the following convention: "domain/eventName". Where the first part is the feature or category that this action belongs to, and the second part is the specific thing that happened.
  • The payload additional information about what happened. Example:
const cookFoodAction = {
	type: "meals/mealCooked",
	payload: "Made Libyan style Couscous with Usbaan."
}

Action Creators

Functions that create (return) an action object. Removes the need to write the action each time…

const cookMeal = () => {
	return {
	type: "meals/mealCooked",
	payload: "Made Libyan style Couscous with Usbaan."
	}
}

Reducer

Reducers are important in the Redux data flow, they’re functions that receive the current state and an action as arguments, and then decide if it’s necessary to update the state. If so, they return the new state Store. Signature: (currState, action) => newState // Kinda like an event listener that behaves based on the action (event) type.

The name, Reducer

They have a segment on why they’re called “Reducer” functions. They’re named after the JS function Array.reduce() which is similar to fold left/right in FunctionalProgramming. Read https://redux.js.org/tutorials/essentials/part-1-overview-concepts#reducers

Rules

Reducer functions must follow a set of rules:

  1. They should only calculate the new state value based on the state and action arguments
  2. They are not allowed to modify the existing state. Instead, they must make immutable updates, by copying the existing state and making changes to the copied values.
  3. They must be “pure” - they cannot do any asynchronous logic, calculate random values, or cause other “side effects”

Process

The logic often follows the same steps.

  • Check if this action is relevant to this reducer function.
    • If yes, copy state, update the copy, and return it.
  • If not relevant, then return the current state as is. Example:
const initialState = { value: 0 }
 
function counterReducer(state = initialState, action) {
  // Check to see if the reducer cares about this action
  if (action.type === 'counter/increment') {
    // If so, make a copy of `state`
    return {
      ...state,// the spread operator
      // and update the copy with the new value
      value: state.value + 1
    }
  }
  // otherwise return the existing state unchanged
  return state
}

Store

This is it, this is where Redux stores the State, in the…Store. It’s basically a JSON object that’s configured with some methods. The store is created by passing in a reducer, and has a method called getState that returns the current state value:

import { configureStore } from '@reduxjs/toolkit'
 
const store = configureStore({ reducer: counterReducer })
 
console.log(store.getState())
// {value: 0}```

Dispatch

A method that’s the only way to update the state. By calling store.dispatch() and passing an action object. The store runs its reducer and saves the new state value. To retrieve the updated value, call getState().

store.dispatch({ type: 'counter/increment' });
console.log(store.getState());
// {value: 1}

Dispatch and Reduce?

Dispatching actions is like triggering events, whereas the reducers act like event-listeners. The flow’s something like: Dispatch action -> Reduce Action -> Handle action and update state if relevant.

Action creators are often called to dispatch the right action:

const increment = () => {
  return {
    type: 'counter/increment'
  }
}
store.dispatch(increment())
console.log(store.getState())
// {value: 2}

Selectors

Functions that extract specific info from the Store state, similar to getter methods in OOP. Great for avoiding repetitive code in large projects.

Data Flow

If you want to read the whole flow in detail, by all means, here. But basically,

  1. A Store is created and configured with a root reducer.
  2. UI components on render for the first time subscribe to the Store.
  3. When an event happens, and action is dispatched.
  4. The action is then reduced by the appropriate reducer.
  5. The Store is updated with a new copy with the latest changes. Visual representation:

// this animated illustration also shows where everything lives...

Summary

  • Redux is a library for managing global application state
    • Redux is typically used with the React-Redux library for integrating Redux and React together
    • Redux Toolkit is the standard way to write Redux logic
  • Redux’s update pattern separates “what happened” from “how the state changes”
    • Actions are plain objects with a type field, and describe “what happened” in the app
    • Reducers are functions that calculate a new state value based on previous state + an action
    • A Redux store runs the root reducer whenever an action is dispatched
  • Redux uses a “one-way data flow” app structure
    • State describes the condition of the app at a point in time, and UI renders based on that state
    • When something happens in the app:
      • The UI dispatches an action
      • The store runs the reducers, and the state is updated based on what occurred
      • The store notifies the UI that the state has changed
    • The UI re-renders based on the new state

References