Redux Action Being Dispatched Over and Over Again

Redux Essentials, Part 5: Async Logic and Information Fetching

What You'll Learn

  • How to utilise the Redux "thunk" middleware for async logic
  • Patterns for treatment async request land
  • How to apply the Redux Toolkit createAsyncThunk API to simplify async calls

Prerequisites

  • Familiarity with using AJAX requests to fetch and update data from a server

Introduction​

In Part 4: Using Redux Data, nosotros saw how to use multiple pieces of data from the Redux store inside of React components, customize the contents of action objects before they're dispatched, and handle more complex update logic in our reducers.

Then far, all the data we've worked with has been directly inside of our React client application. Nevertheless, most real applications need to work with data from a server, by making HTTP API calls to fetch and save items.

In this section, nosotros'll convert our social media app to fetch the posts and users data from an API, and add new posts by saving them to the API.

Example Remainder API and Client​

To go along the example projection isolated but realistic, the initial project setup already includes a false in-retention REST API for our data (configured using the Mock Service Worker mock API tool). The API uses /fakeApi as the base URL for the endpoints, and supports the typical Go/Postal service/PUT/DELETE HTTP methods for /fakeApi/posts, /fakeApi/users, and fakeApi/notifications. It's defined in src/api/server.js.

The project also includes a small HTTP API customer object that exposes customer.become() and client.post() methods, like to popular HTTP libraries like axios. It's defined in src/api/client.js.

We'll utilise the client object to make HTTP calls to our in-retentiveness fake REST API for this department.

Also, the mock server has been set to reuse the same random seed each time the page is loaded, and so that it volition generate the same list of simulated users and fake posts. If you lot want to reset that, delete the 'randomTimestampSeed' value in your browser'south Local Storage and reload the folio, or you can turn that off past editing src/api/server.js and setting useSeededRNG to simulated.

info

As a reminder, the code examples focus on the central concepts and changes for each section. Come across the CodeSandbox projects and the tutorial-steps branch in the project repo for the complete changes in the application.

Thunks and Async Logic​

Using Middleware to Enable Async Logic​

By itself, a Redux shop doesn't know anything most async logic. It only knows how to synchronously dispatch actions, update the state by calling the root reducer function, and notify the UI that something has changed. Whatsoever asynchronicity has to happen outside the store.

Simply, what if you want to have async logic interact with the shop by dispatching or checking the current store country? That'due south where Redux middleware come in. They extend the store, and permit you to:

  • Execute extra logic when any action is dispatched (such as logging the activeness and state)
  • Pause, modify, filibuster, supplant, or halt dispatched actions
  • Write actress lawmaking that has access to dispatch and getState
  • Teach dispatch how to accept other values besides plain action objects, such as functions and promises, by intercepting them and dispatching real action objects instead

The near common reason to utilize middleware is to let different kinds of async logic to interact with the shop. This allows you to write lawmaking that can dispatch actions and bank check the store state, while keeping that logic carve up from your UI.

There are many kinds of async middleware for Redux, and each lets yous write your logic using different syntax. The most common async middleware is redux-thunk, which lets yous write plain functions that may contain async logic directly. Redux Toolkit'southward configureStore function automatically sets upward the thunk middleware by default, and we recommend using thunks as the standard approach for writing async logic with Redux.

Earlier, nosotros saw what the synchronous information flow for Redux looks like. When nosotros introduce asynchronous logic, we add an extra step where middleware can run logic like AJAX requests, then dispatch deportment. That makes the async data period wait like this:

Redux async data flow diagram

Thunk Functions​

One time the thunk middleware has been added to the Redux shop, it allows you lot to laissez passer thunk functions straight to shop.dispatch. A thunk part volition always be called with (acceleration, getState) as its arguments, and you tin use them inside the thunk as needed.

Thunks typically acceleration plain actions using action creators, like dispatch(increment()):

                                          const                                  store                                =                                                configureStore                (                {                                                reducer                :                                  counterReducer                                }                )                                

const exampleThunkFunction = ( dispatch , getState ) => {
const stateBefore = getState ( )
panel . log ( ` Counter before: ${ stateBefore . counter } ` )
dispatch ( increment ( ) )
const stateAfter = getState ( )
panel . log ( ` Counter afterward: ${ stateAfter . counter } ` )
}

