Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Bionic Julia
Bionic Julia

Posted on • Originally published atbionicjulia.com on

     

Implementing RTK Query in a React Native App

I had a bit of downtime in between feature builds last week, so I decided to do a time-boxed investigation into how we might improve API querying throughout our React Native app.

We currently use Axios to help with structuring our API calls. Whilst it does what we need it to, there are definitely ways in which we can improve our querying (e.g. through caching), in addition to DRY-ing up our code (e.g. reducing the need to constantly be setting up local states for things like loading and error statuses).

As a starting point, I had heard lots of good things about React Query, but also the newer Redux Toolkit (RTK) Query. Upon doing a quick read, I confirmed both could do the job I needed, but I ultimately decided on RTK Query for a number of reasons:

  • We already use Redux Toolkit to help with state management in our app and RTK Query is included in the same package. This meant I didn't have to introduce yet another package to our code base.
  • The caching mechanic seemed more straight-forward and easy to understand, to me.
  • We can use the Redux DevTools to monitor the query lifecycle.
  • Auto-generated React hooks are a nice touch.

If you want to read more about the differences between RTK Query and React Query, check out the docshere.

Decision made, what I wanted to do next was to:

  • See how difficult it was to get RTK Query set up and running in our code base;
  • Create a proof of concept (PoC) PR to present to my team mates, on how we'd define GET, POST and PATCH endpoints, and how these would be hooked up to the UI; and
  • See how easy it is to write Jest tests.

Despite RTK Query being released fairly recently (I think around June 2021?), I found the documentation to be substantial and easy to understand, with the set up process being pretty straightforward. You can get the full instructions from thedocs, but I've included some code here for completeness.

Step 1: Set up your API

In my case, I needed to get the user's auth token, to then append to the headers when making an API call. Depending on how auth works for your endpoints, you'll probably need to change this.

// @app/api/rtkApi.tsimportappConfigfrom'@app/appConfig'import{AsyncStorageService}from'@app/AsyncStorageService'import{createApi,fetchBaseQuery}from'@reduxjs/toolkit/query/react'exportconstapi=createApi({baseQuery:fetchBaseQuery({baseUrl:appConfig.apiBase,// e.g. https://yourapi.comprepareHeaders:async(headers)=>{constuser=awaitAsyncStorageService.getStoredData()consthasUser=!!user&&!!user!.userTokenif(hasUser){headers.set('Authorization',`Token${user.userToken}`)}headers.set('Content-Type','application/json')returnheaders},}),endpoints:()=>({}),reducerPath:'api',tagTypes:['Game'],})
Enter fullscreen modeExit fullscreen mode

Step 2: Define GET, POST and PATCH endpoints

You might have realised that I left the endpoints blank above. This is because I wanted to inject my endpoints at runtime, after the initial API slice has been defined. My main reason for this was extensibility, as our app has lots of endpoints to call. I wanted my PoC PR to show how we would structure the code realistically.This page has more information on this.

// @app/api/gameApi.tsimport{api}from'@app/api/rtkApi'import{TGameRequest}from'@app/game/GameRequest'exportconstgameApi=api.injectEndpoints({endpoints:(build)=>({listGames:build.query<TGameRequest[],void>({providesTags:['Game'],query:()=>'/games/',}),addGame:build.mutation<string,{payload:Partial<TGameRequest>;userId:string}>({invalidatesTags:['Game'],query:({userId,payload})=>({body:{user:userId,...payload,},method:'POST',url:'/games/',}),}),updateGame:build.mutation<string,{payload:Partial<TGameRequest>;userId:string}>({invalidatesTags:['Game'],query:({userId,payload})=>({body:{user:userId,...payload,},method:'PATCH',url:`/games/${payload.id}/`,}),}),}),overrideExisting:false,})exportconst{useListGamesQuery,useAddGameMutation,useUpdateGameMutation}=gameApi
Enter fullscreen modeExit fullscreen mode

