将 React useReducer 与打字稿一起使用时如何为状态设置通用接口

How to setup a generic interface for the state when using React useReducer with typescript

当我尝试设置状态接口时,我遇到了这个问题useReducer.I 在自定义挂钩中使用它从服务器获取数据。服务器响应的数据对象可能包含不同的属性。 useFetcher.ts

const initialState = {
  data: null,
  loading: false,
  error: null,
};
type LIST_RESPONSE = {
  page: number;
  totalRows: number;
  pageSize: number;
  totalPages: number;
  list: [];
};
interface State {
  data: LIST_RESPONSE| null;
  loading: boolean;
  error: string | null;
}
const reducer = (state: State, action: ACTIONTYPES) => {
  switch (action.type) {
    case ACTIONS.FETCHING: {
      return {
        ...state,
        loading: true,
      };
    }
    case ACTIONS.FETCH_SUCCESS: {
      return {
        ...state,
        loading: false,
        data: action.data,
      };
    }
    case ACTIONS.ERROR: {
      return {
        ...state,
        loading: false,
        error: action.error,
      };
    }
  }
};

const useFetcher = (url: string): State => {
  const [state, dispatch] = useReducer(reducer, initialState);
  const { data, loading, error } = state;

  useEffect(() => {
    dispatch({ type: ACTIONS.FETCHING });
    const fetchData = async () => {
      try {
        const response = await httpService.get(url);
        dispatch({ type: ACTIONS.FETCH_SUCCESS, data: response.data });
        return;
      } catch (error: unknown) {
        const { message } = error as Error;
        dispatch({ type: ACTIONS.ERROR, error: message });
      }
    };

    fetchData();
  }, [url]);
  return { data, loading, error };
};

export default useFetcher;

组件A调用useFetcher:

 const { data, loading, error } = useFetcher('productList');

这很好用,它 return 是适合 LIST_RESPONSE 类型的数据对象。但是如果我想在组件B中使用useFetcher函数,它应该是return另一种数据类型,比方说:

type CATEGORTY_RESPONSE= {
  categories: [];
};

如何使接口状态中的数据类型通用?例如,当我调用 useFecher 挂钩时,我可以在 State 接口中定义数据类型,如下所示: 组件 A:

const { data, loading, error } = useFetcher<LIST_RESPONSE>('productList');

组件 B:

const { data, loading, error } = useFetcher<CATEGORTY_RESPONSE>('categories');

我试过了

interface State {
  data: LIST_RESPONSE|CATEGORTY_RESPONSE| null;
  loading: boolean;
  error: string | null;
}

const useFetcher = <Type extends State>(url: string): Type =>{...}

none 其中有效。 有帮助吗?谢谢

创建通用挂钩

为了使用泛型,您需要在链中的多个点应用该泛型:

  • useFetcher 挂钩需要知道它的状态类型 returns。
  • useReducer 钩子需要知道减速器的类型。
  • reducer 需要知道其状态和操作的类型。
  • State 需要知道其 data 属性 的类型。
  • 成功操作需要知道其 data 属性 的类型。

这是它的样子:

import {useEffect, useReducer, Reducer} from "react";

enum ACTIONS {
    FETCHING = 'FETCHING',
    FETCH_SUCCESS = 'FETCH_SUCCESS',
    ERROR = 'ERROR'
}

type ACTIONTYPES<DataType> = {
    type: ACTIONS.FETCHING
} | {
    type: ACTIONS.ERROR;
    error: string
} | {
    type: ACTIONS.FETCH_SUCCESS;
    data: DataType;
}

const initialState = {
  data: null,
  loading: false,
  error: null,
};

interface State<DataType> {
  data: DataType | null;
  loading: boolean;
  error: string | null;
}

const reducer = <DataType extends any>(state: State<DataType>, action: ACTIONTYPES<DataType>) => {
  switch (action.type) {
    case ACTIONS.FETCHING: {
      return {
        ...state,
        loading: true,
      };
    }
    case ACTIONS.FETCH_SUCCESS: {
      return {
        ...state,
        loading: false,
        data: action.data,
      };
    }
    case ACTIONS.ERROR: {
      return {
        ...state,
        loading: false,
        error: action.error,
      };
    }
    default: {
        return state;
    }
  }
};

const useFetcher = <DataType extends any>(url: string): State<DataType> => {
  const [state, dispatch] = useReducer<Reducer<State<DataType>, ACTIONTYPES<DataType>>>(reducer, initialState);
  const { data, loading, error } = state;

  useEffect(() => {
    dispatch({ type: ACTIONS.FETCHING });
    const fetchData = async () => {
      try {
        const response = await httpService.get(url);
        dispatch({ type: ACTIONS.FETCH_SUCCESS, data: response.data });
        return;
      } catch (error: unknown) {
        const { message } = error as Error;
        dispatch({ type: ACTIONS.ERROR, error: message });
      }
    };

    fetchData();
  }, [url]);
  return { data, loading, error };
};

export default useFetcher;

我没有使用 Type extends State,而是始终使用相同的通用值,它指的是我们从服务器获得的 data 的类型。这是唯一发生变化的类型。

烦人的部分是在 useReducer 上设置泛型,因为这里的泛型需要是 reducer 的类型而不是 状态。 React Reducer 类型定义为 Reducer<S, A>,其中 S 是状态类型,A 是动作类型。所以我们最终得到 Reducer<State<DataType>, ACTIONTYPES<DataType>> 这是你的 reducer 函数的类型,但仅限于特定的 DataType.


使用通用钩子

useFetcher 挂钩是完全通用的,API 响应数据可以是任何东西。我们不仅限于 LIST_RESPONSECATEGORTY_RESPONSE.

例如,您可以这样做:

const { data, loading, error } = useFetcher<string>('someUrl');

并且 data 的类型将被推断为 string | null


定义列表响应

您当前的 LIST_RESPONSE(和 CATEGORTY_RESPONSE)类型有问题。您已将 list 属性 的类型声明为 [],这是一个空数组。你需要说它是一个数组,其元素是特定类型。

如果列表项类型始终相同,那么我们可以为其定义一个类型并使用 list: ListItem[];。如果类型因 URL 而异,那么我们可以使用另一个泛型来描述它。

type LIST_RESPONSE<ItemType> = {
  page: number;
  totalRows: number;
  pageSize: number;
  totalPages: number;
  list: ItemType[];
};

然后我们可以将您的 useFetcher 挂钩用于任何类型的列表。点赞产品列表:

interface Product {
    price: number;
}
const { data, loading, error } = useFetcher<LIST_RESPONSE<Product>>('productList');

if ( data ) {

    // has type Product
    const product = data.list[0];

    // has type number
    const price = product.price;
}

如果这有点令人困惑,那么您可以逐步考虑。

A ProductListResponseLIST_RESPONSE 的特定类型,其中列表项的类型为 Product:

type ProductListResponse = LIST_RESPONSE<Product>

我们的 'productList' 端点 returns data 类型为 ProductListResponse:

const { data, loading, error } = useFetcher<ProductListResponse>('productList');

Typescript Playground Link