快速学会使用react redux

517 阅读7分钟

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战

如果已经简单了解过redux的概念,可以直接跳到redux的快速使用。

redux的核心概念

掌握了redux的核心概念,才能更加快速的领悟redux的写法。

还记得react的哲学吗? 将设计好的 UI 划分为组件层级、创建静态版本、确定UI state 的最小(且完整)表示、添加反向数据流。

react.docschina.org/docs/thinki…

redux就是将除了静态版本外的所有state(状态)进行管理。所以,在使用redux的过程中,这些与state(状态)有关的内容将全部在redux中处理。

redux三大原则及其在代码中的体现

单一数据源

整个应用的 state 被储存在一棵 三大原则object tree 中,并且这个 object tree 只存在于唯一一个 store 中。

所以项目中应该有一个单独的store文件夹,放置所有的state及其相关内容。如果将整个应用的所有state都放在一起,难免使得可读性变差,很难区分某个store究竟是为哪个页面部分服务的。 为了解决上述问题,我们可以将需要管理状态的页面或者组件的文件夹中加入store文件夹,将action、reducer等写在组件或页面中的store中,最后在整个项目的store中将他们集中起来。

State 是只读的

唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。 确保了视图和网络请求都不能直接修改 state,所有的修改都被集中化处理,且严格按照一个接一个的顺序执行,因此不用担心 race condition 的出现, Action 就是普通对象而已,因此它们可以被日志打印、序列化、储存、后期调试或测试时回放出来。

使用纯函数来执行修改

为了描述 action 如何改变 state tree ,需要编写 reducers Reducer 只是一些纯函数,它接收先前的 state 和 action,并返回新的 state。刚开始你可以只有一个 reducer,随着应用变大,你可以把它拆成多个小的 reducers,分别独立地操作 state tree 的不同部分,因为 reducer 只是函数,你可以控制它们被调用的顺序,传入附加数据,甚至编写可复用的 reducer 来处理一些通用任务,如分页器。

action、reducer 、store的简单介绍

Action

Action 是把数据从应用传到 store 的有效载荷,是 store 数据的唯一来源。一般来说你会通过 store.dispatch() 将 action 传到 store。

Action 创建函数 就是生成 action 的方法。

Reducer

Reducers 指定了应用状态的变化如何响应 actions 并发送到 store 的,记住 actions 只是描述了有事情发生了这一事实,并没有描述应用如何更新 state。

Action 处理

reducer 就是一个纯函数,接收旧的 state 和 action,返回新的 state 永远不要在 reducer 里做这些操作:

  1. 修改传入参数
  2. 执行有副作用的操作,如 API 请求和路由跳转
  3. 调用非纯函数,如 Date.now() 或 Math.random() 只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。

Store

store 是把 action 和使用 reducers 来根据 action 更新 state 的用法 联系到一起的对象

store 的作用

  1. 维持应用的 state
  2. 提供 getState() 方法获取 state
  3. 提供 dispatch(action) 方法更新 state
  4. 通过 subscribe(listener) 注册监听器
  5. 通过 subscribe(listener) 返回的函数注销监听器

redux的快速使用

这里以在react当中的使用为例。

搭配 React Router

一般都会使用react router ,以便于路由处理器可以访问 store。尤其是在需要时光旅行和回放 action 来触发 URL 改变的需求下。

从 React Redux 导入 Provider : import { Provider } from 'react-redux'; <Provider />是由 React Redux 提供的高阶组件,用来让你将 Redux 绑定到 React,将用 <Provider /> 包裹 <Router />

hash 路由

const Root = ({ store }) => (
  <Provider store={store}>
    <Router>
      <Route path="/" component={App} />
    </Router>
  </Provider>
);

不使用 hash 路由

import React, { PropTypes } from 'react';
import { Provider } from 'react-redux';
import { Router, Route, browserHistory } from 'react-router';
import App from './App';

const Root = ({ store }) => (
  <Provider store={store}>
    <Router history={browserHistory}>
      <Route path="/(:filter)" component={App} />
    </Router>
  </Provider>
);