Some things to point out:

  • listGames is a GET endpoint. I definedprovidesTags asGame. Note that theGame tag has to be defined intagTypes within thecreateApi function.
  • For the POST and PATCH endpoints, I definedinvalidatesTags also asGame. What this means is that whenever I call the POST and PATCH endpoints successfully, thelistGames data cache will be invalidated and the GET endpoint will be called again automatically to refresh the data.
  • I don't need to call thelistGames endpoint with any arguments. This is why you seevoid as the second Typescript argument forbuild.query.
  • Because of how the rest of my code base is set up, my payload excludes theuserId. It's simpler ifuserId is sent through as part of thepayload. 😬
  • TheuseListGamesQuery,useAddGameMutation anduseUpdateGameMutation hooks are all auto-generated. The names of these hooks are based on the names of the endpoints. GET endpoints end withQuery, whereas POST, PATCH (and other endpoints that mutate data) end withMutation. You'll need to be sure you've imported from the package'sreact folder if you want this feature:
import{createApi,fetchBaseQuery}from'@reduxjs/toolkit/query/react'
Enter fullscreen modeExit fullscreen mode

Step 3: Complete the remaining RTK Query setup

Add the name of your API to the reducer.

// @app/state/root.tsimport{api}from'@app/api/rtkApi';...exportdefaultcombineReducers({[api.reducerPath]:api.reducer,// remaining reducers});
Enter fullscreen modeExit fullscreen mode

Add the required middleware and setup listeners to your store.

// @app/state/store.tsimport{api}from'@app/core/api/rtkApi'importrootReducerfrom'@app/core/state/root'importAsyncStoragefrom'@react-native-async-storage/async-storage'import{configureStore,getDefaultMiddleware}from'@reduxjs/toolkit'import{setupListeners}from'@reduxjs/toolkit/query'import{persistReducer,persistStore}from'redux-persist'constpersistConfig={key:'root',storage:AsyncStorage,}constpersistedReducer=persistReducer(persistConfig,rootReducer)conststore=configureStore({devTools:__DEV__,middleware:getDefaultMiddleware({serializableCheck:false,}).concat(api.middleware),// NOTE this additionreducer:persistedReducer,})exportconstpersistor=persistStore(store)setupListeners(store.dispatch)// NOTE this additionexportdefaultstore
Enter fullscreen modeExit fullscreen mode

Step 4: Use the auto-generated hooks in your UI components

I've simplified my UI screen examples as much as possible, to focus on the core RTK Query features. Here's an example of what my list screen looks like, where I'm calling thelistGames endpoint.

Where I previously would have useduseState hooks to monitor local state for error and loading statuses (in order to display error messages or loading spinners), this is now provided by theuseListGamesQuery hook. i.e. this sort of thing is no longer needed:

const[error,setError]=React.useState<string>()const[loading,setLoading]=React.useState<boolean>(true)
Enter fullscreen modeExit fullscreen mode

The other nice thing is that the hook just runs and updates itself automatically - you don't have to wrap it in some kind ofuseEffect hook to be called on the screen mounting or updating.

// ListScreen componentimport{useListGamesQuery}from'@app/api/gameApi';// ...other importsexportfunctionListScreen(props:TProps){const{data,isError,isLoading}=useListGamesQuery();return(<>{isLoading?(<LoadingSpinner/>):({data.map((item)=>{// ... render your data})){isError?<ErrorText>Oops, something went wrong</ErrorText>:null}</>)}
Enter fullscreen modeExit fullscreen mode

Moving on to the POST and PATCH endpoints - let's say they're both called in the same screen (theAddUpdateScreen). There's a small difference in how you use mutation hooks, as they don't "run automatically" like the query hooks do. You will instead need to specifically call them at the right point. In the example below, this was on the user clicking a submit button.

// AddUpdateScreen componentimport{useAddGamesMutation,useUpdateGamesMutation}from'@app/api/gameApi';// ...other importsexportfunctionAddUpdateScreen(props:TProps){const[addGames,{isLoading:addRequestSubmitting}]=useAddGamesMutation();const[updateGames,{isLoading:updateRequestSubmitting}]=useUpdateGamesMutation();constonSubmit=async(values:IGameData)=>{constpayload=// sanitise the values receivedif(!addRequestSubmitting&&!updateRequestSubmitting){if(existingGame){awaitupdateGames({userId:props.userId,payload,});}else{awaitaddGames({userId:props.userId,payload,});}}};return(<ButtononPress={onSubmit}>      Submit</Button>)}
Enter fullscreen modeExit fullscreen mode

You'll notice that I renamed theisLoading destructured prop from the 2 mutation hooks as I wanted to refer to both, in the screen logic. What theonSubmit function is trying to do is to first create the payload, and then, assuming a submission is not already happening, to either call the POST or PATCH endpoint.

RTK Query hooks comes with a host of other return values likeisFetching andisError, so check out the docs forqueries andmutations to see what's available.

