Главная » Ошибки » Асинхронные запросы (Thunk)

Асинхронные запросы (Thunk)

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов

Электронная почта *
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Рекомендуемые программы

С нуля до разработчика. Возвращаем деньги, если не удалось найти работу.

Иконка программы Фронтенд-разработчик
Профессия Фронтенд-разработчик Разработка фронтенд-компонентов для веб-приложений
27 октября

10 месяцев
Иконка программы Fullstack-разработчик
Профессия Fullstack-разработчик Разработка фронтенд- и бэкенд-компонентов для веб-приложений
27 октября

16 месяцев

Recent posts

  • API with NestJS #79. Implementing searching with pattern matching and raw SQL
  • API with NestJS #78. Generating statistics using aggregate functions in raw SQL
  • API with NestJS #77. Offset and keyset pagination with raw SQL queries
  • API with NestJS #76. Working with transactions using raw SQL queries
  • API with NestJS #75. Many-to-many relationships using raw SQL queries

After – How to solve these problems?

Let’s have a look at the code again:

import axios from ‘axios;constcreateArticle=(title, text)=> dispatch=> {dispatch({ type:”CREATE_ARTICLE_REQUEST”})return axios
.post(“/api/articles”,{ title, text }).then(successHandler).catch(error=> {// Deal with the errordispatch(errorActionCreator(“CREATE_ARTICLE_ERROR”, error))})}

errorActionCreator implementation that respects FSA

exportconsterrorActionCreator=(errorType, error)=> {return{
type: errorType,
error:true,
payload: error,}}

Then, in the reducer, the handling would be something like:

constreducer=(state, action)=> {switch(action.type){case”CREATE_ARTICLE_REQUEST”:return{…errorReducer(state, action),
isLoading:true,}case”CREATE_ARTICLE_ERROR”:return{…errorReducer(state, action),
isLoading:false,}}}

errorReducer implementation

exportconsterrorReducer=(state, action)=> {if(!action.error){return{…state,
error:null,}}return{…state,
error:{
errorMessage:DEFAULT_ERROR_MESSAGE,…action.payload.response.data,},}}

And on your components, you would do something like the following to show the error to the user:

import{ useSelector }from’react-redux’;import{ getArticlesErrorMessage }from’../store/articles/selectors’;constArticleListPage=({ errorMessage })=> {const errorMessage =useSelector(getArticlesErrorMessage)return(

{/* Cut for brevity */}

);
};

Using ErrorMessage component to present the error coherently.

exportconstErrorMessage=({ error })=> {if(!error){returnnull;}return(

{error}

);}

To finish, we added this selector that is used by components to get error messages.

const getArticlesErrorMessage =createErrorSelector(state=> state.articles)

The implementation of createErrorSelector is shown below, it extends reselectcreateSelector to lookup for the error in the specific structure we want.

import{ createSelector }from”reselect”import{ get }from”lodash”exportconstcreateErrorSelector=fn=> {returncreateSelector(
fn,storeIndex=> get(storeIndex,”error.errorMessage”,null))}

With this approach, we managed to fix all the listed problems. We have created a layer of decoupling that enables us to change without breaking all the other usages.

  • Enforced error structure
  • Error reducer is shared
  • Decoupled reducers and components from errors format
  • Error messages are shown coherently

We’ve decided to locate the 3 functions: createErrorSelector, errorReducer and errorActionCreator in the same file. If one of them needs to be changed, it most likely means that the other two also need to. In contrast, all the reducers, components and thunks that are dealing with errors can remain intact.

This was the kind of flexibility we aimed to.

Updating the data with mutations

To update the data, we need to use mutations. To do that, we can use the most significant hook used for mutation, the
useMutation hook.

1234567891011121314151617181920212223242526272829303132333435import React,{ChangeEvent,FunctionComponent,useState}from’react’;import{useUpdatePhotoMutation}from’./api/api’; interfaceProps{  photoId:number;} const PhotoTitleInput:FunctionComponent

=({photoId})=>{  const[newTitle,setNewTitle]=useState(”);  const[updatePhoto]=useUpdatePhotoMutation();   consthandleInputChange=(event:ChangeEvent)=>{    setNewTitle(event.target.value);  };   consthandleSubmit=()=>{    updatePhoto({      id:photoId,      data:{        title:newTitle      }    })  };   return(    

                

  );}; export defaultPhotoTitleInput;

The crucial thing is that mutating a post with a given id should modify the cache. One way to achieve that is with tags.

Automated cache invalidating with tags

RTK query creates a tag system to automatically refetch the data affected by mutations. Queries can provide tags, while mutations can invalidate them.

12345678910111213141516171819202122232425262728293031323334import{createApi,fetchBaseQuery}from’@reduxjs/toolkit/query/react’;import Photo from’./photo’; export constapi=createApi({  reducerPath:’api’,  tagTypes:[‘Photos’],  baseQuery:fetchBaseQuery({    baseUrl:process.env.REACT_APP_API_URL,  }),  endpoints:(builder)=>({    getPhotos:builder.query

({      query:()=>’photos’,      providesTags:[{type:’Photos’,id:’LIST’}],    }),    getPhotoById:builder.query

({      query:(photoId:number)=>`photos/${photoId}`,      providesTags:(result,error,id)=>[{type:’Photos’,id}],    }),    updatePhoto:builder.mutation<>

}>({      query:({id,data})=>({        url:`photos/${id}`,        method:’PATCH’,        body:data,      }),      invalidatesTags:(result)=>        result          ?[              {type:’Photos’,id:result.id},              {type:’Photos’,id:’LIST’},            ]          :[],    }),  }),});

In our example above, we can see that the
getPhotos provides just one tag:
{type:’Photos’,id:’LIST’}.

The
getPhotoById query provides one tag per photo. For example, using
useGetPhotoByIdQuery(1) causes the
{type:’Photos’,id:1} tag to be created.

The
updatePhoto mutation would invalidate two tags if the update were successful.

123456updatePhoto({  id:1,  data:{    title:’New photo title’,  },});

Using the above mutation invalidates the following tags:

  • {type:’Photos’,id:1}
  • {type:’Photos’,id:’LIST’}

Because of that, running the above mutation causes three API requests:

  • PATCH/photos/1,
  • GET/photos/1,
  • GET/photos.

Thanks to doing the above, the cache is refreshed for the
getPhotos and the
getPhotoById query.

Manual cache invalidating

Making additional API requests every time we change a single photo is not optimal. In RESTful APIs, PATCH and PUT requests often return the modified entity. We can use that to perform manual cache invalidation. To do that, we need to define an
onQueryStarted function.

One way of doing the above is to perform an optimistic update. We assume that our API request will probably be successful, and we want to make the changes immediately. However, if our API request fails for some reason, we undo our changes.

12345678910111213141516171819202122232425262728293031323334353637import{createApi,fetchBaseQuery}from’@reduxjs/toolkit/query/react’;import Photo from’./photo’; export constapi=createApi({  // …  endpoints:(builder)=>({    // …    updatePhoto:builder.mutation<>

}>({      // …      asynconQueryStarted({id,data},{dispatch,queryFulfilled}){        constgetPhotoByIdPatch=dispatch(          api.util.updateQueryData(‘getPhotoById’,id,(currentPhotoValue)=>{            Object.assign(currentPhotoValue,data);          }),        );        constgetAllPhotosPatch=dispatch(          api.util.updateQueryData(‘getPhotos’,undefined,(photosList)=>{            constphotoIndex=photosList.findIndex((photo)=>photo.id===id);            if(photoIndex>-1){              constcurrentPhotoValue=photosList[photoIndex];              Object.assign(photosList[photoIndex],{                …currentPhotoValue,                …data,              });            }          }),        );        try{          await queryFulfilled;        }catch{          getPhotoByIdPatch.undo();          getAllPhotosPatch.undo();        }      },    }),  }),});

The second approach we can implement is the pessimistic update. Here, we wait for the request to be completed before modifying the cache.

1234567891011121314151617181920212223242526272829303132333435363738394041import{createApi,fetchBaseQuery}from’@reduxjs/toolkit/query/react’;import Photo from’./photo’; export constapi=createApi({  // …  endpoints:(builder)=>({    // …    updatePhoto:builder.mutation<>

}>({      // …      asynconQueryStarted({id},{dispatch,queryFulfilled}){        try{          const{data:updatedPost}=await queryFulfilled;           dispatch(            api.util.updateQueryData(              ‘getPhotoById’,              id,              (currentPhotoValue)=>{                Object.assign(currentPhotoValue,updatedPost);              },            ),          );          dispatch(            api.util.updateQueryData(‘getPhotos’,undefined,(photosList)=>{              constphotoIndex=photosList.findIndex(                (photo)=>photo.id===id,              );              if(photoIndex>-1){                constcurrentPhotoValue=photosList[photoIndex];                Object.assign(photosList[photoIndex],{                  …currentPhotoValue,                  …updatedPost,                });              }            }),          );        }catch{}      },    }),  }),});

Series

    • API with NestJS
    • Getting geeky with Git
    • JavaScript design patterns
    • JavaScript testing tutorial
    • Node.js TypeScript
    • React SSR with Next.js
    • Regex course
    • TypeScript Express tutorial
    • Webpack 4 course

Previous article
Redux middleware and how to use it with WebSockets

Home

Next article
Managing WebSockets with Redux Toolkit Query and TypeScript

©

Sign up for newsletter

 I want to receive the newsletter from wanago.io. I understand that my personal data will be processed according to the information in the privacy policy

Integrating errors in reducers and components

As we said before, error handling is not fun. We want to make it as simple as possible so we’re sure the error coverage is easy to increase across the application.

To provide this, we decided to go to a composable solution that can be used in reducers, action creators, and selectors.

If you’re not familiar with redux terms, here’s a TLDR (feel free to skip if you are):

  • reducers define how your state is mutated. Based on an action (think an event) and on the current state, they know how to calculate the next state.
  • action is similar to an event that is sent to the store to trigger a state change
  • action creators are functions that create actions
  • selectors are no more than getters to a store, enabling decoupling of the store from its users

The examples we’ve used are written in react, redux and reselect, but the practices and principles used are technology agnostic.

Defining the basics of our API

To start using the RTK Query, we need to use the
createApi function.

123456789101112131415161718192021222324252627282930import{createApi,fetchBaseQuery}from’@reduxjs/toolkit/query/react’;import Photo from’./photo’; export constapi=createApi({  reducerPath:’api’,  baseQuery:fetchBaseQuery({    baseUrl:process.env.REACT_APP_API_URL,  }),  endpoints:(builder)=>({    getPhotos:builder.query

({      query:()=>’photos’,    }),    getPhotoById:builder.query

({      query:(photoId:number)=>`photos/${photoId}`,    }),    updatePhoto:builder.mutation<>

}>({      query:({id,data})=>({        url:`photos/${id}`,        method:’PATCH’,        body:data,      }),    }),  }),}); exportconst{  useGetPhotosQuery,  useGetPhotoByIdQuery,  useUpdatePhotoMutation,}=api;

A few important things are happening above. First, we need to define the
reducerPath property to tell the Redux Toolkit where we want to keep all of the data from the API in our store.

We also need to tell the RTK how to make our API requests by providing the
baseQuery parameter. A common way is using the
fetchBaseQuery function, a wrapper around the native Fetch API. Even though the above is the solution suggested by the official documentation, RTK aims not to enforce it. So, for example, we can write our base query function that uses axios.

In the property called
endpoints, we specify a set of operations we want to perform with the API. We do that by using the
builder object. When we aim to retrieve the data, we should use the
builder.query function. When we want to alter the data on the server, we need to call the
builder.mutation method.

Both
builder.query and
builder.mutation uses generic types. For example, in
getPhotoById:builder.query

the
Photo is the type of data in the response. When getting the photo, the
number is the type of argument we want the user to pass. Since the
getPhotos query does not require any arguments, we must explicitly pass
void.

Defining the endpoints causes the React Toolkit to generate a set of hooks to interact with our API. We need to use TypeScript in version 4.1 or greater for the hooks to work correctly.

In our code snippet, we use the
REACT_APP_API_URL environment variable. Therefore, we need to add it to our
.env file for the url to be available.

Attaching the RTK Query to the store

The
createApi function creates a reducer and a middleware we need to attach to our existing Redux store.

If you want to know more about Redux Middleware, check out Redux middleware and how to use it with WebSockets

12345678910111213import{configureStore,ThunkAction,Action}from’@reduxjs/toolkit’;import{api}from’./api/api’;import{setupListeners}from’@reduxjs/toolkit/query’; export conststore=configureStore({  reducer:{    [api.reducerPath]:api.reducer,  },  middleware:(getDefaultMiddleware)=>    getDefaultMiddleware().concat(api.middleware),}); setupListeners(store.dispatch);

Above, the
setupListeners is a thing worth noting. We can set the
refetchOnFocus or the
refetchOnReconnect flags to true when using a query. They tell RTK Query to refetch the data when the application window regains focus, or the network connection is reestablished. For it to work, we need to listen to events such as:

  • visibilitychange,
  • focus,
  • online,
  • offline.

When we look under the hood, we can see that the
setupListeners function attaches callbacks to the above events. So as long as you don’t use the mentioned flags, you don’t need to use
setupListeners.

Future improvements

If we decide to add translation codes or to change the error structure that comes from the back-end, we know we will only have to change a single file.

Writing code that is easy to change is something we’re always aiming for at KI labs and xgeeks. Our habitats are fast-changing environments and recently bootstrapped companies that iterate fast. Reach out to us if this is an environment where you would thrive into!

How are you handling your errors in this kind of situation? Have you created something similar? Are you using something automated to deal with error states? How are you displaying them?

I’d love to hear if this solution fixes some of your problems, and if it doesn’t, why?

Reach me out via email or twitter

Thanks for reading

Alexandre Portela dos Santos – Ericeira, Portugal
Helping businesses with tech | Business and product enthusiast | Author
about me | twitter | github | linkedin | resume

Alexandre Portela dos Santos

  • MirageJS to increase developer productivity←
  • →Yes, TDD does apply to your use case

Before – the default way

Let’s create an HTTP request and write the error handling approach.

We will use redux-thunk as we believe they’re simpler to understand. If you’re not familiar with them: they are functions that produce side-effects and dispatch actions.

This is a default thunk, dispatching actions at the start of the request, on success and error.

import axios from”axios”constcreateArticle=(title, text)=> dispatch=> {dispatch({ type:”CREATE_ARTICLE_REQUEST”})return axios
.post(“/api/articles”,{ title, text }).then(successHandler).catch(error=> {// Deal with the errordispatch({ type:”CREATE_ARTICLE_ERROR”, error })})}

Then, in the reducer, the handling would be something like:

constreducer=(state, action)=> {switch(action.type){case”CREATE_ARTICLE_REQUEST”:return{…state,
isLoading:true,
error:null,// reseting the error}case”CREATE_ARTICLE_ERROR”:return{…state,
error: action.error,// setting the error}}}

And on your components, you would do something like the following to show the error to the user:

import{ useSelector }from”react-redux”constArticleListPage=()=> {const errorMessage =useSelector(state=> state.articles.error.errorMessage)return(

{errorMessage &&

There was an error:{errorMessage}

}{/* Cut for brevity */}

)}

We have identified some problems with this approach:

  • There is no enforced error structure. One developer can send error in the action where others can directly send error.errorMessage
  • The code to handle errors on reducers will be repeated for every request and can end up not respecting the structure too.
  • If the reducer, action creator or component change, it is very likely that the other 2 will break, they are coupled. Can’t access property ‘errorMessage of undefined I’m looking at you.
  • Error messages are shown in different formats across the application leading to a not so pleasant user experience.

Side by side

Before

before-changes

After

before-changes

In blue are the layers we’ve added.

Splitting the code

When using React Toolkit Query, we are expected to use the
createApi function just once. However, when our application uses a lot of different endpoints, defining them in one place can lead to a messy codebase. Fortunately, with RTK, we can split our endpoints into multiple additional files.

12345678910import{createApi,fetchBaseQuery}from’@reduxjs/toolkit/query/react’; export constapi=createApi({  reducerPath:’api’,  tagTypes:[‘Photos’],  baseQuery:fetchBaseQuery({    baseUrl:process.env.REACT_APP_API_URL,  }),  endpoints:()=>({}),});
1234567891011121314151617181920212223242526272829303132333435import{api}from’./api’;import Photo from’./photo’; constphotosApi=api.injectEndpoints({  endpoints:(builder)=>({    getPhotos:builder.query

({      query:()=>’photos’,      providesTags:[{type:’Photos’,id:’LIST’}],    }),    getPhotoById:builder.query

({      query:(photoId:number)=>`photos/${photoId}`,      providesTags:(result,error,id)=>[{type:’Photos’,id}],    }),    updatePhoto:builder.mutation<>

}>({      query:({id,data})=>({        url:`photos/${id}`,        method:’PATCH’,        body:data,      }),      invalidatesTags:(result)=>        result          ?[              {type:’Photos’,id:result.id},              {type:’Photos’,id:’LIST’},            ]          :[],    }),  }),}); exportconst{  useGetPhotosQuery,  useGetPhotoByIdQuery,  useUpdatePhotoMutation,}=photosApi;

Please notice that we’re still defining the
tagTypes array in the main API definition. An alternative to doing that is to use the
enhanceEndpoints function.

12345678import{api}from’./api’;import Photo from’./photo’; constapiWithTags=api.enhanceEndpoints({addTagTypes:[‘Photos’]}); constphotosApi=apiWithTags.injectEndpoints({  // …});

Summary

We’ve gone through all of the features we need to start using React Toolkit Query in this article. This included defining the API with code splitting, performing queries, and altering the cache with mutations. RTK Query proves to be a great tool that we can use for data fetching and caching. We can even use the Redux DevTools extension to view our cached data through the developer tools. It does a lot of the job for us and makes a lot of sense especially if we already use Redux in our application.

Subscribe
LoginNotify of new follow-up commentsnew replies to my commentsguest
Label
<текстареа id="wc-текстареа-0_0" name="wc_comment" class="wc_comment wpd-field"> {}[+]Name*Email*Websiteguest
Label
<текстареа id="wc-текстареа-wpdiscuzuniqueid" name="wc_comment" class="wc_comment wpd-field"> {}[+]Name*Email*Website3 Comments OldestNewestMost Voted Inline FeedbacksView all commentsLenz Weber
Lenz Weber

9 months ago

Great article!
One nitpick:

12345                constcurrentPhotoValue=photosList[photoIndex];                Object.assign(photosList[photoIndex],{                  …currentPhotoValue,                  …updatedPost,                });

is doing a bit much. You can either use Object.assign to update an existing object or create and assign a new object, both is not necessary. It behaves like an immer reducer here.
So either

1                Object.assign(photosList[photoIndex],updatedPost);

or

1234                photosList[photoIndex]={                  …currentPhotoValue,                  …updatedPost,                };

2ReplyVictor Olaoye
Victor Olaoye

8 months ago

When I tried to use the query hook in a component, it returns an error. In my case the data is an object so the error is ‘Object is of type unknown’

0ReplyNeil
Neil

5 months ago

Please share what the Photo type is please – import Photo from‘./photo’;

0ReplyFollow @wanago_io

Querying the data

The essential auto-generated hook related to queries is the
useQuery. Using it automatically fetches the data.

12345678910111213141516171819202122232425262728import React,{FunctionComponent}from’react’;import{useGetPhotoByIdQuery}from’./api/api’;import Loader from’./Loader’; interfaceProps{  photoId:number;} const PhotoCard:FunctionComponent

=({photoId})=>{  const{data,isLoading,isError}=useGetPhotoByIdQuery(photoId);   if(isLoading){    return;  }   if(isError||!data){    return

Something went wrong

;  }   return(    

      

{data.title}

      
    

  );}; export defaultPhotoCard;

The cache behavior

It is essential to understand that if we render the
PhotoCard multiple times with the same
photoId, the
useGetPhotoByIdQuery hook returns the result from the cache. The above happens as long as 60 seconds didn’t yet pass until the last component stopped using the
useGetPhotoByIdQuery hook with a given id.

60 seconds can be changed to another value using the
keepUnusedDataFor property.

We can alter this behavior in a few ways. For example, we can use the
pollingInterval property to allow the query to refresh automatically.

123const{data,isLoading,isError}=useGetPhotoByIdQuery(photoId,{  pollingInterval:60_000// 1 minute});

Above I’m using a numeric separator to make the number more readable.

We can also use the
refetchOnMountOrArgChange flag to skip the cached result.

123useGetPhotoByIdQuery(photoId,{  refetchOnMountOrArgChange:true});

Instead of
true we can also pass a number of seconds. In this case RTK refetches if enough time has passed.

RTK also gives us a way to manually request the latest data with the refetch function.

12345678910111213141516171819202122232425262728293031import React,{FunctionComponent}from’react’;import{useGetPhotoByIdQuery}from’./api/api’;import Loader from’./Loader’; interfaceProps{  photoId:number;} const PhotoCard:FunctionComponent

=({photoId})=>{  const{data,isLoading,isError,refetch}=useGetPhotoByIdQuery(photoId);   if(isLoading){    return;  }   if(isError||!data){    return

Something went wrong

;  }   return(    

            

{data.title}

      
    

  );}; export defaultPhotoCard;

We can also use the
refetchOnFocus and
refetchOnReconnect flags as mentioned before in this article.

Источники

  • https://ru.hexlet.io/courses/js-redux-toolkit/lessons/async-thunks/theory_unit
  • https://wanago.io/2021/12/27/redux-toolkit-query-typescript/
  • https://alexandrempsantos.com/sane-error-handling-react-redux/
[свернуть]
Решите Вашу проблему!


×
Adblock
detector