Search code examples
javascriptreact-nativereduxredux-toolkitrtk-query

React Native access token expiration/renewal upon 403 response code from a RTK Query API


I am calling an API defined using RTK Query, within a React Native + Redux Toolkit + Expo app. This is secured with an authentication / authorization system in place i.e. access token (short expiration) and refresh token (longer expiration).

I would like to avoid checking any access token expiration claim (I've seen people suggesting to use a Redux middleware). Rather, if possible, I'd like to trigger the access token renewal when the API being requested returns a 403 response code, i.e. when the access token is expired.

This is the code calling the API:

const SearchResults = () => {

  // get the SearchForm fields and pass them as the request body
  const { fields, updateField } = useUpdateFields();

  // query the RTKQ service
  const { data, isLoading, isSuccess, isError, error } =
    useGetDataQuery(fields);

  return ( ... )

the RTK Query API is defined as follows:

import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
import * as SecureStore from "expo-secure-store";
import { baseUrl } from "~/env";


export const api = createApi({
  reducerPath: "api",
  baseQuery: fetchBaseQuery({
    baseUrl: baseUrl,
    prepareHeaders: async (headers, { getState }) => {
      // retrieve the access_token from the Expo SecureStore
      const access_token = await SecureStore.getItemAsync("access_token");
      if (access_token) {
        headers.set("Authorization", `Bearer ${access_token}`);
        headers.set("Content-Type", "application/json");
      }
      return headers;
    },
  }),
  endpoints: (builder) => ({
    getData: builder.query({
      // body holds the fields passed during the call
      query: (body) => {
        return {
          url: "/data",
          method: "POST",
          body: body,
        };
      },
    }),
  }),
});

export const { useGetDataQuery } = api;

I understand that when the API returns isError = true and error = something 403 I need to renew the access token within the Expo SecureStore (and there's a function already in place for that). However I have no idea about how can I query the RTKQ API again, on the fly, when it returns a 403 response code, and virtually going unnoticed by the user.

Can someone please point me in the right direction?


Solution

  • I got the hang of it, massive thanks to @phry! I don't know how I could have missed this example from RTKQ docs but I'm a n00b for a reason after all.

    This being said, here's how to refactor the RTKQ api to renew the access token on the fly, in case some other react native beginner ever has this problem. Hopefully this is a reasonable way of doing this

    import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
    import * as SecureStore from "expo-secure-store";
    import { baseUrl } from "~/env";
    import { renewAccessToken } from "~/utils/auth";
    
    // fetchBaseQuery logic is unchanged, moved out of createApi for readability
    const baseQuery = fetchBaseQuery({
      baseUrl: baseUrl,
      prepareHeaders: async (headers, { getState }) => {
        // retrieve the access_token from the Expo SecureStore
        const access_token = await SecureStore.getItemAsync("access_token");
        if (access_token) {
          headers.set("Authorization", `Bearer ${access_token}`);
          headers.set("Content-Type", "application/json");
        }
        return headers;
      },
    });
    
    const baseQueryWithReauth = async (args, api) => {
      let result = await baseQuery(args, api);
      if (result.error) {
        /* try to get a new token if the main query fails: renewAccessToken replaces
        the access token in the SecureStore and returns a response code */
        const refreshResult = await renewAccessToken();
        if (refreshResult === 200) {
          // then, retry the initial query on the fly
          result = await baseQuery(args, api);
        }
      }
      return result;
    };
    
    export const apiToQuery = createApi({
      reducerPath: "apiToQuery",
      baseQuery: baseQueryWithReauth,
      endpoints: (builder) => ({
        getData: builder.query({
          // body holds the fields passed during the call
          query: (body) => {
            return {
              url: "/data",
              method: "POST",
              body: body,
            };
          },
        }),
      }),
    });
    
    export const { useGetDataQuery } = apiToQuery;