Last post I described how Redux works in theory, it's now time to explain how to use Redux in your application. It is worthwhile to mention that the majority of developers don't use the Redux library by itself, they use a library called Redux Toolkit, made by the Redux maintainers, which makes Redux development and debugging easier by introducing a few convenience functions. But first I will tell you how things are done in plain old Redux, and then we'll see how Redux Toolkit makes it easier to do those things.
Redux's codebase is just 2KB large, and a Redux store contains three different methods for managing state: dispatch()
, subscribe()
and getState()
. I will cover all of these methods in due time. Yes, they are methods, of a special state object returned by a fourth function called createStore()
.
Installing
You have three options: To install just Redux by itself, you run npm install redux
or yarn add redux
(someday I have to make a guide about yarn). The recommended way however is to install Redux Toolkit which includes Redux along with some other goodies, using npm install @reduxjs/toolkit
or yarn add @reduxjs/toolkit
.
Your third option is to create a React app with the Redux template, which is useful if you trying to make a React project that integrates with Redux.
# Using npm...
npm install -g create-react-app
create-react-app my-app --template redux
# ...or npx
npx create-react-app my-app --template redux
Actions
This is perhaps the easiest part to learn, as an action is an object with type
and optinally payload
fields. type
is simply a descriptive string with a name you give it. Any string is a valid action. Examples of actions are "INCREMENT"
and "counter/increment"
. payload
can be any user-defined data that you would like to pass to a reducer. The payload is useful if you want to pass a parameter alongside an action such as "add 5". Instead of making a reducer that adds 5 to a value, you can make one that adds an abstract amount to the value and have that amount specified by payload
. Keep in mind, the payload
is allowed to be any javascript object. It is not limited to numbers.
Reducers
A reducer is a function that takes the current state and an action as an argument, and returns an updated state. It must not mutate the current state. It is supposed to create a new state object with modified values.
In this example (using plain Redux, not Redux Toolkit) the state is just a number, Notice how the state
argument has a default value. That value is used as the initial state when the app begins.
function counter(state = 0, action) {
switch (action.type) {
// The strings can be arbitrary names
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
// If it is another action then this reducer is not interested in it
return state
}
}
But most likely your entire app state won't consist of a single number, so here's an example that uses an object state.
let stats = {
likes: 0,
comments: []
}
function socialMediaPost(state=stats, action) {
switch (action.type) {
// The strings can be arbitrary names
case 'socialMediaApp/likePost':
return {
...state,
likes: state.likes + 1
}
case 'socialMediaApp/commentPost':
return {
...state,
// Very important, use concat() because it's immutable and not push()
comments: state.comments.concat(action.payload.comment)
}
default:
// If it is another action then this reducer is not interested in it
return state
}
}
Look at the structure of the second reducer. We have a default
clause which ignores other actions this reducer should not handle. That's possible because there can be several reducers in a Redux app, each updating different state. You could have one reducer just for social media post content and another for adding/deleting/loading posts themselves.
Redux API
createStore()
The createStore(reducer)
function is used to create a Redux store. It takes a single argument that is the reducer function it should call when state is updated. How you update the state I will describe later; for now it is important to know that you must call this function at the beginning of your app to have a store, and usually there is only one Redux store in your whole app.
We are not limited to using only one reducer in the entire app. We can make the reducer call child functions which also act like reducers but for their own subset of state that you, the developer, choose for it to manage. Similar to this prototype:
//Defined in other files
export function loginReducer(state, action) { /* ... */ }
// ...another file
export function reportAbuseReducer(state, action) { /* ... */ }
// ...yet another file
export function mainContentReducer(state, action) { /* ... */ }
// mainReducer file
// Assuming you are using Typescript
import {loginReducer} from "path/to/loginReducer";
import {reportAbuseReducer} from "path/to/reportAbuseReducer";
import {mainContentReducer} from "path/to/mainContentReducer";
function mainReducer(state=someDefaultState, action) {
switch (action.payload.featureSubsystem) {
case "loginSubsystem":
return loginReducer(state,action)
case "reportAbuseSubsystem":
return reportAbuseReducer(state,action)
case "mainContentSubsystem":
return mainContentReducer(state,action)
// handle other features and subsystems appropriately
// ...
default:
// undefined action passed to reducer, signal an error somehow
}
}
let appState = createStore(mainReducer)
Technically the schema and function prototypes of the child reducers is up to you, since you are the one calling them, but for ease of use I would give it the same prototype as the main reducer, (state, action)
. You shouldn't need to make a default value since you always pass the state as an argument.
appState
is our Redux store, and has dispatch()
, subscribe()
and getState()
methods.
dispatch()
This method updates the state object, it is the only way to update it. You pass an action object to it like some of the examples above, such as stateObject.dispatch("INCREMENT")
.
Notice if you keep typing action names, eventually you will spell one wrong, and the error won't be noticed until runtime. So instead of specifying actions directly, you typically write an action creator function to return a single action. A code snippet speaks a hundred words so let's look at how this would work in practice:
const Increment = () => {
return { // This is an action object.
type: "INCREMENT"
}
}
Now instead of writing the "INCREMENT"
action, you can call the Increment()
action creator to return an action of type "INCREMENT"
. Action creators are functions you write and manage yourself (at least in plain old Redux, Redux Toolkit can generate action creators for you).
subscribe()
This method lets you pass a function that is called every time the state is changed with dispatch()
. Think of it as a way to put callbacks after dispatch()
calls. The callback function does not take any parameters.
// Example `subscribe()` call
store.subscribe(() => console.log(store.getState()))
subscribe()
is used by web frameworks such as React to update their components after the Redux state has changed. Having said that, most developers don't call this function directly for that purpose, they use the React-Redux library which provides a bridge between Redux state changes and React component updates, so React-Redux ends up being the one calling subscribe()
.
getState()
This returns a copy of the state object. Modifying this won't change the Redux state and so you should not do it. No parameters are passed to this function.
And we're done
If you see any errors in this post, please let me know so I can fix them.
Top comments (0)