• [React Redux] Redux toolkit project notes


    Config store

    app/store.ts

    import { configureStore } from "@reduxjs/toolkit";
    
    export const store = configureStore({ reducer: {} });
    

    main.tsx

    import React from "react";
    import ReactDOM from "react-dom";
    + import { Provider } from "react-redux";
    import "./index.css";
    import App from "./App";
    + import { store } from "./app/store";
    
    ReactDOM.render(
      <React.StrictMode>
    +    <Provider store={store}>
          <App />
    +    </Provider>
      </React.StrictMode>,
      document.getElementById("root")
    );
    

    Slices

    Slice is a concept that each slice needs to own the shape of its part of the data and is generally responsible for owning any reducers, selectors or thunks that primarily access or maniulate that information.

    app/features/cart/cartSlice.ts

    import { createSlice } from "@reduxjs/toolkit";
    
    export interface CartState {
      items: { [producetID: string]: number };
    }
    
    const initialState: CartState = {
      items: {},
    };
    
    const cartSlice = createSlice({
      name: "cart",
      initialState,
      reducers: {},
    });
    
    export default cartSlice.reducer;
    

    app/features/products/productsSlice.ts

    import { createSlice } from "@reduxjs/toolkit";
    import { Product } from "../../app/api";
    
    export interface ProductsState {
      products: { [id: string]: Product };
    }
    
    const initialState: ProductsState = {
      products: {},
    };
    
    const productsSlice = createSlice({
      name: "products",
      initialState,
      reducers: {},
    });
    
    export default productsSlice.reducer;
    

    app/store.ts

    import { configureStore } from "@reduxjs/toolkit";
    + import cartReducer from "../features/cart/cartSlice";
    + import productsReducer from "../features/products/productsSlice";
    
    export const store = configureStore({
      reducer: {
    +    cart: cartReducer,
    +    products: productsReducer,
      },
    });
    

    Type-aware hooks

    app/store.ts

    import { configureStore } from "@reduxjs/toolkit";
    import cartReducer from "../features/cart/cartSlice";
    import productsReducer from "../features/products/productsSlice";
    
    export const store = configureStore({
      reducer: {
        cart: cartReducer,
        products: productsReducer,
      },
    });
    
    + export type RootState = ReturnType<typeof store.getState>;
    + export type AppDispatch = typeof store.dispatch;
    

    app/hooks.ts

    import { TypedUseSelectorHook, useSelector, useDispatch } from "react-redux";
    import { AppDispatch, RootState } from "./store";
    
    export const useAppDispatch = () => useDispatch<AppDispatch>();
    export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
    

    update to use useAppSelector in app/features/products/Products.tsx

    - import React, { useEffect, useState } from "react";
    - import { getProducts, Product } from "../../app/api";
    + import React from "react";
    import styles from "./Products.module.css";
    + import { useAppSelector } from "../../app/hooks";
    
    export function Products() {
    +  const products = useAppSelector((state) => state.products.products);
    -  const [products, setProducts] = useState<Product[]>([]);
    -  useEffect(() => {
    -    getProducts().then((products) => {
    -      setProducts(products);
    -    });
    -  }, []);
      return (
        <main className="page">
          <ul className={styles.products}>
    -        {products.map((product) => (
    +        {Object.values(products).map((product) => (
              <li key={product.id}>
                <article className={styles.product}>
                  <figure>
                    <img src={product.imageURL} alt={product.imageAlt} />
                    <figcaption className={styles.caption}>
                      {product.imageCredit}
                    </figcaption>
                  </figure>
                  <div>
                    <h1>{product.name}</h1>
                    <p>{product.description}</p>
                    <p>${product.price}</p>
                    <button>Add to Cart </button>
                  </div>
                </article>
              </li>
            ))}
          </ul>
        </main>
      );
    }
    

    First reducer method

    app/features/products/productsSlice.ts

    import { createSlice, PayloadAction } from "@reduxjs/toolkit";
    import { Product } from "../../app/api";
    
    export interface ProductsState {
      products: { [id: string]: Product };
    }
    
    const initialState: ProductsState = {
      products: {},
    };
    
    const productsSlice = createSlice({
      name: "products",
      initialState,
      reducers: {
    +    receivedProducts(state, action: PayloadAction<Product[]>) {
    +      const products = action.payload;
    +      products.forEach((product) => {
    +        state.products[product.id] = product;
    +      });
    +    },
      },
    });
    
    + export const { receivedProducts } = productsSlice.actions;
    export default productsSlice.reducer;
    

    app/feature/Products.tsx

    export function Products() {
      const dispatch = useAppDispatch();
      useEffect(() => {
        getProducts().then((products) => {
    +      dispatch(receivedProducts(products));
        });
      });
    
      ...
    

    Using Adapter

    Entity adapter docs

    import {
      createSlice,
      PayloadAction,
      createEntityAdapter,
    } from "@reduxjs/toolkit";
    import { Product } from "../../app/api";
    import { RootState } from "../../app/store";
    
    export interface ProductsState {
      products: { [id: string]: Product };
    }
    
    + const productsAdapter = createEntityAdapter<Product>({
    +  selectId: (product) => product.id,
    + });
    
    const productsSlice = createSlice({
      name: "products",
    +  initialState: productsAdapter.getInitialState(),
      reducers: {
        receivedProducts(state, action: PayloadAction<Product[]>) {
          const products = action.payload;
    +      productsAdapter.setAll(state, products);
        },
      },
    });
    
    + const productsSelector = productsAdapter.getSelectors<RootState>(
    +   (state) => state.products
    + );
    + export const { selectAll } = productsSelector;
    export const { receivedProducts } = productsSlice.actions;
    export default productsSlice.reducer;
    

    app/feature/Products.tsx

    import React, { useEffect } from "react";
    import styles from "./Products.module.css";
    import { receivedProducts } from "./productsSlice";
    import * as productSlice from "./productsSlice";
    import { useAppSelector, useAppDispatch } from "../../app/hooks";
    import { getProducts } from "../../app/api";
    
    export function Products() {
      const dispatch = useAppDispatch();
      useEffect(() => {
        getProducts().then((products) => {
          dispatch(receivedProducts(products));
        });
      });
    +  const products = useAppSelector(productSlice.selectAll);
      return (
        <main className="page">
          <ul className={styles.products}>
    +        {products.map((product) => {
              return (
                product && (
                  <li key={product.id}>
                    <article className={styles.product}>
                      <figure>
                        <img src={product.imageURL} alt={product.imageAlt} />
                        <figcaption className={styles.caption}>
                          {product.imageCredit}
                        </figcaption>
                      </figure>
                      <div>
                        <h1>{product.name}</h1>
                        <p>{product.description}</p>
                        <p>${product.price}</p>
                        <button>Add to Cart </button>
                      </div>
                    </article>
                  </li>
                )
              );
            })}
          </ul>
        </main>
      );
    }
    

    Another flow example

    app/features/cart/cartSlice.ts

    import { createSlice, PayloadAction } from "@reduxjs/toolkit";
    import { RootState } from "../../app/store";
    
    export interface CartState {
      items: { [producetID: string]: number };
    }
    
    const initialState: CartState = {
      items: {},
    };
    
    const cartSlice = createSlice({
      name: "cart",
      initialState,
      reducers: {
        addToCart(state, action: PayloadAction<string>) {
          const id = action.payload;
          if (state.items[id]) {
            state.items[id]++;
          } else {
            state.items[id] = 1;
          }
        },
      },
    });
    
    export const { addToCart } = cartSlice.actions;
    export default cartSlice.reducer;
    
    export function getNumItems(state: RootState) {
      let numItems = 0;
      for (let id in state.cart.items) {
        numItems += state.cart.items[id];
      }
      return numItems;
    }
    

    app/features/products/Products.tsx

    import { addToCart } from "../cart/cartSlice";
    ...
    <button onClick={() => dispatch(addToCart(product.id))}>Add to Cart </button>;
    

    app/features/products/Products.tsx

    import React from "react";
    import { Link } from "react-router-dom";
    import styles from "./CartLink.module.css";
    + import { getNumItems } from "./cartSlice";
    + import { useAppSelector } from "../../app/hooks";
    
    export function CartLink() {
    +  const numItems = useAppSelector(getNumItems);
      return (
        <Link to="/cart" className={styles.link}>
          <span className={styles.text}>
    +        &nbsp;&nbsp;{numItems ? numItems : "Cart"}
          </span>
        </Link>
      );
    }
    

    createSelector

    In previous section we wrote

    export function getNumItems(state: RootState) {
      let numItems = 0;
      for (let id in state.cart.items) {
        numItems += state.cart.items[id];
      }
      return numItems;
    }
    

    This function actually get called whenever store get updated. But we only want to call this function when items in cart changes.

    app/features/cart/cartSlice.ts

    import { createSlice, PayloadAction, createSelector } from "@reduxjs/toolkit";
    ...
    export const getNumItemsMemo = createSelector(
      (state: RootState) => state.cart.items,
      (items) => {
        let numItems = 0;
        for (let id in items) {
          numItems += items[id];
        }
        return numItems;
      }
    );
    

    Aggregate values from multi slices

    app/features/prodcuts/productsSlice.ts

    + export const { selectAll, selectEntities } = productsSelector;
    

    app/features/cart/cartSlice.ts

    export const getTotalPrice = createSelector(
      (state: RootState) => state.cart.items,
      productsSlice.selectEntities,
      (items, products) => {
        let total = 0;
        for (let id in items) {
          total += (products[id]?.price ?? 0) * items[id];
        }
        return total.toFixed(2);
      }
    );
    

    extraReducers

    Everything we added to reducers will be exported to Action.

    // Slices
    const cartSlice = createSlice({
      name: "cart",
      initialState,
      reducers: {
        addToCart(state, action: PayloadAction<string>) {
          const id = action.payload;
          if (state.items[id]) {
            state.items[id]++;
          } else {
            state.items[id] = 1;
          }
        },
        removeFromCart(state, action: PayloadAction<string>) {
          const id = action.payload;
          delete state.items[id];
        },
        updateQuantity(
          state,
          action: PayloadAction<{ id: string; quantity: number }>
        ) {
          const { id, quantity } = action.payload;
          state.items[id] = quantity;
        },
      },
    });
    // Actions
    export const { addToCart, removeFromCart, updateQuantity } = cartSlice.actions;
    

    So what is some action you want custom action creator or you don't want action being created automatically.

    app/features/cart/cartSlice.ts

    // Slices
    const cartSlice = createSlice({
      name: "cart",
      initialState,
      reducers: {
        addToCart(state, action: PayloadAction<string>) {
          const id = action.payload;
          if (state.items[id]) {
            state.items[id]++;
          } else {
            state.items[id] = 1;
          }
        },
        removeFromCart(state, action: PayloadAction<string>) {
          const id = action.payload;
          delete state.items[id];
        },
        updateQuantity(
          state,
          action: PayloadAction<{ id: string; quantity: number }>
        ) {
          const { id, quantity } = action.payload;
          state.items[id] = quantity;
        },
      },
    +  extraReducers: function (builder) {
    +    builder.addCase("cart/checkout/pending", (state, action) => {
    +      state.checkoutState = "LOADING";
    +    });
    +  },
    });
    

    app/features/cart/Cart.tsx

    function onCheckout(e: React.FormEvent<HTMLFormElement>) {
      e.preventDefault();
      dispatch({
        type: "cart/checkout/pending",
      });
    }
    
    <form onSubmit={onCheckout}>
      <button className={styles.button} type="submit">
        Checkout
      </button>
    </form>;
    

    Thunk

    Redux toolkit has intergated thunk already.

    app/features/cart/cartSlice.ts

    extraReducers: function (builder) {
      builder.addCase("cart/checkout/pending", (state, action) => {
        state.checkoutState = CheckoutEnmus.LOADING;
      });
      builder.addCase("cart/checkout/fulfilled", (state, action) => {
        state.checkoutState = CheckoutEnmus.READY;
      });
    },
    // Thunks
    export function checkout() {
      return function checkoutThunk(dispatch: AppDispatch) {
        dispatch({
          type: "cart/checkout/pending",
        });
    
        setTimeout(() => {
          dispatch({
            type: "cart/checkout/fulfilled",
          });
        }, 3000);
      };
    }
    

    app/features/cart/Cart.tsx

    import {
      getTotalPrice,
      removeFromCart,
      updateQuantity,
      getCheckoutState,
      CheckoutEnmus,
      checkout,
    } from "./cartSlice";
    
    function onCheckout(e: React.FormEvent<HTMLFormElement>) {
      e.preventDefault();
      dispatch(checkout());
    }
    
    <form onSubmit={onCheckout}>
      <button className={styles.button} type="submit">
        Checkout
      </button>
    </form>;
    

    CreateAsyncThunk

    The key reason to use createAsyncThunk is that it generates actions for each of the different outcomes for any promised-based async call: pending, fulfilled, and rejected. We then have to use the builder callback API on the extraReducers property to map these actions back into reducer methods we then use to update our state. It's a bit of of a process but it's simpler than the alternative, which is to create a bunch of actions by hand and manually dispatch them.

    app/features/cart/cartSlice.ts

    import {
      createSlice,
    +  createAsyncThunk,
      PayloadAction,
      createSelector,
    } from "@reduxjs/toolkit";
    import { RootState, AppDispatch } from "../../app/store";
    import * as productsSlice from "../products/productsSlice";
    + import { checkout, CartItems } from "../../app/api";
    
    export enum CheckoutEnmus {
      LOADING = "LOADING",
      READY = "READY",
      ERROR = "ERROR",
    }
    type CheckoutState = keyof typeof CheckoutEnmus;
    
    export interface CartState {
      items: { [producetID: string]: number };
      checkoutState: CheckoutState;
    }
    
    const initialState: CartState = {
      items: {},
      checkoutState: "READY",
    };
    
    // Slices
    const cartSlice = createSlice({
      name: "cart",
      initialState,
      reducers: {
        addToCart(state, action: PayloadAction<string>) {
          const id = action.payload;
          if (state.items[id]) {
            state.items[id]++;
          } else {
            state.items[id] = 1;
          }
        },
        removeFromCart(state, action: PayloadAction<string>) {
          const id = action.payload;
          delete state.items[id];
        },
        updateQuantity(
          state,
          action: PayloadAction<{ id: string; quantity: number }>
        ) {
          const { id, quantity } = action.payload;
          state.items[id] = quantity;
        },
      },
      extraReducers: function (builder) {
    +    builder.addCase(checkoutCart.pending, (state, action) => {
          state.checkoutState = CheckoutEnmus.LOADING;
        });
    +    builder.addCase(checkoutCart.fulfilled, (state, action) => {
          state.checkoutState = CheckoutEnmus.READY;
        });
    +    builder.addCase(checkoutCart.rejected, (state, action) => {
    +      state.checkoutState = CheckoutEnmus.ERROR;
    +    });
      },
    });
    
    // Thunks
    - export function checkout() {
    -   return function checkoutThunk(dispatch: AppDispatch) {
    -     dispatch({
    -       type: "cart/checkout/pending",
    -     });
    -     setTimeout(() => {
    -       dispatch({
    -         type: "cart/checkout/fulfilled",
    -       });
    -     }, 3000);
    -   };
    - }
    
    + export const checkoutCart = createAsyncThunk(
    +   "cart/checkout",
    +   async (items: CartItems) => {
    +     const response = await checkout(items);
    +     return response;
    +   }
    + );
    
    // Actions
    export const { addToCart, removeFromCart, updateQuantity } = cartSlice.actions;
    
    // Selectors
    export const getCart = (state: RootState) => state.cart;
    export const getCartItems = createSelector(getCart, (cart) => cart.items);
    export const getCartItemsIds = createSelector(getCartItems, (items) =>
      Object.keys(items)
    );
    export const getItemCounts = createSelector(getCartItems, (items) =>
      Object.values(items)
    );
    export const getNumItems = createSelector(getItemCounts, (counts) =>
      counts.reduce((acc, count) => acc + count, 0)
    );
    export const getTotalPrice = createSelector(
      getCartItems,
      productsSlice.selectEntities,
      (items, products) =>
        Object.keys(items)
          .reduce((total, id) => total + (products[id]?.price ?? 0) * items[id], 0)
          .toFixed(2)
    );
    export const getCheckoutState = createSelector(
      getCart,
      (cart) => cart.checkoutState
    );
    
    // Reducer
    export default cartSlice.reducer;
    

    It generates promise lifecycle action types based on the action type prefix that you pass in, and returns a thunk action creator that will run the promise callback and dispatch the lifecycle actions based on the returned promise.

    checkoutCart.pending;
    checkoutCart.fulfilled;
    checkoutCart.rejected;
    

    Error message for Async Thunk action

    app/features/cart/cartSlice.ts

    import {
      createSlice,
      createAsyncThunk,
      PayloadAction,
      createSelector,
    } from "@reduxjs/toolkit";
    import { RootState, AppDispatch } from "../../app/store";
    import * as productsSlice from "../products/productsSlice";
    import { checkout, CartItems } from "../../app/api";
    
    export enum CheckoutEnmus {
      LOADING = "LOADING",
      READY = "READY",
      ERROR = "ERROR",
    }
    type CheckoutState = keyof typeof CheckoutEnmus;
    
    export interface CartState {
      items: { [producetID: string]: number };
      checkoutState: CheckoutState;
    +  errorMessage: string;
    }
    
    const initialState: CartState = {
      items: {},
      checkoutState: "READY",
    +  errorMessage: "",
    };
    
    // Slices
    const cartSlice = createSlice({
      name: "cart",
      initialState,
      reducers: {
        addToCart(state, action: PayloadAction<string>) {
          const id = action.payload;
          if (state.items[id]) {
            state.items[id]++;
          } else {
            state.items[id] = 1;
          }
        },
        removeFromCart(state, action: PayloadAction<string>) {
          const id = action.payload;
          delete state.items[id];
        },
        updateQuantity(
          state,
          action: PayloadAction<{ id: string; quantity: number }>
        ) {
          const { id, quantity } = action.payload;
          state.items[id] = quantity;
        },
      },
      extraReducers: function (builder) {
        builder.addCase(checkoutCart.pending, (state) => {
          state.checkoutState = CheckoutEnmus.LOADING;
        });
        builder.addCase(checkoutCart.fulfilled, (state) => {
          state.checkoutState = CheckoutEnmus.READY;
        });
    +    // action for rejected promise has a payload of type Error
    +    builder.addCase(checkoutCart.rejected, (state, action) => {
    +      state.checkoutState = CheckoutEnmus.ERROR;
    +      state.errorMessage = action.error.message || "";
    +    });
      },
    });
    
    // Thunks
    export const checkoutCart = createAsyncThunk(
      "cart/checkout",
      async (items: CartItems) => {
        const response = await checkout(items);
        return response;
      }
    );
    
    // Actions
    export const { addToCart, removeFromCart, updateQuantity } = cartSlice.actions;
    
    // Selectors
    export const getCart = (state: RootState) => state.cart;
    export const getCartItems = createSelector(getCart, (cart) => cart.items);
    export const getCartItemsIds = createSelector(getCartItems, (items) =>
      Object.keys(items)
    );
    export const getItemCounts = createSelector(getCartItems, (items) =>
      Object.values(items)
    );
    export const getNumItems = createSelector(getItemCounts, (counts) =>
      counts.reduce((acc, count) => acc + count, 0)
    );
    export const getTotalPrice = createSelector(
      getCartItems,
      productsSlice.selectEntities,
      (items, products) =>
        Object.keys(items)
          .reduce((total, id) => total + (products[id]?.price ?? 0) * items[id], 0)
          .toFixed(2)
    );
    export const getCheckoutState = createSelector(
      getCart,
      (cart) => cart.checkoutState
    );
    + export const getCartErrorMessage = createSelector(
    +   getCart,
    +   (cart) => cart.errorMessage
    + );
    
    // Reducer
    export default cartSlice.reducer;
    

    `app/features/Cart.tsx``

    import {
      getTotalPrice,
      removeFromCart,
      updateQuantity,
      getCheckoutState,
      CheckoutEnmus,
      checkoutCart,
      getCartErrorMessage,
    } from "./cartSlice";
    ...
      <form onSubmit={onCheckout}>
        {checkoutState === "ERROR" && errorMessage ? (
          <p className={styles.errorBox}>{errorMessage}</p>
        ) : null}
        <button className={styles.button} type="submit">
          Checkout
        </button>
      </form>
    

    Global State inside of Async Thunks

    // export const checkoutCart = createAsyncThunk(
    //   "cart/checkout",
    //   async (items: CartItems) => {
    //     const response = await checkout(items);
    //     return response;
    //   }
    // );
    
    export const checkoutCart = createAsyncThunk(
      "cart/checkout",
      async (_, thunkAPI) => {
        const state = thunkAPI.getState() as RootState;
        const items = state.cart.items;
        const response = await checkout(items);
        return response;
      }
    );
    

    From Course
    git clone git@github.com:xjamundx/redux-shopping-cart.git

  • 相关阅读:
    win10 uwp 如何判断一个对象被移除
    win10 uwp 如何判断一个对象被移除
    上传代码 CodePlex
    上传代码 CodePlex
    如何使用 Q#
    让 AE 输出 MPEG
    让 AE 输出 MPEG
    解决 vs 出现Error MC3000 给定编码中的字符无效
    解决 vs 出现Error MC3000 给定编码中的字符无效
    PHP date_date_set() 函数
  • 原文地址:https://www.cnblogs.com/Answer1215/p/16183129.html
Copyright © 2020-2023  润新知