用Redux工具包和RTK查询创建React应用程序

259 阅读15分钟

Cover image

网络前端

18分钟阅读

用Redux工具包和RTK查询创建React应用程序

你有没有想过把Redux和React Query结合起来使用?现在你可以了,通过使用Redux工具包和它的最新成员。RTK查询。这篇文章展示了RTK Query在现实生活中的应用,并附有详细的代码示例。

作者

作者

Gurami Dagundaridze

Gurami是医疗、游戏和娱乐行业的全栈开发者。作为格鲁吉亚银行的高级前端工程师,他重新设计了银行内部软件系统。他的重新设计利用了AWS、Node.js、GraphQL和React。

分享

你曾想过一起使用Redux和React Query吗?现在你可以了,通过使用Redux工具包和它的最新补充。RTK查询

RTK Query是一个先进的数据获取和客户端缓存工具。它的功能类似于React Query,但它的好处是可以直接与Redux集成。对于API交互,开发者在使用Redux时通常使用Thunk等异步中间件模块。这样的方法限制了灵活性;因此React开发者现在有了一个来自Redux团队的官方替代品,它涵盖了当今客户端/服务器通信的所有高级需求。

本文展示了RTK查询如何在真实世界的场景中使用,每一步都包括一个提交差异的链接,以突出增加的功能。完整的代码库的链接出现在文章的最后。

模板和配置

项目初始化差异

首先,我们需要创建一个项目。这是用Create React AppCRA)模板完成的,用于TypeScript和Redux。

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

它有几个依赖项,我们将在此过程中需要,最明显的是。

  • Redux工具箱和RTK查询
  • 材料用户界面
  • Lodash
  • Formik
  • React Router

它还包括为webpack提供自定义配置的能力。通常情况下,CRA不支持这种能力,除非你弹出。

初始化

比弹出更安全的途径是使用可以修改配置的东西,尤其是在这些修改很小的情况下。这个模板使用react-app-rewiredcustomize-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 };

这通过允许导入,使开发者的体验更好。比如说。

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

这样的导入通常会导致bundle大小的增加,但是通过我们配置的重写功能,这些会像这样运作。

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

配置

Redux设置差异

由于整个应用程序是基于Redux的,在初始化之后,我们将需要设置商店配置。

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;

除了标准的商店配置外,我们将添加全局重置状态动作的配置,这在现实世界的应用中很方便,既适用于应用本身,也适用于测试。

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

到目前为止,一切都很好。我们已经创建了模板并配置了Redux。现在让我们来添加一些功能。

认证

检索访问令牌差异

为了简单起见,认证被分解成三个步骤。

  • 添加API定义以检索访问令牌
  • 添加组件来处理GitHub的网络认证流程
  • 通过提供实用组件来最终完成认证,以便为整个应用提供用户

在这一步,我们增加了检索访问令牌的能力。

RTK查询思想决定了所有的API定义都出现在一个地方,这在处理有多个端点的企业级应用时很方便。在企业级应用中,当所有东西都在一个地方时,考虑集成的API以及客户端缓存就容易多了。

RTK查询具有使用OpenAPI标准或GraphQL自动生成API定义的工具。这些工具仍处于起步阶段,但它们正在积极开发中。此外,这个库的设计是为了提供优秀的开发人员使用TypeScript的经验,由于TypeScript能够提高可维护性,它正逐渐成为企业应用程序的选择。

在我们的案例中,定义将存放在API文件夹下。目前,我们只要求了这一点。

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

GitHub的认证是通过一个开源的认证服务器提供的,由于GitHub API的要求,它被单独托管在Heroku上。

认证服务器

虽然这个例子项目不需要,但希望托管自己的认证服务器副本的读者需要。

  1. 在GitHub中创建一个OAuth应用,生成自己的客户端ID和密码。
  2. 通过环境变量GITHUB_CLIENT_IDGITHUB_SECRET 向认证服务器提供 GitHub 的详细信息。
  3. 替换上述API定义中的认证端点baseUrl 值。
  4. 在React方面,在接下来的代码示例中替换client_id 参数。

下一步是添加使用该API的组件。由于GitHub网络应用流程的要求,我们将需要一个负责重定向到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;

一旦GitHub重定向回我们的应用,我们将需要一个路由来处理代码,并根据它检索access_token

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的集成,这提供了一些整洁的功能,我们将在实现更多的功能时观察。对于access_token ,但我们仍然需要通过调度一个动作来手动将其保存在商店中。

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

我们这样做是为了能够在页面重载之间保持令牌的持久性。为了实现持久性和调度动作的能力,我们需要为我们的认证功能定义一个存储配置。