store . dispatch ( exampleThunkFunction )

For consistency with dispatching normal action objects, we typically write these as thunk action creators, which return the thunk part. These action creators can take arguments that can be used within the thunk.

                                          const                                                logAndAdd                                                =                                                amount                                                =>                                                {                                
return ( dispatch , getState ) => {
const stateBefore = getState ( )
console . log ( ` Counter before: ${ stateBefore . counter } ` )
acceleration ( incrementByAmount ( amount ) )
const stateAfter = getState ( )
console . log ( ` Counter afterward: ${ stateAfter . counter } ` )
}
}

store . dispatch ( logAndAdd ( five ) )

Thunks are typically written in "piece" files. createSlice itself does non have whatsoever special support for defining thunks, so y'all should write them as split up functions in the same piece file. That fashion, they take access to the plain action creators for that slice, and information technology's easy to find where the thunk lives.

Writing Async Thunks​

Thunks may accept async logic within of them, such every bit setTimeout, Promises, and async/wait. This makes them a good place to put AJAX calls to a server API.

Data fetching logic for Redux typically follows a predictable blueprint:

  • A "start" activity is dispatched before the request, to indicate that the request is in progress. This may exist used to track loading land to allow skipping duplicate requests or show loading indicators in the UI.
  • The async request is fabricated
  • Depending on the request upshot, the async logic dispatches either a "success" action containing the event information, or a "failure" action containing fault details. The reducer logic clears the loading land in both cases, and either processes the effect data from the success example, or stores the fault value for potential display.

These steps are not required, but are commonly used. (If all you lot care about is a successful result, you can just dispatch a single "success" action when the request finishes, and skip the "start" and "failure" deportment.)

Redux Toolkit provides a createAsyncThunk API to implement the cosmos and dispatching of these actions, and we'll wait at how to apply information technology shortly.

Detailed Caption: Dispatching Request Status Actions in Thunks

If we were to write out the code for a typical async thunk by hand, it might look like this:

                                                const                                                      getRepoDetailsStarted                                                      =                                                      (                  )                                                      =>                                                      (                  {                                    
type : 'repoDetails/fetchStarted'
} )
const getRepoDetailsSuccess = repoDetails => ( {
type : 'repoDetails/fetchSucceeded' ,
payload : repoDetails
} )
const getRepoDetailsFailed = mistake => ( {
type : 'repoDetails/fetchFailed' ,
mistake
} )
const fetchIssuesCount = ( org , repo ) => async dispatch => {
acceleration ( getRepoDetailsStarted ( ) )
endeavour {
const repoDetails = expect getRepoDetails ( org , repo )
acceleration ( getRepoDetailsSuccess ( repoDetails ) )
} catch ( err ) {
acceleration ( getRepoDetailsFailed ( err . toString ( ) ) )
}
}

Even so, writing lawmaking using this approach is tedious. Each separate type of request needs repeated similar implementation:

  • Unique action types need to be defined for the iii different cases
  • Each of those action types commonly has a corresponding activeness creator part
  • A thunk has to exist written that dispatches the correct actions in the right sequence

createAsyncThunk abstracts this blueprint by generating the action types and action creators, and generating a thunk that dispatches those actions automatically. Y'all provide a callback part that makes the async phone call and returns a Promise with the result.


tip

Redux Toolkit has a new RTK Query data fetching API. RTK Query is a purpose built data fetching and caching solution for Redux apps, and tin eliminate the need to write any thunks or reducers to manage information fetching. We encourage y'all to try information technology out and see if it can assistance simplify the data fetching code in your own apps!

We'll cover how to use RTK Query starting in Part 7: RTK Query Nuts.

Loading Posts​

So far, our postsSlice has used some hardcoded sample data equally its initial state. We're going to switch that to start with an empty array of posts instead, and then fetch a list of posts from the server.

In order to do that, we're going to have to modify the construction of the country in our postsSlice, so that nosotros tin go along track of the electric current state of the API request.

Right at present, the postsSlice state is a unmarried array of posts. Nosotros need to change that to be an object that has the posts array, plus the loading land fields.

Meanwhile, the UI components similar <PostsList> are trying to read posts from state.posts in their useSelector hooks, assuming that field is an array. We need to alter those locations also to friction match the new information.

Information technology would be dainty if nosotros didn't accept to keep rewriting our components every time nosotros made a change to the data format in our reducers. One way to avoid this is to define reusable selector functions in the piece files, and have the components use those selectors to excerpt the data they need instead of repeating the selector logic in each component. That mode, if we exercise modify our state construction again, we only need to update the code in the slice file.

The <PostsList> component needs to read a list of all the posts, and the <SinglePostPage> and <EditPostForm> components need to expect up a unmarried post by its ID. Let'south export 2 minor selector functions from postsSlice.js to embrace those cases:

features/posts/postsSlice.js

                                                const                                      postsSlice                                    =                                                      createSlice                  (                  /* omit piece code*/                  )                                    

export const { postAdded , postUpdated , reactionAdded } = postsSlice . deportment

export default postsSlice . reducer

export const selectAllPosts = state => state . posts

export const selectPostById = ( state , postId ) =>
state . posts . find ( post => post . id === postId )

Note that the state parameter for these selector functions is the root Redux state object, equally information technology was for the inlined anonymous selectors we wrote directly inside of useSelector.

We can then employ them in the components:

features/posts/PostsList.js

                                                // omit imports                                    
import { selectAllPosts } from './postsSlice'

export const PostsList = ( ) => {
const posts = useSelector ( selectAllPosts )
// omit component contents
}

features/posts/SinglePostPage.js

                                                // omit imports                                    
import { selectPostById } from './postsSlice'

export const SinglePostPage = ( { match } ) => {
const { postId } = match . params

const mail service = useSelector ( state => selectPostById ( land , postId ) )
// omit component logic
}

features/posts/EditPostForm.js

                                                // omit imports                                    
import { postUpdated , selectPostById } from './postsSlice'

consign const EditPostForm = ( { match } ) => {
const { postId } = match . params

const post = useSelector ( country => selectPostById ( state , postId ) )
// omit component logic
}

Information technology'southward oftentimes a good idea to encapsulate data lookups by writing reusable selectors. You tin can also create "memoized" selectors that can help improve operation, which we'll expect at in a later role of this tutorial.

Merely, like any abstraction, it'southward non something you should practice all the time, everywhere. Writing selectors means more code to sympathise and maintain. Don't feel like you need to write selectors for every single field of your state. Try starting without any selectors, and add together some later when you discover yourself looking up the same values in many parts of your application lawmaking.

Loading State for Requests​

When we make an API call, nosotros tin view its progress as a small country machine that tin can be in one of four possible states:

  • The request hasn't started withal
  • The request is in progress
  • The asking succeeded, and we now have the data we need
  • The request failed, and there's probably an error message

We could rail that data using some booleans, like isLoading: truthful, merely information technology's ameliorate to rails these states every bit a single enum value. A skilful pattern for this is to accept a state section that looks similar this (using TypeScript type notation):

                                          {                                
// Multiple possible status enum values
status : 'idle' | 'loading' | 'succeeded' | 'failed' ,
error : string | aught
}

These fields would exist alongside whatever actual information is being stored. These specific string country names aren't required - feel free to use other names if you lot want, similar 'pending' instead of 'loading', or 'complete' instead of 'succeeded'.

We can use this data to decide what to evidence in our UI as the request progresses, and also add logic in our reducers to prevent cases like loading data twice.

Let'southward update our postsSlice to use this pattern to track loading land for a "fetch posts" request. We'll switch our state from being an array of posts by itself, to expect like {posts, status, error}. We'll also remove the old sample post entries from our initial state. As part of this change, nosotros also need to change any uses of land equally an array to be state.posts instead, because the array is now one level deeper:

features/posts/postsSlice.js

                                                import                                                      {                                      createSlice                  ,                                      nanoid                                    }                                                      from                                                      '@reduxjs/toolkit'                                    

const initialState = {
posts : [ ] ,
status : 'idle' ,
error : null
}

