diff --git a/docs/dev/frontend/img/redux-toolkit.assets/image-20240416155405105.png b/docs/dev/frontend/img/redux-toolkit.assets/image-20240416155405105.png new file mode 100644 index 0000000..dea5e39 Binary files /dev/null and b/docs/dev/frontend/img/redux-toolkit.assets/image-20240416155405105.png differ diff --git a/docs/dev/frontend/redux-toolkit.md b/docs/dev/frontend/redux-toolkit.md new file mode 100644 index 0000000..c6d21d4 --- /dev/null +++ b/docs/dev/frontend/redux-toolkit.md @@ -0,0 +1,395 @@ +# Redux Toolkit 入门 + +在此之前,我的 React 从来都是一把梭,可能是项目写得比较小的原因,也没感觉到过什么问题。 + +上一次自己手动封装了一个`useFetch`请求 API 获取数据,感觉这种写法还不错,觉得那些库是不是有更好的解决方案,于是来了解一下[Redux Toolkit](https://redux-toolkit.js.org/introduction/getting-started)。 + +官方已经不推荐使用`redux`了,而是推荐使用`redux-toolkit`,`RTK`对`redux`进行了封装,提供了一些工具函数,让我们更方便地使用`redux`。它们主要用于状态管理。当应用中有很多组件需要共享状态时,就可以使用`redux`来管理这些状态。 + +平常使用`useContext`也是足够的,但是当应用变得复杂时,就需要使用`redux`这类的框架来管理状态了,虽然我还没遇到这么复杂的应用,但是简单的了解一下也是不错的。 + +在请求 API 接口时,我们也可以使用`RTK Query`来管理数据获取,这样就不用自己封装`useFetch`了。 + +## 创建模板 + +首先,我们使用官方提供的模板来创建一个项目: + +```bash +npx degit reduxjs/redux-templates/packages/vite-template-redux my-app +cd my-app +npm i +``` + +创建完成后,我们就可以使用`npm start`来启动项目,可以看到一个这样的页面 + +![image-20240416155405105](img/redux-toolkit.assets/image-20240416155405105.png) + +可以进入`src/App.tsx`文件里看一下,具体的实现代码。 + +另外,我们也可以下载[Redux DevTools](https://chromewebstore.google.com/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd)这个浏览器扩展来查看 Redux 的状态变化。 + +## 基础概念 + +我们根据上述创建的模板项目来学习下面的一些概念。 + +### Store + +`store`是一个对象,它包含了应用中所有的`state`,在 Redux 中,只会有一个`store`。在示例中处于:`src/app/store.ts`文件中。 + +我们可以使用`configureStore`来创建一个`store`,并且可以传入一些参数,比如`reducer`、`middleware`等。`configureStore`是`createStore`的一个封装。 + +```ts +const store = configureStore({ + reducer: rootReducer, + // Adding the api middleware enables caching, invalidation, polling, + // and other useful features of `rtk-query`. + middleware: (getDefaultMiddleware) => { + return getDefaultMiddleware().concat(quotesApiSlice.middleware); + }, + preloadedState, +}); +``` + +### Action + +`action`是一个对象,它描述了发生了什么事情。必须包含`type`字段,用来描述`action`的类型。另外,还可以包含`payload`字段,用来描述`action`的数据。 + +例如: + +```ts +const addTodoAction = { + type: "todos/todoAdded", + payload: "Buy milk", +}; +``` + +### Reducer + +`reducer`是一个函数,它接收两个参数:`state`和`action`,然后返回一个新的`state`。它根据`action`的`type`来决定如何更新`state`。 + +例如: + +```ts +const initialState = { value: 0 }; + +function counterReducer(state = initialState, action) { + // 检查 reducer 是否关心这个 action + if (action.type === "counter/increment") { + // 如果是,复制 `state` + return { + ...state, + // 使用新值更新 state 副本 + value: state.value + 1, + }; + } + // 返回原来的 state 不变 + return state; +} +``` + +### Dispatch + +`dispatch`是一个函数,它用来发送`action`,然后`store`会调用`reducer`来更新`state`。 + +例如: + +```ts +store.dispatch({ type: "counter/increment" }); +``` + +### Selector + +`selector`是一个函数,它用来从`state`中获取数据。`selector`可以接收`state`作为参数,然后返回需要的数据。当`state`过于复杂时,我们可以使用`selector`来简化获取数据的过程。 + +例如: + +```ts +const selectCount = (state) => state.counter.value; + +const count = selectCount(store.getState()); +``` + +### Middleware + +`middleware`是一个函数,可以用于记录 action 日志、异步请求等。`middleware`可以拦截`dispatch`,然后执行一些操作,最后再调用`dispatch`,这在`RTK Query`中经常使用。 + +### Slice + +`slice`是一个对象,它包含了`reducer`、`action`和`selector`。`slice`可以用来封装`reducer`、`action`和`selector`,然后导出给其他模块使用。 + +可以使用`createSlice`来创建一个`slice`,它接收一个对象,包含`name`、`initialState`、`reducers`等字段。 + +例如: + +```ts +const counterSlice = createSlice({ + name: "counter", + initialState: { value: 0 }, + reducers: { + increment: (state) => { + state.value += 1; + }, + decrement: (state) => { + state.value -= 1; + }, + }, +}); +``` + +## RTK Query + +`RTK Query`是一个用于管理数据获取的库,它可以帮助我们管理数据获取的状态、缓存、轮询等。平常调用 API 接口时,我们就可以使用`RTK Query`来请求调用。 + +`RTK Query`提供了一些工具函数,比如`createApi`、`createApiSlice`等,用来创建一个`API`。 + +## 创建一个简单的示例 + +了解了上面的基础概念与模板项目后,我们可以来写一个简单的示例。 + +将模板项目的操作抽离出来了,只有单独的一个文件,这样看起来会比较清晰一点。 + +像下面的示例,实现了基本的`incr`和延迟`incr`的功能,同时也实现了一个简单的`API`请求。 + +`RTK Query`我实现了一个简单的`API`请求,获取`products`数据,然后展示在页面上,同时添加了 put 方法,`invalidatesTags`可以使缓存失效。 + +你可以先点击 view 按钮,来回切换多个`product`,你可以看到缓存的效果。当你点击 put 按钮时,会使缓存失效,再次点击 view 按钮,会重新请求数据。 + +```ts +import type { PayloadAction } from "@reduxjs/toolkit"; +import { + asyncThunkCreator, + buildCreateSlice, + combineSlices, + configureStore, +} from "@reduxjs/toolkit"; +import "./App.css"; +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { Provider, useDispatch, useSelector } from "react-redux"; +import { useState } from "react"; +import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; + +// `buildCreateSlice` allows us to create a slice with async thunks. +const createAppSlice = buildCreateSlice({ + creators: { asyncThunk: asyncThunkCreator }, +}); + +interface Product { + id: number; + title: string; + description?: string; +} + +interface ProductsApiResponse { + products: Product[]; + total: number; + skip: number; + limit: number; +} + +// Define a service using a base URL and expected endpoints +const apiSlice = createApi({ + baseQuery: fetchBaseQuery({ + baseUrl: "https://dummyjson.com/", + headers: { + Authorization: + "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTUsInVzZXJuYW1lIjoia21pbmNoZWxsZSIsImVtYWlsIjoia21pbmNoZWxsZUBxcS5jb20iLCJmaXJzdE5hbWUiOiJKZWFubmUiLCJsYXN0TmFtZSI6IkhhbHZvcnNvbiIsImdlbmRlciI6ImZlbWFsZSIsImltYWdlIjoiaHR0cHM6Ly9yb2JvaGFzaC5vcmcvSmVhbm5lLnBuZz9zZXQ9c2V0NCIsImlhdCI6MTcxMzYyNTQ5OSwiZXhwIjoxNzEzNjI3Mjk5fQ.LiRqhAj26lexQJ5kHBxeVFo4ry1zTYnHXSDerrOH7T4", + }, + }), + reducerPath: "api", + // Tag types are used for caching and invalidation. + tagTypes: ["Products"], + endpoints: (build) => ({ + // Supply generics for the return type (in this case `ProductsApiResponse`) + // and the expected query argument. If there is no argument, use `void` + // for the argument type instead. + getProducts: build.query({ + query: (limit = 10) => `/products?limit=${limit}`, + // `providesTags` determines which 'tag' is attached to the + // cached data returned by the query. + providesTags: (result, error, id) => [{ type: "Products", id }], + }), + getProduct: build.query({ + query: (id) => `/products/${id}`, + providesTags: (result, error, id) => [{ type: "Products", id }], + }), + putProduct: build.mutation({ + query: (args: { id: number; title: string }) => ({ + url: "/products/" + args.id, + method: "PUT", + body: { title: args.title }, + }), + invalidatesTags: (result, error, product) => [ + { type: "Products", id: product.id }, + ], + }), + }), +}); + +const { useGetProductsQuery, useGetProductQuery, usePutProductMutation } = + apiSlice; + +// 创建slice +const slice = createAppSlice({ + // slice名 + name: "blog", + // 初始化state + initialState: { + title: "Redux Toolkit", + readCount: 0, + }, + // 创建一个reducer + reducers: (create) => ({ + // 添加阅读数 + incrementReadCount: create.reducer((state) => { + state.readCount++; + }), + // 根据传入的参数增加阅读数 + incrementReadCountBy: create.reducer( + (state, action: PayloadAction) => { + state.readCount += action.payload; + } + ), + // 延迟增加阅读数 + incrementAsyncReadCount: create.asyncThunk( + async (count: number) => { + return new Promise((resolve) => { + setTimeout(() => { + resolve(count); + }, 1000); + }); + }, + { + pending: (state) => {}, + fulfilled: (state, action) => { + state.readCount += action.payload; + }, + rejected: (state) => {}, + } + ), + }), + // 创建一个selector + selectors: { + selectReadCount: (state) => state.readCount, + selectTitle: (state) => state.title, + }, +}); + +// 导出selectors +const { selectReadCount, selectTitle } = slice.selectors; + +// 导出actions +const { incrementReadCount, incrementReadCountBy, incrementAsyncReadCount } = + slice.actions; + +// 创建一个store +const store = configureStore({ + reducer: combineSlices(slice, apiSlice), + // 添加一个中间件 + middleware: (getDefaultMiddleware) => { + return getDefaultMiddleware() + .concat((storeAPI) => { + return (next) => (action) => { + console.log(action); + return next(action); + }; + }) + .concat(apiSlice.middleware); + }, +}); + +// Infer the type of `store` +export type AppStore = typeof store; +// Infer the `AppDispatch` type from the store itself +export type AppDispatch = AppStore["dispatch"]; + +export const useAppDispatch = useDispatch.withTypes(); + +const App = () => { + return ( + + + + ); +}; + +const Blog = () => { + // 使用useSelector来获取数据 + const readCount = useSelector(selectReadCount); + const title = useSelector(selectTitle); + const [incrBy, setIncrBy] = useState(2); + const dispatch = useAppDispatch(); + const data = useGetProductsQuery(10); + const [putProduct, { isLoading }] = usePutProductMutation(); + const [viewId, setViewId] = useState(0); + + return ( +
+

{title}

+

ReadCount: {readCount}

+ setIncrBy(Number(e.target.value))} + /> + + + +
    + {data.isLoading &&
    Loading...
    } + {data.isSuccess && + data.data?.products.map((product) => ( +
    +
  • {product.title}
  • + + +
    + ))} +
+
+ {viewId && } +
+ ); +}; + +const View: React.FC<{ id: number }> = ({ id }) => { + const view = useGetProductQuery(id); + return ( +
+

{view.data?.title}

+
+ ); +}; + +export default App; +``` + +## 最后 + +简单了看了模板项目与官方教程,然后写了一个简单示例,大概的了解了`RTK`的使用,感觉还是挺方便的,尤其是`RTK Query`,下次写项目时有机会用上再深入了解一下。 + +另外感觉`RTK`的文档组织得不太好,有些混乱,而且概念很多,不太容易理解,建议大家多看看官方的示例,多动手写一下。