按照惯例,Redux Toolkit将这些称为切片。

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

要使前面的代码发挥作用,还有一个要求。每个API都必须作为存储配置的reducer来提供,而且每个API都有自己的中间件,你必须包括这些中间件。

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;

这就是了!现在我们的应用程序正在检索access_token ,我们准备在上面添加更多的认证功能。

完成认证

完成认证差异

认证的下一个功能列表包括。

  • 从GitHub的API中检索用户,并将其提供给应用的其他部分。
  • 具备只有在认证时或以访客身份浏览时才能访问的路由的功能。

为了增加检索用户的能力,我们需要一些API模板。与认证API不同,GitHub API需要从我们的Redux存储中检索访问令牌,并将其作为授权头应用于每个请求。

在RTK查询中,这可以通过创建一个自定义的基础查询来实现。

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

我在这里使用axios,但也可以使用其他客户端。

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

我们在这里使用我们的自定义基础查询,这意味着userApi 范围内的每个请求都将包括一个授权头。让我们调整一下主商店的配置,以便该API可用。

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;

接下来,我们需要在我们的应用程序被渲染之前调用这个API。为了简单起见,让我们以一种类似于Angular路由的解析功能的方式来做,这样在我们得到用户信息之前,什么都不会被渲染。

用户不在的情况也可以用更细化的方式来处理,通过事先提供一些UI,让用户更快拥有第一个有意义的渲染。这需要更多的思考和工作,而且肯定应该在一个生产就绪的应用程序中解决。

要做到这一点,我们需要定义一个中间件组件。

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;

它的作用很直接。它与GitHub的API交互以获取用户信息,并在响应之前不渲染儿童。现在,如果我们用这个组件来包装应用程序的功能,我们知道用户信息将在其他东西渲染之前得到解决。

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;

让我们来看看最时尚的部分。我们现在有能力在应用程序的任何地方获得用户信息,即使我们没有像access_token 那样在店内手动保存用户信息。

怎么做到的?通过为它创建一个简单的自定义React Hook

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查询为每个端点提供了useQueryState 选项,这使我们有能力检索该端点的当前状态。

为什么这一点如此重要和有用?因为我们不需要写大量的开销来管理代码。作为奖励,我们在Redux中得到了开箱即用的API/客户端数据的分离。

使用RTK查询避免了这些麻烦。通过结合数据获取和状态管理,RTK Query消除了即使我们使用React Query也会存在的差距。(使用React Query,获取的数据必须被UI层上不相关的组件访问。)

作为最后一步,我们定义了一个标准的自定义路由组件,它使用这个钩子来决定一个路由是否应该被渲染。

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

认证测试 Diff

在为React应用编写测试时,RTK Query本身并没有什么特别之处。就个人而言,我赞成Kent C. Dodds的测试方法,以及注重用户体验和用户互动的测试风格。使用RTK Query时,没有什么变化。

也就是说,每个步骤仍将包括其自身的测试,以证明用RTK Query编写的应用程序是完全可以测试的。

注意。 这个例子显示了我对这些测试应该如何编写的看法,包括测试什么,模拟什么,以及引入多少代码重用性。

RTK查询存储库

为了展示RTK Query,我们将在应用程序中引入一些额外的功能,看看它在某些情况下的表现以及如何使用它。

储存库差异测试差异

我们要做的第一件事是为存储库引入一个功能。这个功能将试图模仿你在GitHub中可以体验到的Repositories标签的功能。它将访问你的配置文件,并有能力搜索存储库,并根据某些标准对其进行排序。这一步中引入了许多文件变化。我鼓励你去挖掘你感兴趣的部分。

让我们首先添加涵盖存储库功能所需的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<RepositorySearchData>,
     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 (
   <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;

与Repositories API的交互比我们目前遇到的更复杂,所以让我们定义自定义钩子,为我们提供以下能力。

  • 获取API调用的参数。
  • 获取存储在状态中的当前API结果。
  • 通过调用API端点来获取数据。
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<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();
};

在这种情况下,无论是从可读性的角度还是从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;
};

无论我们调用useQuery 还是useQueryState ,我们提供的那个null参数都是存在的。这是必须的,因为RTK Query通过首先用于检索信息的参数来识别和缓存一个信息。

这意味着我们需要能够在任何时候从实际的API调用中单独获得API调用所需的参数。这样一来,我们就可以在需要的时候用它来检索API数据的缓存状态了。

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

refetchOnMountOrArgChange: 60

为什么?因为在使用像RTK Query这样的库时,重要的一点是处理客户端的缓存和缓存失效。这一点至关重要,同时也需要大量的努力,根据你所处的开发阶段,可能很难提供。

