Как правильно создать интерфейс для объекта действия с реагирующими хуками и машинописью - PullRequest
0 голосов
/ 15 января 2020

Я работаю с реагирующими хуками и машинописью. Я использовал useReducer() для глобального состояния. Действие функции редуктора содержит два свойства name и data. name означает название события или изменения, а data будет определенными данными, необходимыми для этого конкретного имени.

До сих пор существует четыре значения имени. Если имя "setUserData", то data должно IUserData (интерфейс). Если имя setDialog, то data должно DialogNames (тип, содержащий две строки). И если это что-то другое, то данные не требуются.

//different names of dialog.
export type DialogNames = "RegisterFormDialog" | "LoginFormDialog" | "";

//type for name property in action object
type GlobalStateActionNames =
   | "startLoading"
   | "stopLoading"
   | "setUserData"
   | "setDialog";

//interface for main global state object.
export interface IGlobalState {
   loading: boolean;
   userData: IUserData;
   dialog: DialogNames;
}

interface IUserData {
   loggedIn: boolean;
   name: string;
}
//The initial global state
export const initialGlobalState: IGlobalState = {
   loading: false,
   userData: { loggedIn: false, name: "" },
   dialog: ""
};

//The reducer function which is used in `App` component.
export const GlobalStateReducer = (
   state: IGlobalState,
   { name, data }: IGlobalStateAction
): IGlobalState => {
   switch (name) {
      case "startLoading":
         return { ...state, loading: true };
      case "stopLoading":
         return { ...state, loading: false };
      case "setUserData":
         return { ...state, userData: { ...state.userData, ...data } };
      case "setDialog":
         return { ...state, dialog: data };
      default:
         return state;
   }
};

//The interface object which is passed from GlobalContext.Provider as "value"
export interface GlobalContextState {
   globalState: IGlobalState;
   dispatchGlobal: React.Dispatch<IGlobalStateAction<GlobalStateActionNames>>;
}

//intital state which is passed to `createContext`
export const initialGlobalContextState: GlobalContextState = {
   globalState: initialGlobalState,
   dispatchGlobal: function(){}
};

//The main function which set the type of data based on the generic type passed.
export interface IGlobalStateAction<
   N extends GlobalStateActionNames = GlobalStateActionNames
> {
   data?: N extends "setUserData"
      ? IUserData
      : N extends "setDialog"
      ? DialogNames
      : any;
   name: N;
}

export const GlobalContext = React.createContext(initialGlobalContextState);

Мой <App> компонент выглядит так.

const App: React.SFC = () => {
   const [globalState, dispatch] = React.useReducer(
      GlobalStateReducer,
      initialGlobalState
   );


   return (
      <GlobalContext.Provider
         value={{
            globalState,
            dispatchGlobal: dispatch
         }}
      >
         <Child></Child>
      </GlobalContext.Provider>
   );
};

Приведенный выше подход хорош. Я должен использовать его, как показано ниже, в <Child>

dispatchGlobal({
   name: "setUserData",
   data: { loggedIn: false }
} as IGlobalStateAction<"setUserData">);

Проблема в подходе выше состоит в том, что он делает код немного длиннее. И вторая проблема заключается в том, что я должен импортировать IGlobalStateAction без причины, по которой мне когда-либо приходилось использовать dispatchGlobal

Есть ли способ, которым я мог бы только сказать, name и data автоматически назначается для исправления типа или любой другой лучший способ. Пожалуйста, проведите к правильному пути.

1 Ответ

1 голос
/ 15 января 2020

Использование useReducer с машинописью немного сложно, потому что, как вы упомянули, параметры редуктора меняются в зависимости от того, какое действие вы предпринимаете.

Я придумал шаблон, в котором вы используете классы для реализации. ваши действия. Это позволяет передавать типобезопасные параметры в конструктор класса и по-прежнему использовать суперкласс класса в качестве типа параметра редуктора. Звучит, вероятно, более сложно, чем это, вот пример:

interface Action<StateType> {
  execute(state: StateType): StateType;
}

// Your global state
type MyState = {
  loading: boolean;
  message: string;
};

class SetLoadingAction implements Action<MyState> {
  // this is where you define the parameter types of the action
  constructor(private loading: boolean) {}
  execute(currentState: MyState) {
    return {
      ...currentState,
      // this is how you use the parameters
      loading: this.loading
    };
  }
}

Поскольку логи обновления состояния c теперь инкапсулированы в метод класса 'execute, редуктор теперь только такой маленький:

const myStateReducer = (state: MyState, action: Action<MyState>) => action.execute(state);

Компонент, использующий этот редуктор, может выглядеть следующим образом:

const Test: FunctionComponent = () => {
  const [state, dispatch] = useReducer(myStateReducer, initialState);

  return (
    <div>
      Loading: {state.loading}
      <button onClick={() => dispatch(new SetLoadingAction(true))}>Set Loading to true</button>
      <button onClick={() => dispatch(new SetLoadingAction(false))}>Set Loading to false</button>
    </div>
  );
}

Если вы используете этот шаблон, ваши действия инкапсулируют логи обновления состояния c в своем методе execute, который (в мое мнение) масштабируется лучше, так как вы не получите редуктор с огромным распределительным шкафом. Вы также полностью безопасны для типов, так как типы входных параметров определяются конструктором действия, а редуктор может просто принять любую реализацию интерфейса Action.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...