Step 5: Testing

There's not a lot of information in the official docs at the moment, on how to test RTK Query. If you're interested in actually testing that your API endpoints have been set up correctly, and that your auto-generated hooks are working as expected, check out thisMedium article which I found super helpful.

The other thing I specifically wanted to test was whether the UI was rendering components correctly, depending on the data that was coming back from the query. This was easily achieved by mocking the response from the hook. I used thejest-fetch-mock library to help with this. Once you've got that installed, be sure to enable it in your Jest setup file.

// In my Jest setup filerequire('jest-fetch-mock').enableMocks()
Enter fullscreen modeExit fullscreen mode

This is an example of what my tests looked like (I'm going to assume you're already testing your Redux store and have something likeredux-mock-store set up). In this case, for each game returned in my GET response, I wanted to check that aGameRow is rendered.

// In my test fileimport*ashooksfrom'@app/api/gameApi'importinitialStatefrom'@app/store/initialState'import{GameRow}from'@app/components/GameRow'import{getDefaultMiddleware}from'@reduxjs/toolkit'importcreateMockStorefrom'redux-mock-store'constmiddlewares=getDefaultMiddleware()constmockStore=createMockStore(middlewares)conststore=mockStore(initialState)// define your initial state as neededconstRESPONSE_WITH_TWO_GAMES=[// define what your expected response should look like i.e. of type TGameRequest as defined in your API endpoint]describe('ListScreen tests',()=>{it('renders 2 rows when 2 games exist',async()=>{jest.spyOn(hooks,'useListGamesQuery').mockReturnValue({data:[RESPONSE_WITH_TWO_GAMES],isError:false,isLoading:false})constelement=(<ReduxProviderstore={store}><ListScreen/></ReduxProvider>)constinstance=renderer.create(element).rootawaitact(async()=>{expect(instance.findAllByType(GameRow).length).toBe(2)})})it('renders 0 doses when 0 doses exist',async()=>{jest.spyOn(hooks,'useListGamesQuery').mockReturnValue({data:[],isError:false,isLoading:false})constelement=(<ReduxProviderstore={store}><ListScreen/></ReduxProvider>)constinstance=renderer.create(element).rootawaitact(async()=>{expect(instance.findAllByType(GameRow).length).toBe(0)})})})
Enter fullscreen modeExit fullscreen mode

Conclusion

All in all, I really liked working with RTK Query and found it fairly easy to set up. The Redux DevTools were hugely helpful in helping me understand the lifecycle of a query and how caching works, so I'd definitely recommend you install and activate the dev tools if you haven't already.

As we were not using Axios for particularly complex functions, I decided to do away with Axios completely (though you canuse both in tandem if you so prefer). There are also a number of other RTK Query features that I haven't yet had the chance to try out, likeauto-generating API endpoints from OpenAPI schemas andglobal error interception and handling.

I was watching React Conf 2021 a couple of days ago, where they featured React Suspense heavily and mentioned that they're currently working with tools with React Query to make it simple for devs to integrate Suspense. I'm guessing that RTK Query must also be on that list and am really intrigued to see how RTK Query evolves with this. 😄

Any comments / feedback for me? Catch me onhttps://bionicjulia.com,Twitter orInstagram.

Top comments(2)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
wenlong12345 profile image
Wen Long
Android Dev and Kotlin Lover, Currently learning React Native and planning to do more on Mobile Dev 🚀
  • Location
    Kuala Lumpur, Malaysia
  • Education
    University Technology Malaysia
  • Work
    Qumon Intelligence
  • Joined

I am new on React world and I started with React Native. Most of the React idea are coming from reactjs and I was worried about implementing them into mobile app. Thanks for the article on RTK query and I gonna give it a try!

CollapseExpand
 
bionicjulia profile image
Bionic Julia
Formerly a banker and tech startup founder. Now on my 3rd career as a full stack software engineer (with a front end focus). Posts on tech and things learnt on my programming journey. 👩🏻‍💻
  • Location
    London
  • Work
    Software Engineer
  • Joined

You're welcome! Good luck with RTK Query. :)

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Formerly a banker and tech startup founder. Now on my 3rd career as a full stack software engineer (with a front end focus). Posts on tech and things learnt on my programming journey. 👩🏻‍💻
  • Location
    London
  • Work
    Software Engineer
  • Joined

More fromBionic Julia

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp