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'],})
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
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 defined
invalidatesTags
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 the
listGames
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 the
userId
. It's simpler ifuserId
is sent through as part of thepayload
. 😬 - The
useListGamesQuery
,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'
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});
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
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)
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}</>)}
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>)}
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()
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)})})})
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)

- LocationKuala Lumpur, Malaysia
- EducationUniversity Technology Malaysia
- WorkQumon 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!

- LocationLondon
- WorkSoftware Engineer
- Joined
You're welcome! Good luck with RTK Query. :)
For further actions, you may consider blocking this person and/orreporting abuse