原文:https://blog.icodef.com/dev/frontend/redux-toolkit
Redux Toolkit 入门
在此之前,我的 React 从来都是一把梭,可能是项目写得比较小的原因,也没感觉到过什么问题。
上一次自己手动封装了一个useFetch
请求 API 获取数据,感觉这种写法还不错,觉得那些库是不是有更好的解决方案,于是来了解一下Redux Toolkit。
官方已经不推荐使用redux
了,而是推荐使用redux-toolkit
,RTK
对redux
进行了封装,提供了一些工具函数,让我们更方便地使用redux
。它们主要用于状态管理。当应用中有很多组件需要共享状态时,就可以使用redux
来管理这些状态。
平常使用useContext
也是足够的,但是当应用变得复杂时,就需要使用redux
这类的框架来管理状态了,虽然我还没遇到这么复杂的应用,但是简单的了解一下也是不错的。
在请求 API 接口时,我们也可以使用RTK Query
来管理数据获取,这样不用自己封装,也不用担心缓存等问题。
创建模板
首先,我们使用官方提供的模板来创建一个项目:
npx degit reduxjs/redux-templates/packages/vite-template-redux my-app
cd my-app
npm i
创建完成后,我们就可以使用npm start
来启动项目,可以看到一个这样的页面
可以进入src/App.tsx
文件里看一下,具体的实现代码。
另外,我们也可以下载Redux DevTools这个浏览器扩展来查看 Redux 的状态变化。
基础概念
我们根据上述创建的模板项目来学习下面的一些概念。
Store
store
是一个对象,它包含了应用中所有的state
,在 Redux 中,只会有一个store
。在示例中处于:src/app/store.ts
文件中。
我们可以使用configureStore
来创建一个store
,并且可以传入一些参数,比如reducer
、middleware
等。configureStore
是createStore
的一个封装。
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
的数据。
例如:
const addTodoAction = {
type: "todos/todoAdded",
payload: "Buy milk",
};
Reducer
reducer
是一个函数,它接收两个参数:state
和action
,然后返回一个新的state
。它根据action
的type
来决定如何更新state
。
例如:
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
。
例如:
store.dispatch({ type: "counter/increment" });
Selector
selector
是一个函数,它用来从state
中获取数据。selector
可以接收state
作为参数,然后返回需要的数据。当state
过于复杂时,我们可以使用selector
来简化获取数据的过程。
例如:
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
等字段。
例如:
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 按钮,会重新请求数据。
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
的文档组织得不太好,有些混乱,而且概念很多,不太容易理解,建议大家多看看官方的示例,多动手写一下。