阿川私房教材:學程式,拿 offer!

63 個專案實戰,直接上手!
無需補習,按步驟打造你的面試作品。

立即解鎖你的轉職秘笈

標題:使用 useReducer 和 Typescript 反應上下文。

發表:真實

說明:將 React Context API 與強類型化的縮減器和操作一起使用。

標籤: typescript、react、上下文、reducers


只是程式碼嗎?

React 應用程式中有很多處理狀態的選項。顯然你可以使用setState來處理一些小的邏輯,但是如果你有一個複雜的狀態需要管理怎麼辦?

也許您會使用 Redux 或 MobX 來處理這種情況,但也可以選擇使用 React Context,並且您不必安裝其他依賴項。

讓我們看看如何使用 Context API 和 Typescript 來管理複雜的狀態。

在本教程中,我們將建立一個帶有購物車計數器的產品清單。

首先,使用create-react-app建立一個新的 React 專案。

npx create-react-app my-app --template typescript
cd my-app/

接下來,在src目錄中建立一個新的context.tsx檔案。

/*context.tsx*/

import React, { createContext } from 'react';

const AppContext = createContext({});

您可以使用任何您想要的值來初始化上下文 api,就這麼簡單,在本例中,我使用的是一個空物件。

現在讓我們建立一個初始狀態,其中產品清單為零,購物車計數器為零。另外,讓我們為此加入一些類型。

/*context.tsx*/

import React, { createContext } from 'react';

type ProductType = {
  id: number;
  name: string;
  price: number;
}

type InitialStateType = {
  products: ProductType[];
  shoppingCart: number;
}

const initialState = {
  products: [],
  shoppingCart: 0,
}

const AppContext = createContext<InitialStateType>(initialState);

產品清單中的每個產品都有一個 ID、名稱和價格。

現在我們將使用減速器和操作來建立和刪除產品,並將購物車計數器加一。首先,建立一個名為reducers.ts的新檔案。

/*reducers.ts*/

export const productReducer = (state, action) => {
  switch (action.type) {
    case 'CREATE_PRODUCT':
      return [
        ...state,
        {
          id: action.payload.id,
          name: action.payload.name,
          price: action.payload.price,
        }
      ]
    case 'DELETE_PRODUCT':
      return [
        ...state.filter(product => product.id !== action.payload.id),
      ]
    default:
      return state;
  }
}

export const shoppingCartReducer = (state, action) => {
  switch (action.type) {
    case 'ADD_PRODUCT':
      return state + 1;
  }
}

一個reducer函數接收兩個參數,第一個是狀態,我們在使用useReducer鉤子時傳遞,第二個是一個物件,表示事件和一些將改變狀態(動作)的資料。

在本例中,我們建立兩個減速器,一個用於產品,另一個用於購物車。在產品縮減器上,我們新增了兩個操作,一個用於建立新產品,另一個用於刪除任何產品。對於購物車減速器,我們加入的唯一操作是每次加入新產品時增加計數器。

正如您所看到的,為了建立產品,我們傳遞 id、名稱和價格,並使用新物件返回當前狀態。要刪除一個,我們只需要 id ,返回的是狀態,但沒有具有該 id 的產品。

現在讓我們更改上下文文件以導入這些減速器函數。

/*context.tsx*/

import React, { createContext, useReducer } from 'react';
import { productReducer, shoppingCartReducer } from './reducers';

type ProductType = {
  id: number;
  name: string;
  price: number;
}

type InitialStateType = {
  products: ProductType[];
  shoppingCart: number;
}

const intialState = {
  products: [],
  shoppingCart: 0,
}

const AppContext = createContext<{
  state: InitialStateType;
  dispatch: React.Dispatch<any>;
}>({
  state: initialState,
  dispatch: () => null
});

const mainReducer = ({ products, shoppingCart }, action) => ({
  products: productReducer(products, action),
  shoppingCart: shoppingCartReducer(shoppingCart, action),
});

const AppProvider: React.FC = ({ children }) => {
  const [state, dispatch] = useReducer(mainReducer, initialState);

  return (
    <AppContext.Provider value={{state, dispatch}}>
      {children}
    </AppContext.Provider>
  )
}

export { AppContext, AppProvider };

有一個mainReducer函數,它結合了我們將擁有的兩個減速器(產品減速器和購物車減速器),每個減速器管理狀態的選定部分。

此外,我們建立了AppProvider元件,在其中, useReducer鉤子採用mainReducer和初始狀態來傳回statedispatch

我們將這些值傳遞到AppContext.Provider中,這樣做我們可以使用useContext鉤子存取statedispatch

接下來,為減速器和操作加入這些類型。

/*reducers.ts*/