const postsSlice = createSlice ( {
name : 'posts' ,
initialState ,
reducers : {
postAdded : {
reducer ( state , action ) {
state . posts . push button ( action . payload )
} ,
prepare ( championship , content , userId ) {
// omit prepare logic
}
} ,
reactionAdded ( country , action ) {
const { postId , reaction } = action . payload
const existingPost = state . posts . find ( post => post . id === postId )
if ( existingPost ) {
existingPost . reactions [ reaction ] ++
}
} ,
postUpdated ( country , action ) {
const { id , title , content } = activeness . payload
const existingPost = land . posts . find ( mail service => post . id === id )
if ( existingPost ) {
existingPost . title = title
existingPost . content = content
}
}
}
} )

export const { postAdded , postUpdated , reactionAdded } = postsSlice . deportment

export default postsSlice . reducer

export const selectAllPosts = state => state . posts . posts

consign const selectPostById = ( state , postId ) =>
land . posts . posts . find ( post => post . id === postId )

Yep, this does mean that we now take a nested object path that looks similar state.posts.posts, which is somewhat repetitive and airheaded :) We could modify the nested array name to exist items or data or something if we wanted to avoid that, but we'll go out it equally-is for now.

Fetching Data with createAsyncThunk

Redux Toolkit'due south createAsyncThunk API generates thunks that automatically dispatch those "get-go/success/failure" actions for y'all.

Let's outset past adding a thunk that will make an AJAX call to retrieve a list of posts. We'll import the client utility from the src/api binder, and use that to make a request to '/fakeApi/posts'.

features/posts/postsSlice

                                                import                                                      {                                      createSlice                  ,                                      nanoid                  ,                                      createAsyncThunk                                    }                                                      from                                                      '@reduxjs/toolkit'                                    
import { client } from '../../api/customer'

const initialState = {
posts : [ ] ,
status : 'idle' ,
error : nada
}

export const fetchPosts = createAsyncThunk ( 'posts/fetchPosts' , async ( ) => {
const response = await customer . go ( '/fakeApi/posts' )
render response . data
} )

createAsyncThunk accepts two arguments:

  • A string that will be used as the prefix for the generated action types
  • A "payload creator" callback role that should render a Promise containing some information, or a rejected Promise with an error

The payload creator volition usually make an AJAX telephone call of some kind, and can either return the Promise from the AJAX call straight, or excerpt some information from the API response and render that. We typically write this using the JS async/await syntax, which lets us write functions that use Promises while using standard try/catch logic instead of somePromise.so() bondage.

In this case, nosotros pass in 'posts/fetchPosts' equally the activeness blazon prefix. Our payload cosmos callback waits for the API call to return a response. The response object looks like {data: []}, and we want our dispatched Redux action to have a payload that is just the array of posts. So, we extract response.data, and return that from the callback.

If nosotros try calling acceleration(fetchPosts()), the fetchPosts thunk volition outset dispatch an action blazon of 'posts/fetchPosts/pending':

`createAsyncThunk`: posts pending action

We tin listen for this action in our reducer and mark the request status as 'loading'.

Once the Promise resolves, the fetchPosts thunk takes the response.data array we returned from the callback, and dispatches a 'posts/fetchPosts/fulfilled' action containing the posts assortment every bit activeness.payload:

`createAsyncThunk`: posts pending action

Dispatching Thunks from Components​

And then, let'south update our <PostsList> component to actually fetch this data automatically for us.

We'll import the fetchPosts thunk into the component. Like all of our other action creators, we have to dispatch it, and then we'll also demand to add together the useDispatch hook. Since we want to fetch this data when <PostsList> mounts, we need to import the React useEffect hook:

features/posts/PostsList.js

                                                import                                                      React                  ,                                                      {                                      useEffect                                    }                                                      from                                                      'react'                                    
import { useSelector , useDispatch } from 'react-redux'
// omit other imports
import { selectAllPosts , fetchPosts } from './postsSlice'

export const PostsList = ( ) => {
const dispatch = useDispatch ( )
const posts = useSelector ( selectAllPosts )

const postStatus = useSelector ( land => state . posts . status )

useEffect ( ( ) => {
if ( postStatus === 'idle' ) {
dispatch ( fetchPosts ( ) )
}
} , [ postStatus , dispatch ] )

// omit rendering logic
}

It'south of import that nosotros only try to fetch the list of posts in one case. If nosotros exercise information technology every time the <PostsList> component renders, or is re-created considering we've switched betwixt views, we might stop upward fetching the posts several times. We can utilize the posts.condition enum to help decide if we need to actually starting time fetching, by selecting that into the component and but starting the fetch if the status is 'idle'.

