What You'll Learn
Welcome to the Redux Essentials tutorial! This tutorial will introduce you to Redux and teach you how to use it the right way, using our latest recommended tools and best practices. By the time you finish, you should be able to start building your own Redux applications using the tools and patterns you've learned here.
In Part 1 of this tutorial, we'll cover the key concepts and terms you need to know to use Redux, and in Part 2: Redux App Structure we'll examine a typical React + Redux app to see how the pieces fit together.
Starting in Part 3: Basic Redux Data Flow, we'll use that knowledge to build a small social media feed app with some real-world features, see how those pieces actually work in practice, and talk about some important patterns and guidelines for using Redux.
How to Read This TutorialThis tutorial focuses on showing you how to use Redux the right way, and explains concepts along the way so that you can understand how to build Redux apps correctly.
We've tried to keep these explanations beginner-friendly, but we do need to make some assumptions about what you know already:
If you're not already comfortable with those topics, we encourage you to take some time to become comfortable with them first, and then come back to learn about Redux. We'll be here when you're ready!
You should also make sure that you have the React and Redux DevTools extensions installed in your browser:
It helps to understand what this "Redux" thing is in the first place. What does it do? What problems does it help me solve? Why would I want to use it?
Redux is a pattern and library for managing and updating global application state, where the UI triggers events called "actions" to describe what happened, and separate update logic called "reducers" updates the state in response. It serves as a centralized store for state that needs to be used across your entire application, with rules ensuring that the state can only be updated in a predictable fashion.
Why Should I Use Redux?Redux helps you manage "global" state - state that is needed across many parts of your application.
The patterns and tools provided by Redux make it easier to understand when, where, why, and how the state in your application is being updated, and how your application logic will behave when those changes occur. Redux guides you towards writing code that is predictable and testable, which helps give you confidence that your application will work as expected.
When Should I Use Redux?Redux helps you deal with shared state management, but like any tool, it has tradeoffs. There are more concepts to learn, and more code to write. It also adds some indirection to your code, and asks you to follow certain restrictions. It's a trade-off between short term and long term productivity.
Redux is more useful when:
Not all apps need Redux. Take some time to think about the kind of app you're building, and decide what tools would be best to help solve the problems you're working on.
Redux Libraries and ToolsRedux at its core is a small standalone JS library. It is commonly used with several other packages:
Redux ToolkitRedux Toolkit is our recommended approach for writing Redux logic. It contains packages and functions that we think are essential for building a Redux app. Redux Toolkit builds in our suggested best practices, simplifies most Redux tasks, prevents common mistakes, and makes it easier to write Redux applications.
React-ReduxRedux can integrate with any UI framework, and is most frequently used with React. React-Redux is our official package that lets your React components interact with a Redux store by reading pieces of state and dispatching actions to update the store.
Redux DevTools ExtensionThe Redux DevTools Extension shows a history of the changes to the state in your Redux store over time. This allows you to debug your applications effectively, including using powerful techniques like "time-travel debugging".
Redux Terms and ConceptsBefore we dive into some actual code, let's talk about some of the terms and concepts you'll need to know to use Redux.
State ManagementLet's start by looking at a small React counter component. It tracks a number in component state, and increments the number when a button is clicked:
function Counter() {
const [counter, setCounter] = useState(0)
const increment = () => {
setCounter(prevCounter => prevCounter + 1)
}
return (
<div>
Value: {counter} <button onClick={increment}>Increment</button>
</div>
)
}
It is a self-contained app with the following parts:
This is a small example of "one-way data flow":
However, 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. Sometimes this can be solved by "lifting state up" to parent components, but that doesn't always help.
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!
By defining and separating the concepts involved in state management and enforcing rules that maintain independence between views and states, we give our code more structure and maintainability.
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.
Immutability"Mutable" means "changeable". If something is "immutable", it can never be changed.
JavaScript objects and arrays are all mutable by default. If I create an object, I can change the contents of its fields. If I create an array, I can change the contents as well:
const obj = { a: 1, b: 2 }
obj.b = 3
const arr = ['a', 'b']
arr.push('c')
arr[1] = 'd'
This is called mutating the object or array. It's the same object or array reference in memory, but now the contents inside the object have changed.
In order to update values immutably, your code must make copies of existing objects/arrays, and then modify the copies.
We can do this by hand using JavaScript's array / object spread operators, as well as array methods that return new copies of the array instead of mutating the original array:
const obj = {
a: {
c: 3
},
b: 2
}
const obj2 = {
...obj,
a: {
...obj.a,
c: 42
}
}
const arr = ['a', 'b']
const arr2 = arr.concat('c')
const arr3 = arr.slice()
arr3.push('c')
React and Redux expect that all state updates are done immutably. We'll look at where and how this is important a bit later, as well as some easier ways to write immutable update logic.
TerminologyThere are some important Redux terms that you'll need to be familiar with before we continue:
ActionsAn 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, like "todos/todoAdded"
. We 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. By convention, we put that information in a field called payload
.
A typical action object might look like this:
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:
const addTodo = text => {
return {
type: 'todos/todoAdded',
payload: text
}
}
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: (state, action) => newState
. You can think of a reducer as an event listener which handles events based on the received action (event) type.
info
"Reducer" functions get their name because they're similar to the kind of callback function you pass to the Array.reduce()
method.
Reducers must always follow some specific rules:
state
and action
argumentsstate
. Instead, they must make immutable updates, by copying the existing state
and making changes to the copied values.We'll talk more about the rules of reducers later, including why they're important and how to follow them correctly.
The logic inside reducer functions typically follows the same series of steps:
Here's a small example of a reducer, showing the steps that each reducer should follow:
const initialState = { value: 0 }
function counterReducer(state = initialState, action) {
if (action.type === 'counter/increment') {
return {
...state,
value: state.value + 1
}
}
return state
}
Reducers can use any kind of logic inside to decide what the new state should be: if/else
, switch
, loops, and so on.
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 timecurrentItem
, the current item in the arrayThe first time that the callback runs, there isn't a previousResult
available, so we need to also pass in an initial value that will be used as the first previousResult
.
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:
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)
console.log(total)
Notice that this addNumbers
"reduce callback" function doesn't need to keep track of anything itself. It takes the previousResult
and currentItem
arguments, does something with them, and returns a new result value.
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.
If we were to create an array of Redux actions, call reduce()
, and pass in a reducer function, we'd get a final result the same way:
const actions = [
{ type: 'counter/increment' },
{ type: 'counter/increment' },
{ type: 'counter/increment' }
]
const initialState = { value: 0 }
const finalResult = actions.reduce(counterReducer, initialState)
console.log(finalResult)
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.
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:
import { configureStore } from '@reduxjs/toolkit'
const store = configureStore({ reducer: counterReducer })
console.log(store.getState())
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:
store.dispatch({ type: 'counter/increment' })
console.log(store.getState())
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:
const increment = () => {
return {
type: 'counter/increment'
}
}
store.dispatch(increment())
console.log(store.getState())
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:
const selectCounterValue = state => state.value
const currentValue = selectCounterValue(store.getState())
console.log(currentValue)
Redux Application Data Flow
Earlier, we talked about "one-way data flow", which describes this sequence of steps to update the app:
For Redux specifically, we can break these steps into more detail:
state
dispatch({type: 'counter/increment'})
state
and the current action
, and saves the return value as the new state
Here's what that data flow looks like visually:
What You've LearnedRedux does have a number of new terms and concepts to remember. As a reminder, here's what we just covered:
Summary
type
field, and describe "what happened" in the appWe've seen each of the individual pieces of a Redux app. Next, continue on to Part 2: Redux Toolkit App Structure, where we'll look at a full working example to see how the pieces fit together.
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4