type ActionMap<M extends { [index: string]: any }> = {
  [Key in keyof M]: M[Key] extends undefined
    ? {
        type: Key;
      }
    : {
        type: Key;
        payload: M[Key];
      }
};

export enum Types {
  Create = 'CREATE_PRODUCT',
  Delete = 'DELETE_PRODUCT',
  Add = 'ADD_PRODUCT',
}

// Product

type ProductType = {
  id: number;
  name: string;
  price: number;
}

type ProductPayload = {
  [Types.Create] : {
    id: number;
    name: string;
    price: number;
  };
  [Types.Delete]: {
    id: number;
  }
}

export type ProductActions = ActionMap<ProductPayload>[keyof ActionMap<ProductPayload>];

export const productReducer = (state: ProductType[], action: ProductActions | ShoppingCartActions) => {
  switch (action.type) {
    case Types.Create:
      return [
        ...state,
        {
          id: action.payload.id,
          name: action.payload.name,
          price: action.payload.price,
        }
      ]
    case Types.Delete:
      return [
        ...state.filter(product => product.id !== action.payload.id),
      ]
    default:
      return state;
  }
}

// ShoppingCart

type ShoppingCartPayload = {
  [Types.Add]: undefined;
}

export type ShoppingCartActions = ActionMap<ShoppingCartPayload>[keyof ActionMap<ShoppingCartPayload>];

export const shoppingCartReducer = (state: number, action: ProductActions | ShoppingCartActions) => {
  switch (action.type) {
    case Types.Add:
      return state + 1;
    default:
      return state;
  }
}

我從這篇文章中採用了這種方法,基本上我們正在檢查使用了哪個action.type ,並根據該方法,我們產生有效負載的類型。


筆記

您可以採取的另一種方法是使用像這樣的Discriminated unions

type Action =
 | { type: 'ADD' }
 | { type: 'CREATE', create: object }
 | { type: 'DELETE', id: string };

在前面的程式碼中,所有這些類型都有一個稱為 type 的公共屬性。 Typescript 將為可區分的聯合建立類型保護,並讓我們現在根據我們正在使用的類型以及物件類型具有的其他屬性。

但在本教程中,我們為操作typepayload使用兩個常見屬性,並且payload物件類型會根據type而變化,因此可區分的聯合類型將無法運作。


現在,讓我們將定義的類型匯入到context文件中。

/*context.tsx*/

import React, { createContext, useReducer, Dispatch } from 'react';
import { productReducer, shoppingCartReducer, ProductActions, ShoppingCartActions } from './reducers';

type ProductType = {
  id: number;
  name: string;
  price: number;
}

type InitialStateType = {
  products: ProductType[];
  shoppingCart: number;
}

const initialState = {
  products: [],
  shoppingCart: 0,
}

const AppContext = createContext<{
  state: InitialStateType;
  dispatch: Dispatch<ProductActions | ShoppingCartActions>;
}>({
  state: initialState,
  dispatch: () => null
});

const mainReducer = ({ products, shoppingCart }: InitialStateType, action: ProductActions | ShoppingCartActions) => ({
  products: productReducer(products, action),
  shoppingCart: shoppingCartReducer(shoppingCart, action),
});

const AppProvider: React.FC = ({ children }) => {
  const [state, dispatch] = useReducer(mainReducer, initialState);

  return (
    <AppContext.Provider value={{state, dispatch}}>
      {children}
    </AppContext.Provider>
  )
}

export { AppProvider, AppContext };

不要忘記用AppProvider包裝您的主要元件。

/* App.tsx */

import React from 'react';
import { AppProvider } from './context';
import Products from './products';

const App = () => {
  <AppProvider>
    // your stuff
    <Products />
  </AppProvider>
}

export default App

建立一個Products元件並在其中加入以下程式碼。

/* Products.tsx */

import React, { useContext } from 'react';
import { AppContext } from './context';
import { Types } from './reducers';

const Products = () => {
  const { state, dispatch } = useContex(AppContext);

  return (
    <div>
      <button onClick={() => {
        dispatch({
          type: Types.Add,
        })
      }}>
        click
        </button>
      {state.shoppingCart}
    </div>
  )
}

export default Products;

現在一切都是強類型的。

您可以在此處查看程式碼。

來源。

https://medium.com/hackernoon/finally-the-typescript-redux-hooks-events-blog-you-were-looking-for-c4663d823b01


原文出處:https://dev.to/elisealcala/react-context-with-usereducer-and-typescript-4obm


共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。

阿川私房教材:學程式,拿 offer!

63 個專案實戰,直接上手!
無需補習,按步驟打造你的面試作品。

立即解鎖你的轉職秘笈