Concepts

In a React / React Native app, it will generally having 3 parts:

  • The state, the source of truth that drives our app;
  • The view, a declarative description of the UI based on the current state;
  • The actions, the events that occur in the app based on user input, and trigger updates in the state;

image-20240129165229577

This build up a “one-way data flow”:

  1. State describes the condition of the app at specific point in time
  2. The UI is rendered based on that state
  3. When something happens (such as a user clicking a button), the state is updated based on what occurred
  4. The UI re-renders based on the new state

Each time like in step 3 that trigger the actions, it starts a new cycle-life.

However, as project getting large, the simplicity can break down when we have multiple components that need to share and use the same state, especially if those components are located in different parts of the application.

One way to solve this is to extract the shared state from the components, and put it into a centralized location outside the component tree. With this, our component tree becomes a big “view”, and any component can access the state or trigger actions, no matter where they are in the tree!

Restate — the story of Redux Tree | by Anton Korzunov | HackerNoon.com |  Medium

This is the basic idea behind Redux: a single centralized place to contain the global state in your application, and specific patterns to follow when updating that state to make the code predictable.

Basics

Immutability

“Mutable” means “changeable”. If something is “immutable”, it can never be changed.

All contents in Redux should be immutable. In order to update values immutably, your code must make copies of existing objects/arrays, and then modify the copies.

1
2
3
4
5
6
7
8
// Mutable
const obj = {a: 1, b: 2};
/* still the same object outside,
but the contents have changed */
obj.b = 3

// Inmmutable
const newobj = { ...obj, b: 3 };

Why we need “Immutability” ?

In general, specifically in React, a mutable state will run into bugs where your React components don’t re-render since React can quickly determine if a component needs to re-render by shallowly comparing the old state and props with the new ones.

Here is a code example with mutation:

We’ll start with this person object here

1
2
3
4
5
6
7
8
9
let person = {
firstName: "Bob",
lastName: "Loblaw",
address: {
street: "123 Fake St",
city: "Emberton",
state: "NJ"
}
}

Then let’s say we write a function that gives a person special powers:

1
2
3
4
function giveAwesomePowers(person) {
person.specialPower = "invisibility";
return person;
}

Ok so everyone gets the same power. Whatever, invisibility is great. Let’s give some special powers to Mr. Loblaw now.

1
2
3
4
5
6
7
8
9
10
11
12
// Initially, Bob has no powers :(
console.log(person);

// Then we call our function...
let samePerson = giveAwesomePowers(person);

// Now Bob has powers!
console.log(person);
console.log(samePerson);

// He's the same person in every other respect, though.
console.log('Are they the same?', person === samePerson); // true

The object returned from giveAwesomePowers is the same object as the one that was passed in, but its insides have been messed with. Its properties have changed. It has been mutated.

The internals of the object have changed, but the object reference has not. It’s the same object on the outside (which is why an equality check like person === samePerson will be true).

Terminology

There are some important Redux terms that you’ll need to be familiar with before we continue:

Actions

An action is a plain JavaScript object that has a type field. You can think of an action as an event that describes something that happened in the application.

The type field should be a string that gives this action a descriptive name. It usually write that type string like "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.

An action object can have other fields with additional information about what happened. This could be a data that you want to modify to, or a string, etc. By convention, we put that information in a field called payload.

A typical action object should look like this:

1
2
3
4
const addTodoAction = {
type: 'todos/todoAdded',
payload: 'Buy milk'
}

Action Creators

An action creator is a function that creates and returns an action object. We typically use these so we don’t have to write the action object by hand every time:

1
2
3
4
5
6
7
const addTodo = text => {
// returns an action object
return {
type: 'todos/todoAdded',
payload: text // values to update
}
}

Reducers

A reducer is a function that receives the current state and an action object, decides how to update the state if necessary, and returns the new state. You can think of a reducer as an event listener which handles events based on the received action (event) type.

1
(state, action) => newState // reducer I/O

Reducers must always follow some specific rules:

  • They should only calculate the new state value based on the state and action arguments
  • 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.
  • They must not do any asynchronous logic, calculate random values, or cause other “side effects”

The logic inside reducer functions typically follows the same series of steps:

  • Check to see if the reducer cares about this action
    • If so, make a copy of the state, update the copy with new values, and return it
  • Otherwise, return the existing state unchanged

Here’s a small example of a reducer, showing the steps that each reducer should follow:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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,
// and update the copy with the new value
value: state.value + 1
}
}
// otherwise return the existing state unchanged
return state
}

Why are they called “Reducers” ?

“Reducer” functions get their name because they’re similar to the kind of callback function you pass to the Array.reduce() method.

The Array.reduce() method lets you take an array of values, process each item in the array one at a time, and return a single final result. You can think of it as “reducing the array down to one value”.

Array.reduce() takes a callback function as an argument, which will be called one time for each item in the array. It takes two arguments:

  • previousResult, the value that your callback returned last time
  • currentItem, the current item in the array