Root.propTypes = {
  store: PropTypes.object.isRequired,
};

export default Root;

使用 typesafe-actions

www.npmjs.com/package/typ…

使用typesafe-actions 将action生成和reducer使用更加简便。

使用typesafe-actions,我们一般会将store文件夹中加入四个文件。

截屏2021-08-15 21.21.54.png

分别是xxxAction.ts 、xxxEpic.ts、xxxReducer.ts 、xxxxSelector.ts

xxxAction.ts

在这个文件中,定义所有的动作和动作的输入参数。

import { createAction } from 'typesafe-actions';
import { LessonInfo } from '../LessonDetailType';

export const GetLessonDetailAction = createAction('[LessonDetail] start get list', (id: number) => ({ id }))();
export const GetLessonDetailSuccessAction = createAction(
  '[LessonDetail] get list success',
  (lessonInfo: LessonInfo) => ({
    lessonInfo,
  }),
)();
export const GetLessonDetailFailAction = createAction('[LessonDetail] get list failed', (errorText: string) => ({
  errorText,
}))();
export const GetLessonDetailResetAction = createAction('[LessonDetail] rest list')();

其中我们会用到 createAction,这是一个高效的无限参数的 action 生成器 参数: (id, title, amount, etc...) 返回: ({ type, payload, meta }) action 对象。 在这里,第一个参数用来描述这个动作,第二个参数描述了该动作的输入参数。

我们在组件或者页面的代码中,使用useDispatch去触发action

import { useDispatch, useSelector } from 'react-redux';
const dispatch = useDispatch();


useEffect(() => {
dispatch(GetLessonDetailAction(id));
return () => {
  dispatch(GetLessonDetailResetAction());
};
}, [dispatch, id]);

xxxEpic.ts

当一个 action的动作结果有两种状态或多种状态时,我们就需要将action分开,并在epic中定义action的前后逻辑关系。

举例:当我们向服务端进行请求课程列表,就会有请求成功返回课程列表和请求失败什么数据都没有的两种结果。这时候,我们一般会定义三个action,即GetLessonDetailActionGetLessonDetailSuccessActionGetLessonDetailFailAction 。 当我们在代码中触发请求的动作时,即 dispatch(GetLessonDetailAction(id)) ,请求的成功或失败就会导致最后使用的state出现两种状态。所以我们应该根据GetLessonDetailAction(id)的结果去分别触发GetLessonDetailSuccessActionGetLessonDetailFailAction

import { from, of } from 'rxjs';
import { switchMap, catchError, filter, map } from 'rxjs/operators';
import { Epic } from 'redux-observable-es6-compat';
import { RootAction, RootState, isActionOf } from 'typesafe-actions';
import { GetLessonDetailAction, GetLessonDetailSuccessAction, GetLessonDetailFailAction } from './LessonDetailAction';
import { container } from 'tsyringe';
import { LessonDetailService } from '../LessonDetailService';
import { LessonInfo } from '../LessonDetailType';

const lessonDetailService = container.resolve(LessonDetailService);

const GetLessonDetailEpic: Epic<RootAction, RootAction, RootState> = action$ =>
  action$.pipe(
    filter(isActionOf(GetLessonDetailAction)),
    switchMap(action => {
      return from(lessonDetailService.getLessonDetail(action.payload.id)).pipe(
        map((lessonInfo: LessonInfo) => {
          return GetLessonDetailSuccessAction(lessonInfo);
        }),
        catchError(err => {
          return of(GetLessonDetailFailAction('something wrong'));
        }),
      );
    }),
  );
export { GetLessonDetailEpic };

当有dispatch动作时,就会进入epic对触发action进行识别和处理。 我们只需要在epic中写好识别action及其action的处理方式。

xxxReducer.ts