Reducers and Loading Actions​

Adjacent upwards, nosotros demand to handle both these actions in our reducers. This requires a bit deeper look at the createSlice API we've been using.

We've already seen that createSlice will generate an activity creator for every reducer office we define in the reducers field, and that the generated action types include the proper name of the slice, similar:

                                          panel                .                log                (                                
postUpdated ( { id : '123' , championship : 'Kickoff Mail service' , content : 'Some text here' } )
)
/*
{
type: 'posts/postUpdated',
payload: {
id: '123',
title: 'First Post',
content: 'Some text here'
}
}
*/

All the same, there are times when a slice reducer needs to respond to other actions that weren't defined as function of this slice's reducers field. We tin exercise that using the slice extraReducers field instead.

The extraReducers option should be a function that receives a parameter called builder. The builder object provides methods that allow us define additional example reducers that volition run in response to deportment defined exterior of the slice. Nosotros'll use builder.addCase(actionCreator, reducer) to handle each of the actions dispatched by our async thunks.

Detailed Caption: Adding Actress Reducers to Slices

The builder object in extraReducers provides methods that let u.s. define boosted case reducers that will run in response to actions defined outside of the slice:

  • builder.addCase(actionCreator, reducer): defines a case reducer that handles a single known action type based on either an RTK activeness creator or a apparently activeness type cord
  • architect.addMatcher(matcher, reducer): defines a instance reducer that can run in response to any action where the matcher role returns truthful
  • architect.addDefaultCase(reducer): defines a case reducer that will run if no other case reducers were executed for this action.

Y'all tin can chain these together, like builder.addCase().addCase().addMatcher().addDefaultCase(). If multiple matchers match the action, they will run in the order they were defined.

                                                import                                                      {                                      increment                                    }                                                      from                                                      '../features/counter/counterSlice'                                    

const postsSlice = createSlice ( {
proper noun : 'posts' ,
initialState ,
reducers : {
// slice-specific reducers hither
} ,
extraReducers : builder => {
architect
. addCase ( 'counter/decrement' , ( land , action ) => { } )
. addCase ( increase , ( state , action ) => { } )
}
} )

In this case, we need to listen for the "awaiting" and "fulfilled" action types dispatched by our fetchPosts thunk. Those action creators are attached to our bodily fetchPost part, and we can pass those to extraReducers to mind for those actions:

                                          consign                                                const                                  fetchPosts                                =                                                createAsyncThunk                (                'posts/fetchPosts'                ,                                                async                                                (                )                                                =>                                                {                                
const response = look client . get ( '/fakeApi/posts' )
return response . data
} )

const postsSlice = createSlice ( {
proper noun : 'posts' ,
initialState ,
reducers : {
// omit existing reducers hither
} ,
extraReducers ( builder ) {
builder
. addCase ( fetchPosts . pending , ( country , activeness ) => {
state . status = 'loading'
} )
. addCase ( fetchPosts . fulfilled , ( state , activeness ) => {
state . status = 'succeeded'
// Add any fetched posts to the array
state . posts = state . posts . concat ( action . payload )
} )
. addCase ( fetchPosts . rejected , ( land , action ) => {
country . status = 'failed'
state . error = action . error . bulletin
} )
}
} )

We'll handle all 3 action types that could be dispatched by the thunk, based on the Promise we returned:

  • When the asking starts, we'll set the status enum to 'loading'
  • If the request succeeds, we marking the condition as 'succeeded', and add the fetched posts to state.posts
  • If the asking fails, we'll mark the status every bit 'failed', and save any error message into the country so we can display it

Displaying Loading State​

Our <PostsList> component is already checking for whatsoever updates to the posts that are stored in Redux, and rerendering itself any fourth dimension that list changes. And so, if we refresh the page, nosotros should see a random prepare of posts from our faux API evidence upwardly on screen:

The fake API we're using returns information immediately. However, a real API call volition probably take some time to return a response. Information technology'south usually a proficient idea to prove some kind of "loading..." indicator in the UI so the user knows we're waiting for data.

