上一主题 下一主题
ScriptCat,新一代的脚本管理器脚本站,与全世界分享你的用户脚本油猴脚本开发指南教程目录
返回列表 发新帖

Redux Toolkit 入门

[复制链接]
  • TA的每日心情
    开心
    2024-3-13 10:14
  • 签到天数: 211 天

    [LV.7]常住居民III

    296

    主题

    3925

    回帖

    3844

    积分

    管理员

    积分
    3844

    管理员荣誉开发者油中2周年生态建设者喜迎中秋油中3周年挑战者 lv2

    发表于 2024-4-20 23:43:52 | 显示全部楼层 | 阅读模式

    原文:https://blog.icodef.com/dev/frontend/redux-toolkit

    Redux Toolkit 入门

    在此之前,我的 React 从来都是一把梭,可能是项目写得比较小的原因,也没感觉到过什么问题。

    上一次自己手动封装了一个useFetch请求 API 获取数据,感觉这种写法还不错,觉得那些库是不是有更好的解决方案,于是来了解一下Redux Toolkit

    官方已经不推荐使用redux了,而是推荐使用redux-toolkitRTKredux进行了封装,提供了一些工具函数,让我们更方便地使用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来启动项目,可以看到一个这样的页面

    image.png

    可以进入src/App.tsx文件里看一下,具体的实现代码。

    另外,我们也可以下载Redux DevTools这个浏览器扩展来查看 Redux 的状态变化。

    基础概念

    我们根据上述创建的模板项目来学习下面的一些概念。

    Store

    store是一个对象,它包含了应用中所有的state,在 Redux 中,只会有一个store。在示例中处于:src/app/store.ts文件中。

    我们可以使用configureStore来创建一个store,并且可以传入一些参数,比如reducermiddleware等。configureStorecreateStore的一个封装。

    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是一个函数,它接收两个参数:stateaction,然后返回一个新的state。它根据actiontype来决定如何更新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是一个对象,它包含了reduceractionselectorslice可以用来封装reduceractionselector,然后导出给其他模块使用。

    可以使用createSlice来创建一个slice,它接收一个对象,包含nameinitialStatereducers等字段。

    例如:

    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提供了一些工具函数,比如createApicreateApiSlice等,用来创建一个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的文档组织得不太好,有些混乱,而且概念很多,不太容易理解,建议大家多看看官方的示例,多动手写一下。

    上不慕古,下不肖俗。为疏为懒,不敢为狂。为拙为愚,不敢为恶。/ 微信公众号:一之哥哥

    发表回复

    本版积分规则

    快速回复 返回顶部 返回列表