快速学会使用 react redux

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

https://www.npmjs.com/package/typesafe-actions

使用 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);


  • React

    React 是 Facebook 开源的一个用于构建 UI 的 JavaScript 库。

    177 引用 • 270 回帖 • 539 关注
  • Redux
    3 引用 • 22 回帖

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...