使用 Typescript 将 React 和 Redux 提升到新的水平

2025-05-28

使用 Typescript 将 React 和 Redux 提升到新的水平

序幕

如果你曾经使用过 Redux,你就会知道,我们编写 Redux 逻辑的方式以及它的工作机制很大程度上依赖于我们提前了解状态的结构。这种需求与优秀的 TypeScript 代码在构建输出 JavaScript 代码之前强制我们定义函数和变量的结构非常相似。

由于我近期会频繁使用 Redux,而且已经有一段时间没怎么用它了,所以我决定学习一下 Level Up Tutorials (LUT) 的《React and Redux For Everyone》课程,温习一下 Redux 的诸多概念。为了增加一些趣味,也因为我热爱 TypeScript,所以这次我决定用 TypeScript 编写这个教程应用。

这篇文章汇集了我的一些想法和经历的亮点。

一些示例代码

你可以在我的 GitHub上通过 git 标签查​​看课程代码以及我的每一步教程。我还创建了一个CodeSandbox,其中包含使用 Typescript 的最小设置react-redux和连接组件。

你可以随意浏览它们,或者将它们作为你自己代码的灵感。我主要使用 Github 上的代码库来说明一些要点。

定义全局状态和根归约器

在我的仓库中,有两个 reducer 被合并combineReducers,它们的状态定义如下:

  • movies
export interface IReduxMoviesState {
  movies: IMovie[];
  moviesLoaded: boolean;
  moviesLoadedAt?: number;
  movie?: IMovie;
  movieLoaded: boolean;
}
Enter fullscreen mode Exit fullscreen mode
  • toggle
export interface IReduxMessageState {
  messageVisibility: boolean;
}
Enter fullscreen mode Exit fullscreen mode

通过我们的 reducer 返回每个状态,我们可以定义全局应用程序状态,如下所示:

const rootReducer = combineReducers({
  toggle,
  movies
});

export type AppState = ReturnType<typeof rootReducer>;
Enter fullscreen mode Exit fullscreen mode

这使得它AppState看起来像:

type AppState = {
  toggle: IReduxMessageState;
  movies: IReduxMoviesState;
};
Enter fullscreen mode Exit fullscreen mode

这很棒,因为无论在哪里使用我们的 redux 状态,我们都确切地知道它是什么样子,以及在连接组件时我们可以从中引用什么。

定义动作创建者和动作类型常量

在 Redux 中,将操作类型定义为常量是一种常见的做法。因为我们使用的是 TypeScript,所以我们可以使用枚举和扩展接口来使代码更具描述性。在我的代码库中,我有以下操作类型的枚举:

export enum EReduxActionTypes {
  GET_MOVIE = 'GET_MOVIE',
  GET_MOVIES = 'GET_MOVIES',
  RESET_MOVIE = 'RESET_MOVIE',
  TOGGLE_MESSAGE = 'TOGGLE_MESSAGE'
}
Enter fullscreen mode Exit fullscreen mode

如果你熟悉 Typescript,你会发现我给枚举定义了值。这是为了避免枚举键被赋值为数值,因为这可能会降低代码的弹性。无论如何,这都会让我们定义 action creators 更容易一些。

我根据具有更通用值的接口定义了操作type,它非常简单,但具有很强的可扩展性:

export interface IReduxBaseAction {
  type: EReduxActionTypes;
}
Enter fullscreen mode Exit fullscreen mode

例如,在电影 Reducer 的情况下,可以分派几个不同的动作:

export interface IReduxGetMoviesAction extends IReduxBaseAction {
  type: EReduxActionTypes.GET_MOVIES;
  data: IMovie[];
}
export interface IReduxGetMovieAction extends IReduxBaseAction {
  type: EReduxActionTypes.GET_MOVIE;
  data: IMovie;
}

export interface IReduxResetMovieAction extends IReduxBaseAction {
  type: EReduxActionTypes.RESET_MOVIE;
}
Enter fullscreen mode Exit fullscreen mode

与 Typescript 中的许多事物一样,您不需要知道数据的值是如何定义的,在这种情况下,您只需知道每个操作都将包含data我们操作属性的正确类型的对象或数组。

通过将这些类型聚合为联合类型,我可以像下面这样编写电影减速器:

