作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Gurami Dagundaridze's profile image

Gurami Dagundaridze

Gurami是面向医疗保健、游戏和娱乐行业的全栈开发人员. 作为佐治亚银行的高级前端工程师,他重新设计了银行内部软件系统. His redesigns leverage AWS, Node.js, GraphQL, and React.

Expertise

Previously At

Bank of Georgia
Share

This article was updated on August 5, 2022. 它已被修改,以包括最新和相关的信息和来源, and has been reviewed by our editorial team for clarity.

Note: Refactored API slicing approach & Updated RTK Query version - commit.

Have you ever wanted to use React Query with Redux—or at least, Redux with features like React Query provides? 现在,通过使用Redux Toolkit及其最新添加的功能:Redux Toolkit Query,或者 RTK Query for short.

RTK Query is an advanced data-fetching and client-side caching tool. When it comes to React Query vs. RTK Query, RTK Query的功能类似于React Query,但它的好处是可以直接与Redux集成. For API interaction, 开发人员在使用Redux时通常使用像Thunk这样的异步中间件模块. Such an approach limits flexibility; thus React developers 现在有了Redux团队的官方替代方案,它涵盖了当今客户机/服务器通信的所有高级需求.

本文演示了如何在真实场景中使用React应用中的RTK Query, 每个步骤都包含一个到commit diff的链接,以突出显示添加的功能. A link to the complete codebase appears at the end.

Boilerplate and Configuration

Project Initialization Diff

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

npx create-react-app . --template redux-typescript

它有几个我们在开发过程中需要的依赖项,最值得注意的是:

  • Redux Toolkit and RTK Query
  • Material UI
  • Lodash
  • Formik
  • React Router

It also includes the ability to provide custom configuration for webpack. Normally, CRA does not support such abilities unless you eject.

Initialization

比弹出更安全的方法是使用可以修改配置的东西, especially if those modifications are small. This boilerplate uses react-app-rewired and customize-cra 为了实现这个功能,引入一个自定义Babel配置:

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 };

This makes the developer experience better by allowing imports. For example:

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

Such imports usually result in an increased bundle size, but with the rewriting functionality that we configured, these will function like so:

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

Configuration

Redux Setup Diff

Since the whole app is based on Redux, after initialization we will need to set up store configuration:

import { combineReducers, configureStore } from '@reduxjs/toolkit';
从react-redux中导入{TypedUseSelectorHook, useDispatch, useSelector};
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(reducers);

export const rootReducer: Reducer = (
 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;
export const useTypedDispatch = () => useDispatch();
export const useTypedSelector: TypedUseSelectorHook = useSelector;

Apart from the standard store configuration, 我们将添加一个全局重置状态操作的配置,这在现实世界的应用程序中会派上用场, both for the apps themselves and for testing:

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

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

接下来,我们将添加自定义中间件,通过清除存储来处理401响应:

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);
};

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

Authentication

Retrieving Access Token Diff

Authentication is broken down into three steps for simplicity:

  • Adding API definitions to retrieve an access token
  • Adding components to handle GitHub web authentication flow
  • 通过提供实用程序组件来完成身份验证,以便向整个应用程序提供用户

At this step, we add the ability to retrieve the access token.

RTK Query思想要求所有API定义都出现在一个地方, 在处理具有多个端点的企业级应用程序时,哪一种更方便. In an enterprise application, it is much easier to contemplate the integrated API, as well as client caching, when everything is in one place.

RTK Query features tools for auto-generating API definitions using OpenAPI standards or GraphQL. 这些工具仍处于起步阶段,但它们正在积极发展. In addition, 该库旨在为TypeScript开发人员提供出色的体验, 由于其提高可维护性的能力,哪个正日益成为企业应用程序的选择.

In our case, definitions will reside under the API folder. For now we have required only this:

从“@reduxjs/toolkit/query/react”中导入{createApi, fetchBaseQuery};
import { AuthResponse } from './types';

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

GitHub身份验证是通过开源身份验证服务器提供的, 由于GitHub API的要求,它在Heroku上单独托管.

The Authentication Server

虽然这个RTK查询示例项目不需要,但希望托管自己的副本的读者 the authentication server will need to:

  1. Create an OAuth app in GitHub to generate their own client ID and secret.
  2. 通过环境变量向身份验证服务器提供GitHub详细信息 GITHUB_CLIENT_ID and GITHUB_SECRET.
  3. Replace the authentication endpoint baseUrl value in the above API definitions.
  4. On the React side, replace the client_id parameter in the next code sample.