If we wanted to add together an array of numbers to find out what the total is, we could write a reduce callback that looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const numbers = [2, 5, 8]

const addNumbers = (previousResult, currentItem) => {
console.log({ previousResult, currentItem })
return previousResult + currentItem
}

const initialValue = 0

const total = numbers.reduce(addNumbers, initialValue)
// {previousResult: 0, currentItem: 2}
// {previousResult: 2, currentItem: 5}
// {previousResult: 7, currentItem: 8}

console.log(total)
// 15

A Redux reducer function is exactly the same idea as this “reduce callback” function! It takes a “previous result” (the state), and the “current item” (the action object), decides a new state value based on those arguments, and returns that new state.

1
2
3
4
5
6
7
8
9
10
11
const actions = [
{ type: 'counter/increment' },
{ type: 'counter/increment' },
{ type: 'counter/increment' }
]

const initialState = { value: 0 }

const finalResult = actions.reduce(counterReducer, initialState)
console.log(finalResult)
// {value: 3}

We can say that Redux reducers reduce a set of actions (over time) into a single state. The difference is that with Array.reduce() it happens all at once, and with Redux, it happens over the lifetime of your running app.

Store

The current Redux application state lives in an object called the store .

The store is created by passing in a reducer, and has a method called getState that returns the current state value:

1
2
3
4
5
6
import { configureStore } from '@reduxjs/toolkit'

const store = configureStore({ reducer: counterReducer })

console.log(store.getState())
// {value: 0}

Dispatch

The Redux store has a method called dispatch. The only way to update the state is to call store.dispatch() and pass in an action object. The store will run its reducer function and save the new state value inside, and we can call getState() to retrieve the updated value:

1
2
3
4
store.dispatch({ type: 'counter/increment' })

console.log(store.getState())
// {value: 1}

You can think of dispatching actions as “triggering an event” in the application. Something happened, and we want the store to know about it. Reducers act like event listeners, and when they hear an action they are interested in, they update the state in response.

We typically call action creators to dispatch the right action:

1
2
3
4
5
6
7
8
9
10
const increment = () => {
return {
type: 'counter/increment'
}
}

store.dispatch(increment())

console.log(store.getState())
// {value: 2}

Selectors

Selectors are functions that know how to extract specific pieces of information from a store state value. As an application grows bigger, this can help avoid repeating logic as different parts of the app need to read the same data:

1
2
3
4
5
const selectCounterValue = state => state.value

const currentValue = selectCounterValue(store.getState())
console.log(currentValue)
// 2

Redux Application Data Flow

Earlier, we talked about “one-way data flow” in concept section, which describes this sequence of steps to update the app:

  1. State describes the condition of the app at a specific point in time
  2. The UI is rendered based on that state
  3. When something happens (such as a user clicking a button), the state is updated based on what occurred
  4. The UI re-renders based on the new state

For Redux specifically, we can break these steps into more detail:

  • Initial setup:
    • A Redux store is created using a root reducer function
    • The store calls the root reducer once, and saves the return value as its initial state
    • When the UI is first rendered, UI components access the current state of the Redux store, and use that data to decide what to render. They also subscribe to any future store updates so they can know if the state has changed.
  • Updates:
    • Something happens in the app, such as a user clicking a button
    • The app code dispatches an action to the Redux store, like dispatch({type: 'counter/increment'})
    • The store runs the reducer function again with the previous state and the current action, and saves the return value as the new state
    • The store notifies all parts of the UI that are subscribed that the store has been updated
    • Each UI component that needs data from the store checks to see if the parts of the state they need have changed.
    • Each component that sees its data has changed forces a re-render with the new data, so it can update what’s shown on the screen

Here’s what that data flow looks like visually:

  1. When user want to deposit $10, the Deposit $10 button will be pressed, which triggers a event:

image-20240131113133755

  1. Event handler receives the event, process them, and send a action object using store.dispatch to store.

image-20240131113847470

  1. Store receives the action object, it will change the state with [state, action] as the input on reducer.

image-20240131114430186

  1. Once state been updated, UI will be re-rendered:

image-20240131114556031

Notice that in Dispatch section, it saids “You can think of dispatching actions as “triggering an event” in the application.” This is only in redux scope, where as in step1, the event is triggered by UI and apply to event handler which let app knows user made a change.

Redux Toolkit App Structure

Here are the key files that make up this application:

  • /src
    • index.js: the starting point for the app
    • App.js: the top-level React component
    • /app
      • store.js: creates the Redux store instance
    • /features
      • /counter
        • Counter.js: a React component that shows the UI for the counter feature
        • counterSlice.js: the Redux logic for the counter feature

Let’s start by looking at how the Redux store is created.

Redux Store

References

  1. Redux Essentials, Part 1: Redux Overview and Concepts | Redux
  2. Restate — the story of Redux Tree | by Anton Korzunov | HackerNoon.com | Medium
  3. Immutability in React and Redux: The Complete Guide (daveceddia.com)