We can update our <PostsList> to show a dissimilar fleck of UI based on the state.posts.status enum: a spinner if we're loading, an mistake message if it failed, or the actual posts list if we have the information. While we're at it, this is probably a good time to extract a <PostExcerpt> component to encapsulate the rendering for one item in the list likewise.

The result might await like this:

features/posts/PostsList.js

                                                import                                                      {                                      Spinner                                    }                                                      from                                                      '../../components/Spinner'                                    
import { PostAuthor } from './PostAuthor'
import { TimeAgo } from './TimeAgo'
import { ReactionButtons } from './ReactionButtons'
import { selectAllPosts , fetchPosts } from './postsSlice'

const PostExcerpt = ( { post } ) => {
return (
< article className = " mail-excerpt " central = { postal service . id } >
< h3 > { post . title } </ h3 >
< div >
< PostAuthor userId = { post . user } />
< TimeAgo timestamp = { post . date } />
</ div >
< p className = " mail-content " > { postal service . content . substring ( 0 , 100 ) } </ p >

< ReactionButtons post = { post } />
< Link to = { ` /posts/ ${ mail service . id } ` } className = " push muted-button " >
View Post
</ Link >
</ commodity >
)
}

export const PostsList = ( ) => {
const acceleration = useDispatch ( )
const posts = useSelector ( selectAllPosts )

const postStatus = useSelector ( country => land . posts . status )
const error = useSelector ( state => state . posts . error )

useEffect ( ( ) => {
if ( postStatus === 'idle' ) {
dispatch ( fetchPosts ( ) )
}
} , [ postStatus , dispatch ] )

permit content

if ( postStatus === 'loading' ) {
content = < Spinner text = " Loading... " />
} else if ( postStatus === 'succeeded' ) {
// Sort posts in reverse chronological gild by datetime string
const orderedPosts = posts
. piece ( )
. sort ( ( a , b ) => b . date . localeCompare ( a . date ) )

content = orderedPosts . map ( mail => (
< PostExcerpt central = { mail service . id } postal service = { post } />
) )
} else if ( postStatus === 'failed' ) {
content = < div > { error } </ div >
}

return (
< department className = " posts-list " >
< h2 > Posts </ h2 >
{ content }
</ department >
)
}

Y'all might discover that the API calls are taking a while to consummate, and that the loading spinner is staying on screen for a couple seconds. Our mock API server is configured to add a 2-2d delay to all responses, specifically to aid visualize times when there's a loading spinner visible. If you want to change this behavior, you can open up api/server.js, and alter this line:

api/server.js

                                                // Add an extra filibuster to all endpoints, so loading spinners show upward.                                    
const ARTIFICIAL_DELAY_MS = 2000

Feel gratis to turn that on and off as nosotros go if yous want the API calls to complete faster.

Loading Users​

We're now fetching and displaying our list of posts. But, if nosotros look at the posts, there'due south a problem: they all now say "Unknown author" as the authors:

Unknown post authors

This is considering the mail service entries are being randomly generated by the fake API server, which too randomly generates a gear up of fake users every time we reload the page. We need to update our users slice to fetch those users when the awarding starts.

Similar last time, we'll create some other async thunk to get the users from the API and return them, and then handle the fulfilled action in the extraReducers slice field. We'll skip worrying about loading country for now:

features/users/usersSlice.js

                                                import                                                      {                                      createSlice                  ,                                      createAsyncThunk                                    }                                                      from                                                      '@reduxjs/toolkit'                                    
import { client } from '../../api/client'

const initialState = [ ]

export const fetchUsers = createAsyncThunk ( 'users/fetchUsers' , async ( ) => {
const response = await customer . get ( '/fakeApi/users' )
return response . data
} )

const usersSlice = createSlice ( {
name : 'users' ,
initialState ,
reducers : { } ,
extraReducers ( builder ) {
builder . addCase ( fetchUsers . fulfilled , ( state , action ) => {
return action . payload
} )
}
} )

export default usersSlice . reducer

