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:
- They should only calculate the new state value based on the
state
andaction
arguments - They are not allowed to modify the existing
state
. Instead, they must make immutable updates, by copying the existingstate
and making changes to the copied values. - 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,
- A Store is created and configured with a root reducer.
- UI components on render for the first time subscribe to the Store.
- When an event happens, and action is dispatched.
- The action is then reduced by the appropriate reducer.
- 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
- Actions are plain objects with a
- 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