The next step is to add components that use this API. Due to the requirements of GitHub web application flow,我们需要一个登录组件负责重定向到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 (
   
     
       
         
           
             Log in via Github
           
           
             
           
         
       
     
   
 );
};

export default Login;

一旦GitHub重定向回我们的应用程序,我们将需要一个路由来处理代码和检索 access_token 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);
 const accessTokenQueryResult = authApi.endpoints.getAccessToken.useQuery(
   code!,
   {
     skip: !code
   }
 );
 const { data } = accessTokenQueryResult;
 const accessToken = data?.access_token;

 useEffect(() => {
   if (!accessToken) return;

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

如果您曾经使用过React Query,那么与API交互的机制与RTK Query类似. 由于Redux集成,这提供了一些简洁的特性,我们将在实现其他特性时观察到这些特性. For access_token但是,我们仍然需要通过调度一个操作来手动将它保存在store中:

dispatch(authSlice.actions.updateAccessToken(accessToken));

We do this for the ability to persist the token between page reloads. Both for persistence and the ability to dispatch the action, we need to define a store configuration for our authentication feature.

Per convention, Redux Toolkit refers to these as 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) {
     state.accessToken = action.payload;
   },
 },
});

export const authReducer = persistReducer({
 key: 'rtk:auth',
 storage,
 whitelist: ['accessToken']
}, authSlice.reducer);

There is one more requirement for the preceding code to function. Each API has to be provided as a reducer for store configuration, and each API comes with its own middleware, which you have to include:

import { combineReducers, configureStore } from '@reduxjs/toolkit';
从react-redux中导入{TypedUseSelectorHook, useDispatch, useSelector};
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(reducers);

export const rootReducer: Reducer = (
 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;
export const useTypedDispatch = () => useDispatch();
export const useTypedSelector: TypedUseSelectorHook = useSelector;

That’s it! Now our app is retrieving access_token and we are ready to add more authentication features on top of it.

Completing Authentication

Completing Authentication Diff

The next feature list for authentication includes:

  • 能够从GitHub API中检索用户,并将其提供给应用程序的其余部分.
  • 该实用程序具有仅在经过身份验证或以访客身份浏览时才能访问的路由.

要添加检索用户的功能,我们需要一些API样板. Unlike with the authentication API, GitHub API需要能够从我们的Redux存储中检索访问令牌,并将其作为授权头应用于每个请求.

In RTK Query that is achieved by creating a custom base query:

从“@octokit/types/dist-types/RequestOptions”中导入{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: 'http://api.github.com',
 headers: {
   accept: `application/vnd.github.v3+json`
 }
});

const axiosBaseQuery = (): BaseQueryFn => async (
 requestOpts,
 { getState }
) => {
 try {
   const token = (getState() as RootState).authSlice.accessToken;
   const result = await githubAxiosInstance({
     ...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();

I am using axios here, but other clients can be used too.

下一步是定义一个API,用于从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, null>({
     query: () => {
       return endpoint('GET /user');
     },
   }),
 }),
});

我们在这里使用自定义基查询,这意味着范围内的每个请求 userApi will include an Authorization header. Let’s tweak the main store configuration so that the API is available:

import { combineReducers, configureStore } from '@reduxjs/toolkit';
从react-redux中导入{TypedUseSelectorHook, useDispatch, useSelector};
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(reducers);

export const rootReducer: Reducer = (
 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;
export const useTypedDispatch = () => useDispatch();
export const useTypedSelector: TypedUseSelectorHook = useSelector;

Next, we need to call this API before our app is rendered. For simplicity, let’s do it in a manner that resembles how the resolve functionality 适用于Angular路由,所以在我们获得用户信息之前不会渲染任何内容.

The absence of the user can also be handled in a more granular way, 通过预先提供一些UI,以便用户更快地获得第一次有意义的渲染. 这需要更多的思考和工作,并且绝对应该在生产就绪的应用程序中解决.

To do that, 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 (
     
   );
 }

 return children as React.ReactElement;
};

export default UserMiddleware;

What this does is straightforward. 它与GitHub API交互以获取用户信息,并且在响应可用之前不会呈现子节点. Now if we wrap the app functionality with this component, 我们知道用户信息将在任何其他渲染之前被解析:

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 (
   
     } persistor={persistor}>
       
         
           
           
             
           
         
       
     
   
 );
};