You may have noticed that this time the case reducer isn't using the state variable at all. Instead, we're returning the action.payload direct. Immer lets usa update country in two ways: either mutating the existing land value, or returning a new result. If we return a new value, that will replace the existing state completely with whatever we return. (Note that if you lot desire to manually return a new value, information technology'south up to you to write any immutable update logic that might exist needed.)

In this case, the initial state was an empty array, and we probably could accept done state.push(...action.payload) to mutate it. Simply, in our case nosotros actually want to supervene upon the list of users with whatever the server returned, and this avoids any hazard of accidentally duplicating the listing of users in country.

Nosotros only need to fetch the list of users once, and nosotros want to practise it correct when the awarding starts. We tin exercise that in our index.js file, and directly dispatch the fetchUsers thunk considering nosotros accept the store right there:

index.js

                                                // omit other imports                                    

import { fetchUsers } from './features/users/usersSlice'

import { worker } from './api/server'

async function main ( ) {
// Start our mock API server
await worker . commencement ( { onUnhandledRequest : 'featherbed' } )

store . dispatch ( fetchUsers ( ) )

ReactDOM . render (
< React . StrictMode >
< Provider store = { store } >
< App / >
< / Provider >
< / React . StrictMode > ,
certificate . getElementById ( 'root' )
)
}
main ( )

At present, each of the posts should exist showing a username again, and we should besides take that same list of users shown in the "Author" dropdown in our <AddPostForm>.

Calculation New Posts​

Nosotros accept i more step for this section. When we add together a new post from the <AddPostForm>, that post is only getting added to the Redux store inside our app. Nosotros need to actually make an API call that will create the new mail entry in our fake API server instead, so that it's "saved". (Since this is a fake API, the new mail service won't persist if we reload the folio, only if we had a real backend server it would be available next time we reload.)

Sending Data with Thunks​

We can use createAsyncThunk to help with sending data, not just fetching it. Nosotros'll create a thunk that accepts the values from our <AddPostForm> as an argument, and makes an HTTP POST call to the false API to save the data.

In the process, nosotros're going to change how we piece of work with the new post object in our reducers. Currently, our postsSlice is creating a new post object in the set up callback for postAdded, and generating a new unique ID for that post. In most apps that save information to a server, the server will take care of generating unique IDs and filling out whatever extra fields, and volition ordinarily return the completed data in its response. And then, we can ship a request trunk like { championship, content, user: userId } to the server, and then accept the complete post object information technology sends dorsum and add it to our postsSlice land.

features/posts/postsSlice.js

                                                export                                                      const                                      addNewPost                                    =                                                      createAsyncThunk                  (                                    
'posts/addNewPost' ,
// The payload creator receives the partial `{title, content, user}` object
async initialPost => {
// We transport the initial data to the faux API server
const response = wait client . post ( '/fakeApi/posts' , initialPost )
// The response includes the consummate post object, including unique ID
return response . data
}
)

const postsSlice = createSlice ( {
name : 'posts' ,
initialState ,
reducers : {
// The existing `postAdded` reducer and gear up callback were deleted
reactionAdded ( state , activity ) { } , // omit logic
postUpdated ( state , activity ) { } // omit logic
} ,
extraReducers ( builder ) {
// omit posts loading reducers
builder . addCase ( addNewPost . fulfilled , ( country , activeness ) => {
// We can directly add the new post object to our posts array
state . posts . push button ( action . payload )
} )
}
} )

Checking Thunk Results in Components​

Finally, we'll update <AddPostForm> to dispatch the addNewPost thunk instead of the old postAdded action. Since this is another API call to the server, it will take some time and could neglect. The addNewPost() thunk will automatically acceleration its pending/fulfilled/rejected actions to the Redux store, which we're already handling. We could track the request status in postsSlice using a second loading enum if nosotros wanted to, but for this example let's keep the loading state tracking limited to the component.

Information technology would be good if nosotros can at to the lowest degree disable the "Save Mail" button while we're waiting for the request, then the user can't accidentally try to save a post twice. If the request fails, we might also desire to show an error message here in the class, or possibly just log information technology to the console.

Nosotros can have our component logic wait for the async thunk to cease, and check the effect when information technology's done:

features/posts/AddPostForm.js

                                                import                                                      React                  ,                                                      {                                      useState                                    }                                                      from                                                      'react'                                    
import { useDispatch , useSelector } from 'react-redux'

import { addNewPost } from './postsSlice'

