Have you ever wanted to use Redux with the functionality provided by React Query? You can now use the Redux Toolkit and its latest addition: RTK Query.

RTK Query is an advanced data retrieval and client-side caching tool. Its functionality is similar to React Query, but it has the benefit of integrating directly with Redux. For API interactions, developers typically use asynchronous middleware modules like Thunk when using Redux. This approach limits flexibility; So React developers now have the Redux team’s official alternative, which covers all the advanced requirements for today’s client/server communication.

This article demonstrated how to use RTK queries in a real-world scenario, with each step including a link to submit differences to highlight the added functionality. A link to the complete code base appears at the end.

Templates and configurations

Project initialization differences

First, we need to create a project. This is done using the Create React App (CRA) template for TypeScript and Redux:

npx create-react-app . --template redux-typescript
Copy the code

It has several dependencies that we will need, most notably:

  • Redux toolkit and RTK queries
  • Material interface
  • Los dash
  • F Mick
  • Reaction router

It also includes the ability to provide custom configurations for WebPack. Typically, CRA does not support such capabilities unless you opt out.

Initialize the

A safer approach than pop-ups is to use something that can modify the configuration, especially if the changes are small. The template uses react-app-rewired and custom-cra to implement this functionality to introduce a custom Babel configuration:

const plugins = [
 [
   'babel-plugin-import',
   {
     'libraryName': '@material-ui/core'.'libraryDirectory': 'esm'.'camel2DashComponentName': false
   },
   'core'
 ],
 [
   'babel-plugin-import',
   {
     'libraryName': '@material-ui/icons'.'libraryDirectory': 'esm'.'camel2DashComponentName': false
   },
   'icons'
 ],
 [
   'babel-plugin-import',
   {
     "libraryName": "lodash"."libraryDirectory": ""."camel2DashComponentName": false.// default: true}]].module.exports = { plugins };
Copy the code

This makes the developer experience better by allowing imports. Such as:

import { omit } from 'lodash';
import { Box } from '@material-ui/core';
Copy the code

Such imports usually result in an increase in package size, but with the override functionality we configured, they do the following:

import omit from 'lodash/omit';
import Box from '@material-ui/core/Box';
Copy the code

configuration

Redux Settings difference

Since the entire app is based on Redux, after initialization we need to set the Store configuration:

import { combineReducers, configureStore } from '@reduxjs/toolkit';
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import { Reducer } from 'redux';
import {
 FLUSH,
 PAUSE,
 PERSIST,
 persistStore,
 PURGE,
 REGISTER,
 REHYDRATE
} from 'redux-persist';
import { RESET_STATE_ACTION_TYPE } from './actions/resetState';
import { unauthenticatedMiddleware } from './middleware/unauthenticatedMiddleware';

const reducers = {};

const combinedReducer = combineReducers<typeof reducers>(reducers);

export const rootReducer: Reducer<RootState> = (state, action) = > {
 if (action.type === RESET_STATE_ACTION_TYPE) {
   state = {} as RootState;
 }

 return combinedReducer(state, action);
};

export const store = configureStore({
 reducer: rootReducer,
 middleware: (getDefaultMiddleware) = >
   getDefaultMiddleware({
     serializableCheck: {
       ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER]
     }
   }).concat([
     unauthenticatedMiddleware
   ]),
});

export const persistor = persistStore(store);

export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof combinedReducer>;
export const useTypedDispatch = () = > useDispatch<AppDispatch>();
export const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector;
Copy the code

In addition to the standard store configuration, we’ll add configuration for the global reset state operations that come in handy in the real world, including the application itself and tests:

import { createAction } from '@reduxjs/toolkit';

export const RESET_STATE_ACTION_TYPE = 'resetState';
export const resetStateAction = createAction(
 RESET_STATE_ACTION_TYPE,
 () = > {
   return { payload: null}; });Copy the code

Next, we’ll add custom middleware for handling 401 responses by simply cleaning up the store:

import { isRejectedWithValue, Middleware } from '@reduxjs/toolkit';
import { resetStateAction } from '.. /actions/resetState';

export const unauthenticatedMiddleware: Middleware = ({ dispatch }) = > (next) = > (action) = > {
 if (isRejectedWithValue(action) && action.payload.status === 401) {
   dispatch(resetStateAction());
 }

 return next(action);
};
Copy the code

So far so good. We have created the template and configured Redux. Now let’s add some functionality.

validation

Retrieves access token differences

For simplicity, authentication is divided into three steps:

  • Add API definitions to retrieve access tokens
  • Add a component to handle the GitHub Web authentication process
  • Authentication is done by providing utility components that provide the entire application to the user

In this step, we added the ability to retrieve access tokens.

The RTK query idea dictates that all API definitions appear in one place, which is handy when dealing with enterprise-class applications with multiple endpoints. In enterprise applications, it’s much easier to think about integrating apis and client caches when everything is in one place.

RTK queries have tools to automatically generate API definitions using the OpenAPI standard or GraphQL. These tools are still in their infancy, but are under active development. In addition, the library is designed to provide a great developer experience with TypeScript, which is increasingly becoming the choice for enterprise applications because of its ability to improve maintainability.

In our case, the definition will be located under the API folder. Now we just need this:

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { AuthResponse } from './types';

export const AUTH_API_REDUCER_KEY = 'authApi';
export const authApi = createApi({
 reducerPath: AUTH_API_REDUCER_KEY,
 baseQuery: fetchBaseQuery({
   baseUrl: 'https://tp-auth.herokuapp.com',}).endpoints: (builder) = > ({
   getAccessToken: builder.query<AuthResponse, string>({
     query: (code) = > {
       return ({
         url: 'github/access_token'.method: 'POST'.body: { code } }); }}),})});Copy the code

GitHub authentication is provided through an open source authentication server, which is hosted separately on Heroku due to GitHub API requirements.

Authentication server

Although not required for this sample project, readers who want to host their own copy of the authentication server need:

  1. Create an OAuth application on GitHub to generate their own client ID and key.
  2. By environment variablesGITHUB_CLIENT_IDandGITHUB_SECRET.
  3. replacebaseUrlThe authentication endpoint value in the API definition above.
  4. On the React side, replaceclient_idParameters in the next code example.

The next step is to add components that use this API. Due to GitHub Web application flow requirements, we need a login component responsible for redirecting to GitHub:

import { Box, Container, Grid, Link, Typography } from '@material-ui/core';
import GitHubIcon from '@material-ui/icons/GitHub';
import React from 'react';

const Login = () = > {
 return (
   <Container maxWidth={false}>
     <Box height="100vh" textAlign="center" clone>
       <Grid container spacing={3} justify="center" alignItems="center">
         <Grid item xs="auto">
           <Typography variant="h5" component="h1" gutterBottom>
             Log in via Github
           </Typography>
           <Link
             href={`https://github.com/login/oauth/authorize?client_id=b1bd2dfb1d172d1f1589`}
             color="textPrimary"
             data-testid="login-link"
             aria-label="Login Link"
           >
             <GitHubIcon fontSize="large"/>
           </Link>
         </Grid>
       </Grid>
     </Box>
   </Container>
 );
};

export default Login;
Copy the code

Once GitHub redirects back to our application, we’ll need a route to process the code and access_token retrieves it based on it:

import React, { useEffect } from 'react';
import { Redirect } from 'react-router';
import { StringParam, useQueryParam } from 'use-query-params';
import { authApi } from '.. /.. /.. /.. /api/auth/api';
import FullscreenProgress
 from '.. /.. /.. /.. /shared/components/FullscreenProgress/FullscreenProgress';
import { useTypedDispatch } from '.. /.. /.. /.. /shared/redux/store';
import { authSlice } from '.. /.. /slice';

const OAuth = () = > {
 const dispatch = useTypedDispatch();
 const [code] = useQueryParam('code', StringParam);
 constaccessTokenQueryResult = authApi.endpoints.getAccessToken.useQuery( code! , {skip: !code
   }
 );
 const { data } = accessTokenQueryResult;
 constaccessToken = data? .access_token; useEffect(() = > {
   if(! accessToken)return;

   dispatch(authSlice.actions.updateAccessToken(accessToken));
 }, [dispatch, accessToken]);
Copy the code

If you’ve ever used React Query, the mechanism for interacting with the API is similar to RTK Query. Thanks to the Redux integration, this provides some neat functionality that we will observe as we implement other functionality. Access_token However, we still need to manually save it in the store via the dispatch operation:

dispatch(authSlice.actions.updateAccessToken(accessToken));
Copy the code

We did this to be able to preserve the token between page reloads. For persistence and the ability to dispatch actions, we need to define a storage configuration for our authentication functionality.

By convention, the Redux Toolkit calls these slices:

import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import { AuthState } from './types';

const initialState: AuthState = {};

export const authSlice = createSlice({
 name: 'authSlice',
 initialState,
 reducers: {
   updateAccessToken(state, action: PayloadAction<string | undefined>){ state.accessToken = action.payload; ,}}});export const authReducer = persistReducer({
 key: 'rtk:auth',
 storage,
 whitelist: ['accessToken']
}, authSlice.reducer);
Copy the code

There is one more requirement for the previous code to work. Each API must be provided as a reducer of the Store configuration, each API has its own middleware, and you must include:

import { combineReducers, configureStore } from '@reduxjs/toolkit';
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import { Reducer } from 'redux';
import {
 FLUSH,
 PAUSE,
 PERSIST,
 persistStore,
 PURGE,
 REGISTER,
 REHYDRATE
} from 'redux-persist';
import { AUTH_API_REDUCER_KEY, authApi } from '.. /.. /api/auth/api';
import { authReducer, authSlice } from '.. /.. /features/auth/slice';
import { RESET_STATE_ACTION_TYPE } from './actions/resetState';
import { unauthenticatedMiddleware } from './middleware/unauthenticatedMiddleware';

const reducers = {
 [authSlice.name]: authReducer,
 [AUTH_API_REDUCER_KEY]: authApi.reducer,
};

const combinedReducer = combineReducers<typeof reducers>(reducers);

export const rootReducer: Reducer<RootState> = (state, action) = > {
 if (action.type === RESET_STATE_ACTION_TYPE) {
   state = {} as RootState;
 }

 return combinedReducer(state, action);
};

export const store = configureStore({
 reducer: rootReducer,
 middleware: (getDefaultMiddleware) = >
   getDefaultMiddleware({
     serializableCheck: {
       ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER]
     }
   }).concat([
     unauthenticatedMiddleware,
     authApi.middleware
   ]),
});

export const persistor = persistStore(store);

export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof combinedReducer>;
export const useTypedDispatch = () = > useDispatch<AppDispatch>();
export const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector;
Copy the code

That’s it! Now that our application is retrieving access_Token, we are ready to add more authentication capabilities on top of it.

To complete the certification

Complete the authentication difference

The next list of authentication features includes:

  • The ability to retrieve users from the GitHub API and provide them to the rest of the application.
  • The utility has routes that are accessible only when authenticated or viewed as a visitor.

To add the ability to retrieve users, we need some API templates. Unlike the authentication API, the GitHub API needs to be able to retrieve the access token from our Redux store and apply it to each request as an authorization header.

In RTK queries implemented by creating custom base queries:

import { RequestOptions } from '@octokit/types/dist-types/RequestOptions';
import { BaseQueryFn } from '@reduxjs/toolkit/query/react';
import axios, { AxiosError } from 'axios';
import { omit } from 'lodash';
import { RootState } from '.. /.. /shared/redux/store';
import { wrapResponseWithLink } from './utils';

const githubAxiosInstance = axios.create({
 baseURL: 'https://api.github.com'.headers: {
   accept: `application/vnd.github.v3+json`}});const axiosBaseQuery = (): BaseQueryFn<RequestOptions> => async (
 requestOpts,
 { getState }
) => {
 try {
   const token = (getState() as RootState).authSlice.accessToken;
   const result = awaitgithubAxiosInstance({ ... requestOpts,headers: {
       ...(omit(requestOpts.headers, ['user-agent')),Authorization: `Bearer ${token}`}});return { data: wrapResponseWithLink(result.data, result.headers.link) };
 } catch (axiosError) {
   const err = axiosError as AxiosError;
   return { error: { status: err.response? .status,data: err.response?.data } };
 }
};

export const githubBaseQuery = axiosBaseQuery();
Copy the code

I’m using Axios here, but I can use other clients as well.

The next step is to define an API for retrieving user information from GitHub:

import { endpoint } from '@octokit/endpoint';
import { createApi } from '@reduxjs/toolkit/query/react';
import { githubBaseQuery } from '.. /index';
import { ResponseWithLink } from '.. /types';
import { User } from './types';

export const USER_API_REDUCER_KEY = 'userApi';
export const userApi = createApi({
 reducerPath: USER_API_REDUCER_KEY,
 baseQuery: githubBaseQuery,
 endpoints: (builder) = > ({
   getUser: builder.query<ResponseWithLink<User>, null> ({query: () = > {
       return endpoint('GET /user'); }}),})});Copy the code

We use our custom base query here, which means that the userApi will contain an Authorization header for each request in the scope. Let’s adjust the main store configuration so that the API is available:

import { combineReducers, configureStore } from '@reduxjs/toolkit';
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import { Reducer } from 'redux';
import {
 FLUSH,
 PAUSE,
 PERSIST,
 persistStore,
 PURGE,
 REGISTER,
 REHYDRATE
} from 'redux-persist';
import { AUTH_API_REDUCER_KEY, authApi } from '.. /.. /api/auth/api';
import { USER_API_REDUCER_KEY, userApi } from '.. /.. /api/github/user/api';
import { authReducer, authSlice } from '.. /.. /features/auth/slice';
import { RESET_STATE_ACTION_TYPE } from './actions/resetState';
import { unauthenticatedMiddleware } from './middleware/unauthenticatedMiddleware';

const reducers = {
 [authSlice.name]: authReducer,
 [AUTH_API_REDUCER_KEY]: authApi.reducer,
 [USER_API_REDUCER_KEY]: userApi.reducer,
};

const combinedReducer = combineReducers<typeof reducers>(reducers);

export const rootReducer: Reducer<RootState> = (state, action) = > {
 if (action.type === RESET_STATE_ACTION_TYPE) {
   state = {} as RootState;
 }

 return combinedReducer(state, action);
};

export const store = configureStore({
 reducer: rootReducer,
 middleware: (getDefaultMiddleware) = >
   getDefaultMiddleware({
     serializableCheck: {
       ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER]
     }
   }).concat([
     unauthenticatedMiddleware,
     authApi.middleware,
     userApi.middleware
   ]),
});

export const persistor = persistStore(store);

export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof combinedReducer>;
export const useTypedDispatch = () = > useDispatch<AppDispatch>();
export const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector;
Copy the code

Next, we need to call this API before rendering our application. For simplicity, let’s work in a way similar to how the parsing function works for Angular routes, so that nothing is rendered until we get the user information.

By providing some UI up front, user absences can also be handled in a more subtle way so that the user gets the first meaningful render more quickly. This requires a lot more thought and work, and should definitely be addressed in a production-ready application.

To do this, we need to define a middleware component:

import React, { FC } from 'react';
import { userApi } from '.. /.. /.. /.. /api/github/user/api';
import FullscreenProgress
 from '.. /.. /.. /.. /shared/components/FullscreenProgress/FullscreenProgress';
import { RootState, useTypedSelector } from '.. /.. /.. /.. /shared/redux/store';
import { useAuthUser } from '.. /.. /hooks/useAuthUser';

const UserMiddleware: FC = ({ children }) = > {
 const accessToken = useTypedSelector(
   (state: RootState) = > state.authSlice.accessToken
 );
 const user = useAuthUser();

 userApi.endpoints.getUser.useQuery(null, {
   skip: !accessToken
 });

 if(! user && accessToken) {return (
     <FullscreenProgress/>
   );
 }

 return children as React.ReactElement;
};

export default UserMiddleware;
Copy the code

It’s easy to do that. It interacts with the GitHub API to get user information and does not render subitems until the response is available. Now, if we wrap application functionality with this component, we know that user information will be parsed before any other rendering:

import { CssBaseline } from '@material-ui/core';
import React from 'react';
import { Provider } from 'react-redux';
import { BrowserRouter as Router, Route, } from 'react-router-dom';
import { PersistGate } from 'redux-persist/integration/react';
import { QueryParamProvider } from 'use-query-params';
import Auth from './features/auth/Auth';
import UserMiddleware
 from './features/auth/components/UserMiddleware/UserMiddleware';
import './index.css';
import FullscreenProgress
 from './shared/components/FullscreenProgress/FullscreenProgress';
import { persistor, store } from './shared/redux/store';

const App = () = > {
 return (
   <Provider store={store}>
     <PersistGate loading={<FullscreenProgress/>} persistor={persistor}>
       <Router>
         <QueryParamProvider ReactRouterRoute={Route}>
           <CssBaseline/>
           <UserMiddleware>
             <Auth/>
           </UserMiddleware>
         </QueryParamProvider>
       </Router>
     </PersistGate>
   </Provider>
 );
};

export default App;
Copy the code

Let’s move on to the most fashionable part. We can now get user information anywhere in the application, even if we don’t use access_token like this.

How to? Create a simple custom React Hook for it:

import { userApi } from '.. /.. /.. /api/github/user/api';
import { User } from '.. /.. /.. /api/github/user/types';

export const useAuthUser = (): User | undefined= > {
 const state = userApi.endpoints.getUser.useQueryState(null);
 returnstate.data? .response; };Copy the code

The RTK query useQueryState provides options for each endpoint, which enables us to retrieve the current state of that endpoint.

Why is this so important and useful? Because we don’t have to write a lot of overhead to manage the code. As a bonus, we split the API/ client data right out of the box in Redux.

Using RTK queries saves trouble. By combining data retrieval with state management, RTK Query Bridges the gap that would have existed even if we had used React Query. (With React Query, the retrieved data must be accessed by unrelated components at the UI layer.)

As a final step, we define a standard custom routing component that uses this hook to determine if the route should be rendered:

import React, { FC } from 'react';
import { Redirect, Route, RouteProps } from 'react-router';
import { useAuthUser } from '.. /.. /hooks/useAuthUser';

exporttype AuthenticatedRouteProps = { onlyPublic? : boolean; } & RouteProps;const AuthenticatedRoute: FC<AuthenticatedRouteProps> = ({
 children,
 onlyPublic = false. routeProps }) = > {
 const user = useAuthUser();

 return (
   <Route
     {. routeProps}
     render={({ location}) = >{ if (onlyPublic) { return ! user ? ( children ) : (<Redirect
             to={{
               pathname: '/',
               state: { from: location }
             }}
           />
         );
       }

       return user ? (
         children
       ) : (
         <Redirect
           to={{
             pathname: '/login',
             state: { from: location }
           }}
         />); }} / >
 );
};

export default AuthenticatedRoute;
Copy the code

Authentication test differences

RTK Query has no inherent features when writing tests for React applications. Personally, I favor Kent C. Dodds’ testing approach and focus on user experience and user interaction. Not much changes when using RTK queries.

Having said that, each step will include its own tests to prove that the application written in RTK Query is fully testable.

Note: This example shows my thoughts on how to write these tests, including what to test, what to simulate, and how much code reusability to introduce.

RTK queries the repository

To demonstrate RTK Query, we’ll introduce some additional functionality to the application to see how it behaves in some scenarios and how it can be used.

Repository differences and test differences

The first thing we need to do is introduce a feature for the repository. This feature will try to mimic the functionality of the repository TAB that you can experience on GitHub. It will have access to your profile and be able to search repositories and sort them based on specific criteria. Many file changes are introduced in this step. I encourage you to delve into the parts that interest you.

Let’s start by adding the API definitions needed to override the repository functionality:

import { endpoint } from '@octokit/endpoint';
import { createApi } from '@reduxjs/toolkit/query/react';
import { githubBaseQuery } from '.. /index';
import { ResponseWithLink } from '.. /types';
import { RepositorySearchArgs, RepositorySearchData } from './types';

export const REPOSITORY_API_REDUCER_KEY = 'repositoryApi';
export const repositoryApi = createApi({
 reducerPath: REPOSITORY_API_REDUCER_KEY,
 baseQuery: githubBaseQuery,
 endpoints: (builder) = > ({
   searchRepositories: builder.query<
     ResponseWithLink<RepositorySearchData>,
     RepositorySearchArgs
     >(
     {
       query: (args) = > {
         return endpoint('GET /search/repositories', args); }})}),refetchOnMountOrArgChange: 60
});
Copy the code

Once we’re ready, let’s introduce a search/grid/paging repository feature:

import { Grid } from '@material-ui/core';
import React from 'react';
import PageContainer
 from '.. /.. /.. /.. /.. /.. /shared/components/PageContainer/PageContainer';
import PageHeader from '.. /.. /.. /.. /.. /.. /shared/components/PageHeader/PageHeader';
import RepositoryGrid from './components/RepositoryGrid/RepositoryGrid';
import RepositoryPagination
 from './components/RepositoryPagination/RepositoryPagination';
import RepositorySearch from './components/RepositorySearch/RepositorySearch';
import RepositorySearchFormContext
 from './components/RepositorySearch/RepositorySearchFormContext';

const Repositories = () = > {
 return (
   <RepositorySearchFormContext>
     <PageContainer>
       <PageHeader title="Repositories"/>
       <Grid container spacing={3}>
         <Grid item xs={12}>
           <RepositorySearch/>
         </Grid>
         <Grid item xs={12}>
           <RepositoryGrid/>
         </Grid>
         <Grid item xs={12}>
           <RepositoryPagination/>
         </Grid>
       </Grid>
     </PageContainer>
   </RepositorySearchFormContext>
 );
};

export default Repositories;
Copy the code

The interaction with the Repositories API is more complex than we have encountered so far, so let’s define custom hooks that will enable us to:

  • Gets the parameters of the API call.
  • Gets the result of the current API stored in the state.
  • Get the data by calling the API endpoint.
import { debounce } from 'lodash';
import { useCallback, useEffect, useMemo } from 'react';
import urltemplate from 'url-template';
import { repositoryApi } from '.. /.. /.. /.. /.. /.. /.. /api/github/repository/api';
import { RepositorySearchArgs }
 from '.. /.. /.. /.. /.. /.. /.. /api/github/repository/types';
import { useTypedDispatch } from '.. /.. /.. /.. /.. /.. /.. /shared/redux/store';
import { useAuthUser } from '.. /.. /.. /.. /.. /.. /auth/hooks/useAuthUser';
import { useRepositorySearchFormContext } from './useRepositorySearchFormContext';

const searchQs = urltemplate.parse('user:{user} {name} {visibility}');
export const useSearchRepositoriesArgs = (): RepositorySearchArgs= > {
 constuser = useAuthUser()! ;const { values } = useRepositorySearchFormContext();
 return useMemo<RepositorySearchArgs>(() = > {
   return {
     q: decodeURIComponent(
       searchQs.expand({
         user: user.login,
         name: values.name && `${values.name} in:name`.visibility: ['is:public'.'is:private'][values.type] ?? ' ',
       })
     ).trim(),
     sort: values.sort,
     per_page: values.per_page,
     page: values.page,
   };
 }, [values, user.login]);
};

export const useSearchRepositoriesState = () = > {
 const searchArgs = useSearchRepositoriesArgs();
 return repositoryApi.endpoints.searchRepositories.useQueryState(searchArgs);
};

export const useSearchRepositories = () = > {
 const dispatch = useTypedDispatch();
 const searchArgs = useSearchRepositoriesArgs();
 const repositorySearchFn = useCallback((args: typeof searchArgs) = > {
   dispatch(repositoryApi.endpoints.searchRepositories.initiate(args));
 }, [dispatch]);
 const debouncedRepositorySearchFn = useMemo(
   () = > debounce((args: typeof searchArgs) = > {
     repositorySearchFn(args);
   }, 100),
   [repositorySearchFn]
 );

 useEffect(() = > {
   repositorySearchFn(searchArgs);
   // Non debounced invocation should be called only on initial render
   // eslint-disable-next-line react-hooks/exhaustive-deps} []); useEffect(() = > {
   debouncedRepositorySearchFn(searchArgs);
 }, [searchArgs, debouncedRepositorySearchFn]);

 return useSearchRepositoriesState();
};
Copy the code

In this case, it is important to use this level of separation as an abstraction layer, both from a readability perspective and because of RTK query requirements.

You may have noticed that when we introduced useQueryState, a hook to use to retrieve user data, we had to provide the same arguments as we did for the actual API call.

import { userApi } from '.. /.. /.. /api/github/user/api';
import { User } from '.. /.. /.. /api/github/user/types';

export const useAuthUser = (): User | undefined= > {
 const state = userApi.endpoints.getUser.useQueryState(null);
 returnstate.data? .response; };Copy the code

Whether we call useQuery or the null we provide as an argument exists in useQueryState. This is necessary because the RTK query first identifies and caches a piece of information through the parameters used to retrieve that information.

This means that we need to be able to retrieve the parameters needed for the API call separately from the actual API call at any point in time. This way, we can use it to retrieve the cache state of API data when needed.

There is one more thing you need to notice in this code in our API definition:

refetchOnMountOrArgChange: 60
Copy the code

Why is that? Because one of the points of using a library like RTK Query is dealing with client caching and cache invalidation. This is critical and requires a great deal of effort, which may be difficult to provide depending on your stage of development.

I find RTK Query very flexible in this regard. Using this configuration property allows us to:

  • Disable caching completely, which comes in handy when you want to migrate to RTK queries to avoid caching issues as an initial step.
  • Introduce time-based caching, which is a simple invalidation mechanism used when you know something can be cached for X times.

submit

Commit differences and test differences

This step adds more functionality to the repository pages by adding the ability to view each repository’s submissions, paginate them, and filter them by branch. It also tries to mimic the functionality you get on the GitHub page.

We have introduced two additional endpoints for fetching branches and commits, as well as custom hooks for these endpoints, following the style we established during the implementation of the repository:

github/repository/api.ts import { endpoint } from '@octokit/endpoint'; import { createApi } from '@reduxjs/toolkit/query/react'; import { githubBaseQuery } from '.. /index'; import { ResponseWithLink } from '.. /types'; import { RepositoryBranchesArgs, RepositoryBranchesData, RepositoryCommitsArgs, RepositoryCommitsData, RepositorySearchArgs, RepositorySearchData } from './types'; export const REPOSITORY_API_REDUCER_KEY = 'repositoryApi'; export const repositoryApi = createApi({ reducerPath: REPOSITORY_API_REDUCER_KEY, baseQuery: githubBaseQuery, endpoints: (builder) => ({ searchRepositories: builder.query< ResponseWithLink<RepositorySearchData>, RepositorySearchArgs >( { query: (args) => { return endpoint('GET /search/repositories', args); }, }), getRepositoryBranches: builder.query< ResponseWithLink<RepositoryBranchesData>, RepositoryBranchesArgs >( { query(args) { return endpoint('GET /repos/{owner}/{repo}/branches', args); } }), getRepositoryCommits: builder.query< ResponseWithLink<RepositoryCommitsData>, RepositoryCommitsArgs >( { query(args) { return endpoint('GET /repos/{owner}/{repo}/commits', args); }, }), }), refetchOnMountOrArgChange: 60 }); useGetRepositoryBranches.ts import { useEffect, useMemo } from 'react'; import { useParams } from 'react-router-dom'; import { repositoryApi } from '.. /.. /.. /.. /.. /.. /.. /api/github/repository/api'; import { RepositoryBranchesArgs } from '.. /.. /.. /.. /.. /.. /.. /api/github/repository/types'; import { useTypedDispatch } from '.. /.. /.. /.. /.. /.. /.. /shared/redux/store'; import { useAuthUser } from '.. /.. /.. /.. /.. /.. /auth/hooks/useAuthUser'; import { CommitsRouteParams } from '.. /types'; export const useGetRepositoryBranchesArgs = (): RepositoryBranchesArgs => { const user = useAuthUser()! ; const { repositoryName } = useParams<CommitsRouteParams>(); return useMemo<RepositoryBranchesArgs>(() => { return { owner: user.login, repo: repositoryName, }; }, [repositoryName, user.login]); }; export const useGetRepositoryBranchesState = () => { const queryArgs = useGetRepositoryBranchesArgs(); return repositoryApi.endpoints.getRepositoryBranches.useQueryState(queryArgs); }; export const useGetRepositoryBranches = () => { const dispatch = useTypedDispatch(); const queryArgs = useGetRepositoryBranchesArgs(); useEffect(() => { dispatch(repositoryApi.endpoints.getRepositoryBranches.initiate(queryArgs)); }, [dispatch, queryArgs]); return useGetRepositoryBranchesState(); }; useGetRepositoryCommits.ts import isSameDay from 'date-fns/isSameDay'; import { useEffect, useMemo } from 'react'; import { useParams } from 'react-router-dom'; import { repositoryApi } from '.. /.. /.. /.. /.. /.. /.. /api/github/repository/api'; import { RepositoryCommitsArgs } from '.. /.. /.. /.. /.. /.. /.. /api/github/repository/types'; import { useTypedDispatch } from '.. /.. /.. /.. /.. /.. /.. /shared/redux/store'; import { useAuthUser } from '.. /.. /.. /.. /.. /.. /auth/hooks/useAuthUser'; import { AggregatedCommitsData, CommitsRouteParams } from '.. /types'; import { useCommitsSearchFormContext } from './useCommitsSearchFormContext'; export const useGetRepositoryCommitsArgs = (): RepositoryCommitsArgs => { const user = useAuthUser()! ; const { repositoryName } = useParams<CommitsRouteParams>(); const { values } = useCommitsSearchFormContext(); return useMemo<RepositoryCommitsArgs>(() => { return { owner: user.login, repo: repositoryName, sha: values.branch, page: values.page, per_page: 15 }; }, [repositoryName, user.login, values]); }; export const useGetRepositoryCommitsState = () => { const queryArgs = useGetRepositoryCommitsArgs(); return repositoryApi.endpoints.getRepositoryCommits.useQueryState(queryArgs); }; export const useGetRepositoryCommits = () => { const dispatch = useTypedDispatch(); const queryArgs = useGetRepositoryCommitsArgs(); useEffect(() => { if (! queryArgs.sha) return; dispatch(repositoryApi.endpoints.getRepositoryCommits.initiate(queryArgs)); }, [dispatch, queryArgs]); return useGetRepositoryCommitsState(); }; export const useAggregatedRepositoryCommitsData = (): AggregatedCommitsData => { const { data: repositoryCommits } = useGetRepositoryCommitsState(); return useMemo(() => { if (! repositoryCommits) return []; return repositoryCommits.response.reduce((aggregated, commit) => { const existingCommitsGroup = aggregated.find(a => isSameDay( new Date(a.date), new Date(commit.commit.author! .date!) )); if (existingCommitsGroup) { existingCommitsGroup.commits.push(commit); } else { aggregated.push({ date: commit.commit.author! .date! , commits: [commit] }); } return aggregated; }, [] as AggregatedCommitsData); }, [repositoryCommits]); };Copy the code

With this done, we can now improve UX by prefetching commit data when someone hovers over the repository name:

import {
 Badge,
 Box,
 Chip,
 Divider,
 Grid,
 Link,
 Typography
} from '@material-ui/core';
import StarOutlineIcon from '@material-ui/icons/StarOutline';
import formatDistance from 'date-fns/formatDistance';
import React, { FC } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import { repositoryApi } from '.. /.. /.. /.. /.. /.. /.. /.. /api/github/repository/api';
import { Repository } from '.. /.. /.. /.. /.. /.. /.. /.. /api/github/repository/types';
import { useGetRepositoryBranchesArgs }
 from '.. /.. /.. /Commits/hooks/useGetRepositoryBranches';
import { useGetRepositoryCommitsArgs }
 from '.. /.. /.. /Commits/hooks/useGetRepositoryCommits';

const RepositoryGridItem: FC<{ repo: Repository }> = ({ repo }) = > {
 const getRepositoryCommitsArgs = useGetRepositoryCommitsArgs();
 const prefetchGetRepositoryCommits = repositoryApi.usePrefetch(
   'getRepositoryCommits');
 const getRepositoryBranchesArgs = useGetRepositoryBranchesArgs();
 const prefetchGetRepositoryBranches = repositoryApi.usePrefetch(
   'getRepositoryBranches');

 return (
   <Grid container spacing={1}>
     <Grid item xs={12}>
       <Typography variant="subtitle1" gutterBottom aria-label="repository-name">
         <Link
           aria-label="commit-link"
           component={RouterLink}
           to={` /repositories/ ${repo.name} `}onMouseEnter={()= >{ prefetchGetRepositoryBranches({ ... getRepositoryBranchesArgs, repo: repo.name, }); prefetchGetRepositoryCommits({ ... getRepositoryCommitsArgs, sha: repo.default_branch, repo: repo.name, page: 1 }); }} > {repo.name}</Link>
         <Box marginLeft={1} clone>
           <Chip label={repo.private ? 'Private' : 'Public'} size="small"/>
         </Box>
       </Typography>
       <Typography component="p" variant="subtitle2" gutterBottom
                   color="textSecondary">
         {repo.description}
       </Typography>
     </Grid>
     <Grid item xs={12}>
       <Grid container alignItems="center" spacing={2}>
         <Box clone flex="0 0 auto" display="flex" alignItems="center"
              marginRight={2}>
           <Grid item>
             <Box clone marginRight={1} marginLeft=0.5} {>
               <Badge color="primary" variant="dot"/>
             </Box>
             <Typography variant="body2" color="textSecondary">
               {repo.language}
             </Typography>
           </Grid>
         </Box>
         <Box clone flex="0 0 auto" display="flex" alignItems="center"
              marginRight={2}>
           <Grid item>
             <Box clone marginRight=0.5} {>
               <StarOutlineIcon fontSize="small"/>
             </Box>
             <Typography variant="body2" color="textSecondary">
               {repo.stargazers_count}
             </Typography>
           </Grid>
         </Box>
         <Grid item>
           <Typography variant="body2" color="textSecondary">
             Updated {formatDistance(new Date(repo.pushed_at), new Date())} ago
           </Typography>
         </Grid>
       </Grid>
     </Grid>
     <Grid item xs={12}>
       <Divider/>
     </Grid>
   </Grid>
 );
};

export default RepositoryGridItem;
Copy the code

While hovering may seem artificial, it can seriously impact UX in real applications, and it’s always handy to provide such functionality in our toolset of libraries for API interaction.

Advantages and disadvantages of RTK queries

Final source code

We’ve seen how to use RTK queries in our applications, how to test these applications, and how to handle different issues, such as state retrieval, cache invalidation, and prefetching.

This article demonstrates a number of advanced advantages:

  • Data acquisition is built on top of Redux using its state management system.
  • API definitions and cache invalidation policies are in one place.
  • TypeScript improves the development experience and maintainability.

There are also some notable downsides:

  • The library is still under active development, so the API is subject to change.
  • Information scarcity: There is not much information except for documents that may be out of date.

We covered a lot of ground in our practical walkthrough using the GitHub API, but there’s more to RTK queries, such as:

  • Error handling
  • mutation
  • polling
  • Optimistic updates

If you are interested in the benefits of RTK Query, I encourage you to delve further into these concepts. Feel free to use this example as a base to build on.