export default App;

Let’s move on to the sleekest part. We now have the ability to get user information anywhere in the app, 尽管我们并没有手动保存用户信息 access_token.

How? By creating 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);
 return state.data?.response;
};

RTK Query provides the useQueryState option for every endpoint, 这使我们能够检索端点的当前状态.

Why is this so important and useful? Because we don’t have to write a lot of overhead to manage code. 作为奖励,我们在Redux中实现了API/客户端数据的分离.

Using RTK Query avoids the hassle. By combining data fetching with state management, RTK查询消除了即使我们使用React查询也会存在的差距. (使用React Query,获取的数据必须由UI层上不相关的组件访问.)

As a final step, 我们定义了一个标准的自定义路由组件,它使用这个钩子来决定路由是否应该被渲染:

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

export type AuthenticatedRouteProps = {
 onlyPublic?: boolean;
} & RouteProps;

const AuthenticatedRoute: FC = ({
 children,
 onlyPublic = false,
 ...routeProps
}) => {
 const user = useAuthUser();

 return (
    {
       if (onlyPublic) {
         return !user ? (
           children
         ) : (
           
         );
       }

       return user ? (
         children
       ) : (
         
       );
     }}
   />
 );
};

export default AuthenticatedRoute;

Authentication Tests Diff

在React应用程序中为RTK Query编写测试时,没有什么固有的特定要求. Personally, I am in favor of Kent C. Dodds’ approach to testing and a testing style that focuses on user experience and user interaction. Nothing much changes when using RTK Query.

That being said, 每个步骤仍然包括自己的测试,以证明使用RTK Query编写的应用程序是完全可测试的.

Note: 这个例子展示了我对这些测试应该如何编写的看法, what to mock, and how much code reusability to introduce.

RTK Query Repositories

To showcase RTK Query, 我们将介绍应用程序的一些附加特性,以了解它在某些场景中的性能以及如何使用它.

Repositories Diff and Tests Diff

The first thing we will do is introduce a feature for repositories. 此功能将尝试模仿您在GitHub中可以体验到的知识库选项卡的功能. 它将访问您的配置文件,并具有搜索存储库并根据特定标准对其进行排序的能力. There are many file changes introduced in this step. I encourage you to dig into the parts that you are interested in.

让我们首先添加涵盖存储库功能所需的API定义:

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,
     RepositorySearchArgs
     >(
     {
       query: (args) => {
         return endpoint('GET /search/repositories', args);
       },
     }),
 }),
 refetchOnMountOrArgChange: 60
});

一旦准备好了,让我们引入一个由搜索/网格/分页组成的存储库特性:

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 (
   
     
       
       
         
           
         
         
           
         
         
           
         
       
     
   
 );
};

export default Repositories;

与repository API的交互比我们目前遇到的要复杂得多, 因此,让我们定义自定义钩子,它将为我们提供以下能力:

  • Get arguments for API calls.
  • Get the current API result as stored in the state.
  • Fetch data by calling API endpoints.
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 => {
 const user = useAuthUser()!;
 const { values } = useRepositorySearchFormContext();
 return useMemo(() => {
   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();
};

在这种情况下,从可读性的角度和RTK查询的需求来看,将这种级别的分离作为抽象层是很重要的.

您可能已经注意到,我们引入了一个钩子,该钩子通过使用 useQueryState,我们必须提供与实际API调用相同的参数.

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);
 return state.data?.response;
};

That null we provide as an argument is there whether we call useQuery or useQueryState. 这是必需的,因为RTK Query通过首先用于检索该信息的参数来标识和缓存该信息.

这意味着我们需要能够在任何时间点单独获得API调用所需的参数,而不是实际的API调用. 这样,我们就可以在需要的时候使用它来检索API数据的缓存状态.

在我们的API定义中的这段代码中,还有一件事需要注意:

refetchOnMountOrArgChange: 60

Why? 因为在使用RTK Query等库时,重要的一点是处理客户端缓存和缓存无效. This is vital and also requires a substantial amount of effort, 根据你所处的开发阶段,哪些是很难提供的.

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

  • Disable caching altogether, which comes in handy when you want to migrate toward RTK Query, avoiding cache issues as an initial step.
  • Introduce time-based caching, 当您知道某些信息可以缓存X段时间时,可以使用简单的无效机制.

Commits

Commits Diff and Tests Diff