reducer文件是对state改变前后和数据结构的管理和定义。 在reducer文件中,我们需要先定义好defaultState的默认状态,然后对于有对state进行改变的action,handleType的第二个参数(state, action) => {return {...state,}},state是当前state的数值,action是对于action完成后的action对象,其中action.payload中包含着我们在xxxaction.ts 文件中的设置的payload,函数的返回是更新后的state。

import { createReducer, getType, RootAction } from 'typesafe-actions';
import {
  GetLessonDetailAction,
  GetLessonDetailSuccessAction,
  GetLessonDetailFailAction,
  GetLessonDetailResetAction,
} from './LessonDetailAction';
import { LessonDetail } from '../LessonDetailType';

const defaultState: LessonDetail = {
  id: 0,
  lessonName: '',
  duration: '',
  teacher: null,
  episodes: [],
  paid: false,
  product: null,
};

const changeFigureToChinese = (day: number): string => {
  switch (day) {
    case 0:
      return '一';
    case 1:
      return '二';
    case 2:
      return '三';
    case 3:
      return '四';
    case 4:
      return '五';
    case 5:
      return '六';
    case 6:
      return '日';
    default:
      return 'x';
  }
};

export default createReducer<LessonDetail, RootAction>(defaultState)
  .handleType(getType(GetLessonDetailAction), (state, action) => {
    return {
      ...state,
      id: action.payload.id,
    };
  })
  .handleType(getType(GetLessonDetailSuccessAction), (state, action) => {
    const lessonInfo = action.payload.lessonInfo;
    if (lessonInfo) {
      const startDate = new Date(lessonInfo.lesson.startTime);
      const endDate = new Date(lessonInfo.lesson.endTime);
      const episodes = lessonInfo.lessonAgendas.map(({ id, name, startTime, endTime }) => {
        const episodeStartTime = new Date(startTime);
        const episodeEndTime = new Date(endTime);
        return {
          id,
          name,
          duration: `${episodeStartTime.getMonth() + 1}${episodeStartTime.getDate()}日 周${changeFigureToChinese(
            episodeStartTime.getDay(),
          )} ${episodeStartTime.getHours() + 1}:${episodeStartTime.getMinutes()}-${
            episodeEndTime.getHours() + 1
          }:${episodeEndTime.getMinutes()}  `,
        };
      });

      return {
        ...state,
        id: lessonInfo.lesson.id,
        lessonName: lessonInfo.lesson.name,
        duration: `${startDate.getMonth() + 1}${startDate.getDate()}日 - ${
          endDate.getMonth() + 1
        }${endDate.getDate()}日  `,
        teacher: lessonInfo.lesson.teacher[0],
        episodes,
        paid: lessonInfo.paid,
        product: lessonInfo.product,
      };
    } else {
      return {
        ...state,
      };
    }
  })
  .handleType(getType(GetLessonDetailFailAction), (state, action) => {
    return { ...state };
  })
  .handleType(getType(GetLessonDetailResetAction), (state, action) => defaultState);

xxxxSelector.ts

selector的作用是方便我们直接使用state中的内容。

我们需要先在全局的store中,将上述reducer引入

import { combineReducers } from 'redux';
import login from '@/containers/account-content/store/LoginReducer';
import courseList from '@/containers/home-course-list/store/CourseListReducer';
import lessonDetail from '@/containers/lesson-detail/store/LessonDetailReducer';

const rootReducer = combineReducers({
  login,
  courseList,
  lessonDetail,
});

export default rootReducer;

然后就可以在 xxxxSelector.ts中使用

import { createSelector } from 'reselect';
import { RootState } from 'typesafe-actions';
import { LessonDetail } from '../LessonDetailType';

export const selectLessonDetailState = createSelector(
  (state: RootState) => state.lessonDetail,
  (lessonDetail: LessonDetail) => lessonDetail,
);

然后我们就可以在代码中直接取出我们想要的值了。

import { useDispatch, useSelector } from 'react-redux';
import { selectLessonDetailState } from './store/LessonDetailSelector';

  const { lessonName, duration, teacher, episodes, paid, product } = useSelector(selectLessonDetailState);