type TMoviesReducerActions = IReduxGetMoviesAction | IReduxGetMovieAction | IReduxResetMovieAction;

export default function(state: IReduxMoviesState = initialState, action: TMoviesReducerActions) {
  switch (action.type) {
    case EReduxActionTypes.GET_MOVIES:
      return { ...state, movies: action.data, moviesLoaded: true, moviesLoadedAt: Date.now() };
    case EReduxActionTypes.GET_MOVIE:
      return { ...state, movie: action.data, movieLoaded: true };
    case EReduxActionTypes.RESET_MOVIE:
      return { ...state, movie: undefined, movieLoaded: false };
    default:
      return state;
  }
}
Enter fullscreen mode Exit fullscreen mode

这个 Reducer 是这个 TS 和 Redux 实现中我最喜欢的部分之一。

EReduxActionTypes因为我对每个动作使用了不同的值。当我进入action.data不同的case时,Typescript 已经知道数据是正确的类型,即Imovie(电影数组)IReduxGetMovieActionIMovie[]IReduxGetMoviesAction

这是一件非常强大的事情。

在我的教程应用程序中,减速器相当简单,但我们已经可以看到,扩展它并不是什么大问题,也不会真正增加我们商店的复杂性。

如果我们考虑到 VS Code 为 Typescript 提供的出色的开发人员体验,这一点尤其正确。

连接组件并使用我们的商店状态

为了将我们的movies状态与MoviesList组件连接起来,使用的代码如下:

const mapStateToProps = (state: AppState) => ({
  movies: state.movies.movies,
  isLoaded: state.movies.moviesLoaded,
  moviesLoadedAt: state.movies.moviesLoadedAt
});

const mapDispatchToProps = (dispatch: Dispatch<AnyAction>) =>
  bindActionCreators(
    {
      getMovies
    },
    dispatch
  );

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(MoviesList);
Enter fullscreen mode Exit fullscreen mode

这里有相当多的代码和值的重新赋值。通常这可能会导致一些混淆,关于哪些 props 可用于我们的MoviesList组件,但 Typescript 会确保不会发生这种情况,它允许我们解析mapStateToProps和的类型定义mapDispatchToProps并在创建组件时使用它:

class MoviesList extends PureComponent<ReturnType<typeof mapStateToProps> & ReturnType<typeof mapDispatchToProps>, {}> {
  // Code for the component goes here
}
Enter fullscreen mode Exit fullscreen mode

MoviesList我们甚至可以通过创建如下 props 类型来稍微简化事情:

type TMoviesListProps = ReturnType<typeof mapStateToProps> & ReturnType<typeof mapDispatchToProps>;

class MoviesList extends PureComponent<TMoviesListProps, {}> {
  // Code for the component goes here
}
Enter fullscreen mode Exit fullscreen mode

现在,如果我们尝试从组件内部引用任何内容,我们将能够完全看到this.props提供给我们的所有属性mapStateToPropsmapDispatchToProps

结论

尽管使用 Redux 管理状态并遵循其标准实践可能会导致我们将逻辑分散到多个文件中,并且/或者添加大量的样板代码。通过使用 Typescript,我们可以大大提高代码的可读性,并且对于那些不太了解复杂应用程序的来龙去脉、每个部分负责什么以及他们期望从其他组件接收什么的人来说,它可能会更容易理解。

这个教程应用可能不是最复杂的,我可能也没有最细致地使用 Typescript。但我仍然认为它凸显了 Typescript 的一些强大功能,以及为什么最近越来越多的人开始研究它。

您如何看待 Typescript?它如何改变我们开发者在创建和扩展应用程序时的体验?欢迎在下方评论或通过社交媒体联系我,详情请访问我的网站:leomeloxp.dev

最后一点。在编写此应用时,我尽量使代码与 LUT 的“人人皆可使用的 React 和 Redux”课程中的原始代码保持一致。如果您想了解更多关于该课程或 Level Up 教程的信息,欢迎访问他们的网站

这篇文章不是由 Level Up Tutorials 赞助的,我只是真的很喜欢他们的内容。

文章来源:https://dev.to/leomeloxp/take-react-and-redux-to-the-next-level-with-typescript-1m84
PREV
理解时间复杂度 - 大 O 符号
NEXT
人类可读的 JavaScript