此步骤通过添加查看每个存储库提交的功能,向存储库页面添加了更多功能, paginate those commits, and filter by branch. 它还试图模仿你在GitHub页面上获得的功能.

我们已经介绍了另外两个用于获取分支和提交的端点, as well as custom hooks for these endpoints, 遵循我们在实现存储库时建立的风格:

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,
     RepositorySearchArgs
     >(
     {
       query: (args) => {
         return endpoint('GET /search/repositories', args);
       },
     }),
   getRepositoryBranches: builder.query<
     ResponseWithLink,
     RepositoryBranchesArgs
     >(
     {
       query(args) {
         return endpoint('GET /repos/{owner}/{repo}/branches', args);
       }
     }),
   getRepositoryCommits: builder.query<
     ResponseWithLink, 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();
 return useMemo(() => {
   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();
 const { values } = useCommitsSearchFormContext();
 return useMemo(() => {
   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]);
};

Having done this, 我们现在可以通过预取提交数据来改善用户体验,只要有人在存储库名称上徘徊:

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 (
   
     
       
          {
             prefetchGetRepositoryBranches({
               ...getRepositoryBranchesArgs,
               repo: repo.name,
             });
             prefetchGetRepositoryCommits({
               ...getRepositoryCommitsArgs,
               sha: repo.default_branch,
               repo: repo.name,
               page: 1
             });
           }}
         >
           {repo.name}
         
         
           
         
       
       
         {repo.description}
       
     
     
       
         
           
             
               
             
             
               {repo.language}
             
           
         
         
           
             
               
             
             
               {repo.stargazers_count}
             
           
         
         
           
             Updated {formatDistance(new Date(repo.pushed_at), new Date())} ago
           
         
       
     
     
       
     
   
 );
};

export default RepositoryGridItem;

While the hover may seem artificial, this heavily impacts UX in real-world applications, 在我们用于API交互的库的工具集中提供这样的功能总是很方便的.

The Pros and Cons of RTK Query

Final Source Code

We have seen how to use RTK Query in our apps, how to test those apps, and how to handle different concerns like state retrieval, cache invalidation, and prefetching.

本文展示了一些高级的好处:

  • 数据获取构建在Redux之上,利用其状态管理系统.
  • API定义和缓存无效策略放在一个地方.
  • TypeScript improves the development experience and maintainability.

There are some downsides worth noting as well:

  • 该库仍在积极开发中,因此api可能会发生变化.
  • Information scarcity: Besides the documentation, which may be out of date, there isn’t much information around.

在这个使用GitHub API的RTK/React演练中,我们介绍了很多内容, but there is much more to RTK Query, such as:

如果您对RTK Query的优点感兴趣,我鼓励您进一步研究这些概念. Feel free to use this RTK Query example as a basis to build on.

Understanding the basics

  • What is RTK Query?

    RTK Query是一种高级API交互工具,灵感来自于类似的工具,如React Query. 但与React Query不同的是,RTK Query提供了与框架无关的Redux的完全集成.

  • How does RTK Query compare to using Redux with Thunk/Saga?

    RTK Query在与Redux的API交互方面,为Thunk/Saga的使用提供了很大的改进, namely in caching, refetching, prefetching, streaming updates, and optimistic updates, 这需要在现有功能的基础上进行大量定制工作才能完成《欧博体育app下载》和《欧博体育app下载》.

  • Is it worth switching to RTK Query if I am already using React Query?

    You should consider switching to RTK Query if your app, apart from its API needs, requires some central state management like Redux. 保持React Query和Redux同步是痛苦的,并且需要一些自定义代码开销, while RTK Query provides out-of-the-box compatibility.

  • What are the pros and cons of using RTK Query?

    对于已经在使用Redux生态系统的人来说,RTK Query是一个不错的选择. On the other hand, if you are not using Redux, it’s not for you. 在定义API端点方面,它也是相当新颖和固执的.

Consult the author or an expert on this topic.
Schedule a call
Gurami Dagundaridze's profile image
Gurami Dagundaridze

Located in Tbilisi, Georgia

Member since October 16, 2020

About the author

Gurami是面向医疗保健、游戏和娱乐行业的全栈开发人员. 作为佐治亚银行的高级前端工程师,他重新设计了银行内部软件系统. His redesigns leverage AWS, Node.js, GraphQL, and React.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Expertise

Previously At

Bank of Georgia

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

Toptal Developers

Join the Toptal® community.