我发现RTK Query在这方面非常灵活。使用这个配置属性,我们可以。

  • 完全禁用缓存,这在你想向RTK Query迁移时很方便,作为第一步避免了缓存问题。
  • 引入基于时间的缓存,这是一种简单的失效机制,当你知道某些信息可以被缓存X时间的时候,就可以使用。

承诺

提交差异测试差异

这一步为版本库页面增加了更多的功能,可以查看每个版本库的提交,分页显示这些提交,并按分支过滤。它还试图模仿你在 GitHub 页面上得到的功能。

我们又引入了两个用于获取分支和提交的端点,以及这些端点的自定义钩子,沿用了我们在实现存储库时建立的风格。

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

这样做之后,我们现在可以通过在有人将鼠标悬停在版本库名称上时预取提交数据来改善用户体验。

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;

虽然悬停看起来是人为的,但这在现实世界的应用中严重影响了用户体验,而且在我们用于API交互的库的工具集中,有这样的功能总是很方便。

RTK查询的优点和缺点

最终的源代码

我们已经看到了如何在我们的应用程序中使用RTK查询,如何测试这些应用程序,以及如何处理不同的问题,如状态检索、缓存失效和预取。

在这篇文章中,有许多高层次的好处被展示出来。

  • 数据获取是建立在Redux之上的,利用了它的状态管理系统。
  • API定义和缓存失效策略都在一个地方。
  • TypeScript改善了开发经验和可维护性。

也有一些值得注意的缺点。

  • 该库仍在积极开发中,因此,API可能会改变。
  • 信息匮乏。除了可能已经过时的文档之外,周围没有什么信息。

我们在这个使用GitHub API的实践演练中涵盖了很多内容,但RTK查询还有很多内容,比如。

如果你对RTK查询的好处感兴趣,我鼓励你进一步挖掘这些概念。请随意使用这个例子作为基础。

了解基础知识

什么是RTK查询?

RTK Query是一个先进的API交互工具,其灵感来自于React Query等类似工具。但与React Query不同,RTK Query提供了与框架无关的Redux的完全集成。

与使用Redux与Thunk/Saga相比,RTK Query是怎样的?

与Thunk/Saga相比,RTK Query在与Redux的API交互方面有很大的改进,即在缓存、重取、预取、流式更新和乐观更新方面,这需要在Thunk和Saga的现有功能之上完成大量的自定义工作。

如果我已经在使用React Query,是否值得切换到RTK Query?

如果你的应用除了API需求外,还需要一些像Redux一样的中央状态管理,你应该考虑切换到RTK Query。保持React Query和Redux的同步是很痛苦的,需要一些自定义的代码开销,而RTK Query提供开箱即用的兼容性。

使用RTK Query的优点和缺点是什么?

对于已经使用Redux生态系统的人来说,RTK Query是一个不错的选择。另一方面,如果你不使用Redux,它不适合你。在定义API端点方面,它也是相当新的和有意见的。

标签

ReactReduxReduxToolkitRTKQuery

自由职业者? 寻找你的下一份工作。

React.js开发工作

[

查看完整资料

](www.toptal.com/resume/gura…)

古拉米-达贡达里泽

全栈开发者

关于作者

Gurami在不同的领先公司获得了相当多的工作经验,他在那里做技术决策,管理项目,并使用AWS、Node.js、GraphQL和React进行基础的重新设计。他对为初创企业从头开始做项目以及与专业人士合作感兴趣。Gurami总是不遗余力地寻求完美的解决方案,并期待着与那些和他一样热爱自己工作的团队合作。

聘请古拉米

评论

请启用JavaScript以查看由Disqus提供的评论。评论由Discuz提供

世界级的文章,每周交付。

获取精彩内容

订阅意味着同意我们的隐私政策

谢谢您!
请查看您的收件箱以确认您的邀请。

热门文章

工程学IconChevron数据科学与数据库

掌握Python/NetworkX的图形数据科学

工程图标Chevron后端

使用Express.js路由进行基于承诺的错误处理

工程图解ChevronWeb前端

企业应用的最佳React状态管理工具

工程图示 ChevronBack-end

使用AWS SSM进行SSH日志和会话管理

查看我们的相关人才

React.js

自由职业者? 寻找你的下一份工作。

React.js开发工作

聘请作者

[

查看完整资料

](www.toptal.com/resume/gura…)

古拉米-达贡达里泽

全栈开发者

阅读下一页

工程师IconChevron数据科学和数据库

使用Python/NetworkX的图形数据科学