blog/docs/dev/frontend/redux-toolkit.md
王一之 2a2684fbf9
All checks were successful
Release / deploy (push) Successful in 4m59s
update rtk 入门
2024-04-20 23:42:55 +08:00

396 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`来管理数据获取,这样不用自己封装,也不用担心缓存等问题。
## 创建模板
首先,我们使用官方提供的模板来创建一个项目:
```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<ProductsApiResponse, number>({
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<Product, number>({
query: (id) => `/products/${id}`,
providesTags: (result, error, id) => [{ type: "Products", id }],
}),
putProduct: build.mutation<Product, Product>({
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<number>) => {
state.readCount += action.payload;
}
),
// 延迟增加阅读数
incrementAsyncReadCount: create.asyncThunk(
async (count: number) => {
return new Promise<number>((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<AppDispatch>();
const App = () => {
return (
<Provider store={store}>
<Blog />
</Provider>
);
};
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<number>(0);
return (
<div className="App">
<h1>{title}</h1>
<h2>ReadCount: {readCount}</h2>
<input
value={incrBy}
onChange={(e) => setIncrBy(Number(e.target.value))}
/>
<button
onClick={() => {
dispatch(incrementReadCount());
}}
>
read
</button>
<button onClick={() => dispatch(incrementReadCountBy(incrBy))}>
read by
</button>
<button onClick={() => dispatch(incrementAsyncReadCount(incrBy))}>
async read by
</button>
<ul>
{data.isLoading && <div>Loading...</div>}
{data.isSuccess &&
data.data?.products.map((product) => (
<div>
<li key={product.id}>{product.title}</li>
<button
onClick={() => {
setViewId(product.id);
}}
>
view
</button>
<button
onClick={() => {
// 变更
putProduct({
id: product.id,
title: product.title + " New",
});
}}
>
put
</button>
</div>
))}
</ul>
<br />
{viewId && <View id={viewId} />}
</div>
);
};
const View: React.FC<{ id: number }> = ({ id }) => {
const view = useGetProductQuery(id);
return (
<div>
<h1>{view.data?.title}</h1>
</div>
);
};
export default App;
```
## 最后
简单了看了模板项目与官方教程,然后写了一个简单示例,大概的了解了`RTK`的使用,感觉还是挺方便的,尤其是`RTK Query`,下次写项目时有机会用上再深入了解一下。
另外感觉`RTK`的文档组织得不太好,有些混乱,而且概念很多,不太容易理解,建议大家多看看官方的示例,多动手写一下。