export const AddPostForm = ( ) => {
const [ championship , setTitle ] = useState ( '' )
const [ content , setContent ] = useState ( '' )
const [ userId , setUserId ] = useState ( '' )
const [ addRequestStatus , setAddRequestStatus ] = useState ( 'idle' )

// omit useSelectors and change handlers

const canSave =
[ title , content , userId ] . every ( Boolean ) && addRequestStatus === 'idle'

const onSavePostClicked = async ( ) => {
if ( canSave ) {
try {
setAddRequestStatus ( 'pending' )
await dispatch ( addNewPost ( { title , content , user : userId } ) ) . unwrap ( )
setTitle ( '' )
setContent ( '' )
setUserId ( '' )
} catch ( err ) {
console . error ( 'Failed to save the post: ' , err )
} finally {
setAddRequestStatus ( 'idle' )
}
}
}

// omit rendering logic
}

We tin can add a loading status enum field as a React useState hook, similar to how nosotros're tracking loading land in postsSlice for fetching posts. In this example, we just want to know if the asking is in progress or not.

When we call dispatch(addNewPost()), the async thunk returns a Promise from acceleration. Nosotros can await that promise here to know when the thunk has finished its request. Merely, nosotros don't yet know if that request succeeded or failed.

createAsyncThunk handles whatsoever errors internally, so that we don't come across any messages nigh "rejected Promises" in our logs. It then returns the final action it dispatched: either the fulfilled activeness if it succeeded, or the rejected activeness if information technology failed.

Yet, it'due south common to want to write logic that looks at the success or failure of the actual asking that was made. Redux Toolkit adds a .unwrap() function to the returned Promise, which will return a new Hope that either has the actual action.payload value from a fulfilled action, or throws an error if it's the rejected action. This lets us handle success and failure in the component using normal attempt/catch logic. So, nosotros'll articulate out the input fields to reset the form if the post was successfully created, and log the mistake to the console if information technology failed.

If you desire to see what happens when the addNewPost API telephone call fails, attempt creating a new mail where the "Content" field only has the word "error" (without quotes). The server will see that and ship back a failed response, so yous should see a message logged to the console.

What You've Learned​

Async logic and information fetching are always a circuitous topic. As you've seen, Redux Toolkit includes some tools to automate the typical Redux data fetching patterns.

Hither'due south what our app looks like at present that we're fetching data from that fake API:

As a reminder, here's what we covered in this section:

Summary

  • You can write reusable "selector" functions to encapsulate reading values from the Redux land
    • Selectors are functions that get the Redux state as an argument, and return some data
  • Redux uses plugins called "middleware" to enable async logic
    • The standard async middleware is chosen redux-thunk, which is included in Redux Toolkit
    • Thunk functions receive dispatch and getState equally arguments, and can use those as part of async logic
  • You tin can dispatch additional actions to help track the loading status of an API phone call
    • The typical design is dispatching a "pending" activity before the phone call, then either a "success" containing the data or a "failure" action containing the fault
    • Loading state should usually exist stored as an enum, similar 'idle' | 'loading' | 'succeeded' | 'failed'
  • Redux Toolkit has a createAsyncThunk API that dispatches these deportment for you
    • createAsyncThunk accepts a "payload creator" callback that should return a Hope, and generates pending/fulfilled/rejected activity types automatically
    • Generated action creators like fetchPosts dispatch those actions based on the Hope you lot render
    • You tin can listen for these action types in createSlice using the extraReducers field, and update the country in reducers based on those actions.
    • Action creators can be used to automatically fill in the keys of the extraReducers object and so the slice knows what deportment to mind for.
    • Thunks tin can return promises. For createAsyncThunk specifically, you tin can await dispatch(someThunk()).unwrap() to handle the request success or failure at the component level.

What's Next?​

We've got one more set of topics to cover the core Redux Toolkit APIs and usage patterns. In Part 6: Operation and Normalizing Data, nosotros'll look at how Redux usage affects React performance, and some means we tin optimize our application for improved performance.

munozrookencepor.blogspot.com

Source: https://redux.js.org/tutorials/essentials/part-5-async-logic

0 Response to "Redux Action Being Dispatched Over and Over Again"

Post a Comment

Iklan Atas Artikel

Iklan Tengah Artikel 1

Iklan Tengah Artikel 2

Iklan Bawah Artikel