Redux Action Being Dispatched Over and Over Again
Redux Fundamentals, Part 2: Concepts and Data Flow
What Yous'll Acquire
- Key terms and concepts for using Redux
- How data flows through a Redux app
Introduction
In Part ane: Redux Overview, nosotros talked about what Redux is, why yous might want to use it, and listed the other Redux libraries that are typically used with the Redux core. We too saw a modest example of what a working Redux app looks like and the pieces that make up the app. Finally, nosotros briefly mentioned some of the terms and concepts used with Redux.
In this department, we'll wait at those terms and concepts in more detail, and talk more than nigh how information flows through a Redux awarding.
Background Concepts
Before we dive into some actual code, let'due south talk about some of the terms and concepts you'll need to know to use Redux.
Country Direction
Permit's beginning by looking at a small React counter component. It tracks a number in component country, and increments the number when a push is clicked:
role Counter ( ) {
// Country: 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
render (
< div >
Value: { counter } < button onClick = { increment } > Increase </ button >
</ div >
)
}
It is a self-contained app with the post-obit parts:
- The country, the source of truth that drives our app;
- The view, a declarative description of the UI based on the current country
- The actions, the events that occur in the app based on user input, and trigger updates in the state
This is a small example of "ane-way data flow":
- State describes the condition of the app at a specific bespeak in fourth dimension
- The UI is rendered based on that state
- When something happens (such as a user clicking a button), the country is updated based on what occurred
- The UI re-renders based on the new state
Even so, the simplicity can break down when we take multiple components that demand to share and use the aforementioned state, especially if those components are located in unlike parts of the application. Sometimes this can be solved by "lifting land up" to parent components, but that doesn't e'er assist.
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 whatever component can admission the land or trigger deportment, no matter where they are in the tree!
By defining and separating the concepts involved in country management and enforcing rules that maintain independence between views and states, nosotros give our code more than construction and maintainability.
This is the bones idea behind Redux: a unmarried centralized place to contain the global state in your application, and specific patterns to follow when updating that land to make the code predictable.
Immutability
"Mutable" means "changeable". If something is "immutable", information technology can never exist changed.
JavaScript objects and arrays are all mutable by default. If I create an object, I tin change the contents of its fields. If I create an array, I can change the contents too:
const obj = { a : 1 , b : 2 }
// however the same object outside, but the contents have inverse
obj . b = 3
const arr = [ 'a' , 'b' ]
// In the same way, nosotros can modify the contents of this array
arr . button ( 'c' )
arr [ 1 ] = 'd'
This is called mutating the object or array. It'due south the aforementioned object or array reference in retentivity, but now the contents within the object have changed.
In order to update values immutably, your code must make copies of existing objects/arrays, so change the copies.
We can do this by hand using JavaScript'southward assortment / object spread operators, as well equally array methods that return new copies of the array instead of mutating the original array:
const obj = {
a : {
// To safely update obj.a.c, nosotros have to copy each slice
c : iii
} ,
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 finish
const arr2 = arr . concat ( 'c' )
// or, we tin can make a copy of the original assortment:
const arr3 = arr . slice ( )
// and mutate the copy:
arr3 . push ( 'c' )
Redux expects that all state updates are done immutably. We'll await at where and how this is important a bit later, besides as some easier ways to write immutable update logic.
Want to Know More?
Redux Terminology
There's some important Redux terms that you lot'll need to exist familiar with before we continue:
Actions
An action is a plain JavaScript object that has a blazon field. You lot can think of an activity as an event that describes something that happened in the application.
The blazon field should exist a cord that gives this activity a descriptive name, similar "todos/todoAdded". We normally write that blazon cord similar "domain/eventName", where the first part is the characteristic or category that this activeness belongs to, and the 2nd function is the specific affair that happened.
An activity object can have other fields with additional information about what happened. By convention, nosotros put that information in a field called payload.
A typical activeness object might look like this:
const addTodoAction = {
type : 'todos/todoAdded' ,
payload : 'Buy milk'
}
Reducers
A reducer is a role that receives the current country and an activity object, decides how to update the state if necessary, and returns the new land: (country, action) => newState. You can think of a reducer as an event listener which handles events based on the received action (effect) blazon.
info
"Reducer" functions get their name because they're similar to the kind of callback function you lot pass to the Array.reduce() method.
Reducers must always follow some specific rules:
- They should only calculate the new state value based on the
landandactionarguments - They are not allowed to change the existing
state. Instead, they must make immutable updates, by copying the existinglandand making changes to the copied values. - They must non practise any asynchronous logic, calculate random values, or cause other "side effects"
We'll talk more nigh the rules of reducers afterward, including why they're important and how to follow them correctly.
The logic inside reducer functions typically follows the same serial of steps:
- Check to see if the reducer cares about this action
- If so, make a re-create of the state, update the copy with new values, and return information technology
- Otherwise, render the existing state unchanged
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 ) {
// Check to see if the reducer cares near this action
if ( activity . type === 'counter/incremented' ) {
// If so, brand a re-create of `state`
return {
... country ,
// and update the copy with the new value
value : state . value + 1
}
}
// otherwise return the existing state unchanged
return state
}
Reducers can use whatever kind of logic inside to determine what the new state should be: if/else, switch, loops, and so on.
Detailed Caption: Why Are They Called 'Reducers?'
The Array.reduce() method lets you take an array of values, process each item in the assortment one at a fourth dimension, and return a unmarried final outcome. You tin retrieve of it every bit "reducing the assortment downward to one value".
Array.reduce() takes a callback function as an argument, which will be chosen 1 time for each item in the array. It takes two arguments:
-
previousResult, the value that your callback returned last time -
currentItem, the electric current item in the assortment
The first time that the callback runs, there isn't a previousResult available, and so we need to likewise pass in an initial value that will be used as the first previousResult.
If we wanted to add together together an array of numbers to find out what the total is, nosotros could write a reduce callback that looks like this:
const numbers = [ ii , 5 , eight ]
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: v}
// {previousResult: 7, currentItem: viii}
panel . log ( full )
// 15
Notice that this addNumbers "reduce callback" function doesn't need to go along rails of anything itself. It takes the previousResult and currentItem arguments, does something with them, and returns a new outcome value.
A Redux reducer office is exactly the same idea as this "reduce callback" part! It takes a "previous result" (the state), and the "current item" (the activeness object), decides a new state value based on those arguments, and returns that new state.
If we were to create an array of Redux deportment, call reduce(), and pass in a reducer function, we'd get a final result the same way:
const actions = [
{ type : 'counter/incremented' } ,
{ type : 'counter/incremented' } ,
{ blazon : 'counter/incremented' }
]
const initialState = { value : 0 }
const finalResult = actions . reduce ( counterReducer , initialState )
console . log ( finalResult )
// {value: iii}
Nosotros can say that Redux reducers reduce a set of actions (over time) into a unmarried country. The departure is that with Array.reduce() it happens all at once, and with Redux, information technology 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 past 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 ( shop . getState ( ) )
// {value: 0}
Dispatch
The Redux store has a method called dispatch. The but way to update the state is to call store.dispatch() and pass in an action object. The store volition run its reducer function and relieve the new state value inside, and we can call getState() to retrieve the updated value:
store . dispatch ( { type : 'counter/incremented' } )
console . log ( store . getState ( ) )
// {value: one}
You can think of dispatching actions as "triggering an event" in the application. Something happened, and we desire the shop 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.
Selectors
Selectors are functions that know how to extract specific pieces of information from a shop state value. As an application grows bigger, this can aid avoid repeating logic as different parts of the app need to read the aforementioned data:
const selectCounterValue = state => land . value
const currentValue = selectCounterValue ( store . getState ( ) )
console . log ( currentValue )
// 2
Core Concepts and Principles
Overall, we tin summarize the intent behind Redux's design in three core concepts:
Single Source of Truth
The global state of your application is stored as an object inside a single store. Any given slice of information should just exist in 1 location, rather than existence duplicated in many places.
This makes it easier to debug and inspect your app'southward state every bit things change, as well every bit centralizing logic that needs to interact with the entire awarding.
tip
This does not mean that every piece of state in your app must go into the Redux store! You lot should determine whether a piece of state belongs in Redux or your UI components, based on where information technology's needed.
Land is Read-But
The only style to change the state is to acceleration an action, an object that describes what happened.
This way, the UI won't accidentally overwrite data, and information technology'due south easier to trace why a state update happened. Since actions are plain JS objects, they can be logged, serialized, stored, and subsequently replayed for debugging or testing purposes.
Changes are Made with Pure Reducer Functions
To specify how the state tree is updated based on actions, you write reducer functions. Reducers are pure functions that take the previous country and an action, and return the side by side land. Like any other functions, yous tin split reducers into smaller functions to help do the piece of work, or write reusable reducers for common tasks.
Redux Awarding Data Period
Earlier, we talked almost "one-mode data flow", which describes this sequence of steps to update the app:
- State describes the status of the app at a specific point in fourth dimension
- The UI is rendered based on that state
- When something happens (such every bit a user clicking a push), the state is updated based on what occurred
- The UI re-renders based on the new land
For Redux specifically, we can break these steps into more detail:
- Initial setup:
- A Redux shop is created using a root reducer function
- The store calls the root reducer in one case, and saves the return value as its initial
state - When the UI is first rendered, UI components access the electric current state of the Redux store, and apply that data to decide what to render. They also subscribe to any future store updates so they tin can know if the land has changed.
- Updates:
- Something happens in the app, such as a user clicking a button
- The app code dispatches an action to the Redux shop, like
dispatch({blazon: 'counter/incremented'}) - The store runs the reducer function again with the previous
landand the electric currentaction, and saves the render value as the newstate - 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 run across if the parts of the state they demand have inverse.
- Each component that sees its data has inverse forces a re-render with the new data, so it can update what's shown on the screen
Here's what that information flow looks like visually:
What You've Learned
Summary
- Redux's intent can be summarized in three principles
- Global app state is kept in a single store
- The store country is read-just to the residue of the app
- Reducer functions are used to update the state in response to deportment
- Redux uses a "ane-mode 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 activity
- The store runs the reducers, and the state is updated based on what occurred
- The shop notifies the UI that the state has changed
- The UI re-renders based on the new country
What'southward Next?
You should now be familiar with the key concepts and terms that describe the unlike parts of a Redux app.
Now, permit's see how those pieces work together equally nosotros start building a new Redux awarding in Role 3: State, Actions, and Reducers.
Source: https://redux.js.org/tutorials/fundamentals/part-2-concepts-data-flow
0 Response to "Redux Action Being Dispatched Over and Over Again"
Post a Comment