React 02 :: Redux状态管理

405 阅读7分钟

0. 什么是Redux?

一套状态管理工具。

Redux is a predictable state container for JavaScript apps.

Redux是JavaScript应用程序的可预测状态容器。

1. 你不一定需要Redux

阮一峰老师在他的Redux系列教程里面对于你是否需要Redux有过非常精辟的论述。

如果你不知道是否需要 Redux,那就是不需要它。

只有遇到 React 实在解决不了的问题,你才需要 Redux 。

这个是阮一峰老师的系列教程,一共有两篇,感兴趣的小伙伴可以去看一下,讲的非常好,不过是基于ReactJs讲的,我们这篇文章会基于React+Typescript来讲解他的用法。

Redux 入门教程(一):基本用法

Redux 入门教程(二):中间件与异步操作

2. Store, Action, Reducer

2.1 Store

就是保存数据的地方,你可以把它看成一个容器。整个应用只能有一个 Store。

每一个Component都将自己的状态数据存放到Store中,当Store中的状态发生改变,会下发给每一个Connect到Store的Component,调用Component的render方法。

按照Redux的官方文档,Store中的State是只读的,如果要修改State的状态,只能通过Reducer生成新的State实例。

2.2 Action

每一个Component只能通过发送Action到Store来修改Store中的State。Action是一个数据结构,可以参考这个规范来定义Action的结构。

对于Action的定义,一般会按照如下方式定义:

export interface Action {
    // Action的Type一般是一个字符串常量,Reducer会使用这个Type来修改状态
    type: string; 
    // Payload的类型与Component的State的类型对应
    payload: number;
}
  • Action Creator 一般会创建一个纯函数来创建一个View相对应的Action。
export function onLike(count: number): LikeAction {
    return {
        type: FETCH_LIKE_COUNT,
        payload: count
    };
}
  • 发送Action 一般会通过调用store.dispatch(action),将action发送给Store,store将action分发给对应的reducer来处理。不过在本篇的Demo中,我们使用了typescript推荐的connect方式,所以dispatch的方式略有不同,后续会通过Demo详细介绍。
const mapDispatchToProps = (dispatch: Dispatch<AnyAction>) => ({
    onLike: (count: number) => dispatch(onLike(count))
});

export const LikeWrapper = connect(mapStateToProps, mapDispatchToProps)(Like);

2.3 Reducer

在上一节中,提到了Reducer,这一节来详细介绍Reducer。

Store 收到 Action 以后,必须给出一个新的 State,这样 View 才会发生变化。这种 State 的计算过程就叫做 Reducer。

Reducer 是一个函数,它接受 Action 和当前 State 作为参数,返回一个新的 State。

Reducer会接受两个参数,一个是state,另一个是action。下面的例子展示了如何编写一个Reducer函数。

export function likeReducer(state = initState, action: any): LikeState {
    let result = initState;
    // 前面提到,Reducer会根据action.type返回各种新的state。
    // 按照Redux的官方文档,state是只读的,不能修改,只能生成新的state。
    switch (action.type) {
        case FETCH_LIKE_COUNT:
            result = {
                ...state,
                count: action.payload
            };
            break;
        default:
            result = state;
            break;
    }
    return result;
}

3. Redux工作流程

下面这个图展示了从Component生成一个Action开始,如何更新State,之后如何根据new-state更新Component的UI。

sequenceDiagram
Component->>Component: create action1
Component->>Store: dispatch(action1)
Store->>Reducers: send action1 to <br/> specific reducer
Reducers->>Reducers: Generate new State <br/> by action1.type
Reducers->>Store: update(new-state)
Store->>Component: new-state
Component->>Component: merge props with new-state, call render()

4. 异步

根据上图,其实有一个关键问题没有解决:异步操作怎么办?Action 发出以后,Reducer 立即算出 State,这叫做同步;Action 发出以后,过一段时间再执行 Reducer,这就是异步。

为了解决异步的问题,Redux引入了中间件的概念。

4.1 中间件

其实各种平台都有中间件的概念,核心思想就是把所有要做的事情编成队列,一个一个执行。这里也是一样的,其实就是把Action分解成不同的阶段,放到队里额里面,然后由第三方的中间件,进行dispatch。

在阮一峰老师的教程里面,介绍了两种第三方中间件,一个是thunk一个是promise,个人感觉promise比较好用,而且网络少大部分教程都是介绍thunk中间件,很少有介绍promise中间件的,所以,我们这篇文档就基于promise中间件来实现我们的demo。

下一篇文章我们将介绍如何使用thunk中间件进行异步操作。

5. 实现

本篇文章的示例代码repo:gitlab.com/yafeya/reac…

下面我们分步骤讲解一下。

5.1 安装

npm i redux react-redux redux-promise-middleware

5.2 为每一个component定义State, Action, Reducer

这个demo中,对于每一个component,我都新建了一个叫做[Component]Redux.ts的文件,存放这个component所有的actionsstate,以及reducer

其实,同步或者异步的差别主要在于ActionCreator

Action Creator
同步直接返回Action
Promise Middleware直接返回Action,Action的Payload类型必须是Promise< RealType >。
Thunk Middleware返回ThunkAction,ThunkAction泛型,有四个泛型参数, 分别为:
1. 最后Dispatch的Action的Promise类型
2. 最后Dispatch的Action的Payload类型
3. Action Creator接收的参数类型
4. 最后Dispatch的Action类型。
我们将在下一篇文档中详细描述。
  • Like Component(同步)

    // LikeRedux.ts
    export interface LikeState {
       count: number;
    }
       
    export const FETCH_LIKE_COUNT = 'FETCH_LIKE_COUNT';
    
    export interface LikeAction {
       type: string;
       payload: number;
    }
    
    export function onLike(count: number): LikeAction {
        return {
            type: FETCH_LIKE_COUNT,
            payload: count
        };
    }
    
    const initState: LikeState = {
        count: 0
    };
    
    export function likeReducer(state = initState, action: any): LikeState {
        let result = initState;
        switch (action.type) {
            case FETCH_LIKE_COUNT:
                result = {
                    ...state,
                    count: action.payload
                };
                break;
            default:
                result = state;
                break;
        }
        return result;
    }
    
  • Clock Component(Promise Middleware)

    Promise Middleware 有一个特别之处,在于在ActionCreator中你需要创建Action.Type, 然后Promise Middleware会为你创建3个additional action,然后这3个Action由promise middlewaredispatch给reducer。作为开发者只需要在reducer中处理相应的action,创建不同的state即可。

    Additional Action类型分别为:
    • *_PENDING // 表示正在获取state
    • *_FULFILLED // 表示已经获取到state
    • *_REJECTED // 表示获取state失败
    export interface ClockState {
       time: Date;
       isFetching: boolean;
       succeed: boolean;
    }
    
    export const FETCH_TIME = 'FETCH_TIME';
    export const FETCH_TIME_PENDING = 'FETCH_TIME_PENDING';
    export const FETCH_TIME_FULFILLED = 'FETCH_TIME_FULFILLED';
    export const FETCH_TIME_REJECTED = 'FETCH_TIME_REJECTED';
    
    export interface FetchTimeAction {
       type: string;
       payload: Promise<Date>;
    }
    
    // Mock Api
    function getTimeAsync(): Promise<Date> {
       return new Promise<Date>((resolve, reject) => {
           resolve(new Date());
       });
    }
    
    export function fetchTime(): FetchTimeAction {
       return {
           type: FETCH_TIME,
           payload: getTimeAsync()
       };
    }
    
    const initState: ClockState = {
       time: new Date(),
       isFetching: false,
       succeed: true
    };
    
    export function clockReducer(state = initState, action: any): ClockState {
       let result = initState;
       switch (action.type) {
           case FETCH_TIME_PENDING:
               result = {
                   ...state,
                   isFetching: true
               };
               break;
           case FETCH_TIME_FULFILLED:
               result = {
                   ...state,
                   isFetching: false,
                   succeed: true,
                   time: action.payload
               };
               break;
           case FETCH_TIME_REJECTED:
               result = {
                   ...state,
                   isFetching: false,
                   succeed: false
               };
               break;
           default:
               result = state;
               break;
       }
       return result;
    }
    

5.3 在Compnent中Connect

Redux引入了一个语法糖为connect方法,下面我们对它最有用的前两个参数和返回值进行说明。

// Clock.tsx
interface ClockProps {
    time: Date;
    fetchTime(): void;
}

export const Clock: React.FC<ClockProps> = (props) => {
    const clockRef = useRef<HTMLParagraphElement>(null);

    useEffect(() => {
        setInterval(() => props.fetchTime(), 1000);
    }, [clockRef]);

    return <p ref={clockRef}>The current time is {props.time.toLocaleTimeString()}</p>;
};

const mapStateToProps = (state: State) => {
    const { time } = state.clock;
    return { time };
}

const mapDispatchToProps = (dispatch: Dispatch<AnyAction>) => ({
    fetchTime: () => dispatch(fetchTime())
});

export const ClockWrapper = connect(mapStateToProps, mapDispatchToProps)(Clock);
  • mapStateToProps

    将reducer返回的new-state merge到component的props中,component如果需要展示数据,都是用props里面的数据进行render。

  • mapDispatchToProps

    dispatch方法merge到props的方法中,之前我们提到过,redux的思想就是,整个redux就是一个状态机,状态的改变只能通过dispatch(action),而发起这个动作的只能是UX,也就是component。所以Component需要调用props中merge的方法dispatch action

  • 返回值

    返回一个新的Component, 提供给App.tsx引用。

    // Asp.tsx
    export class App extends Component {
    
        render() {
          return (
            <div className="App">
              <header className="App-header">
                <Image src={logo} alt="logo"></Image>
                <Paragraph part1="Edit" code="src/App.tsx" part2="and save to reload."></Paragraph>
                <HyperLink href="https://reactjs.org" title="Learn React"></HyperLink>
                <LikeWrapper></LikeWrapper>
                // ClockWrapper 就是我们上面的返回值
                <ClockWrapper></ClockWrapper>
              </header>
            </div>
          );
        }
    }
    

5.4 Index.tsx中启动Redux

const appReducer = combineReducers({
   clock: clockReducer,
   like: likeReducer
});
const middleware = [promise];
const store = createStore(appReducer, applyMiddleware(...middleware));

ReactDOM.render(
    <Provider store={store}>
        <React.StrictMode>
          <App />
        </React.StrictMode>
    </Provider>,
    document.getElementById('root')
);
  • 添加middleware
    const middleware = [promise];
    
  • Combine Reducer
    const appReducer = combineReducers({
        clock: clockReducer,
        like: likeReducer
    });
    
  • Create Store
    const store = createStore(appReducer, applyMiddleware(...middleware));
    
  • 添加Provider
    <Provider store={store}>
       <React.StrictMode>
           <App />
       </React.StrictMode>
    </Provider>
    

5.5 在Component中使用state的数据

我们肯定希望在自己的Component使用相对应的Reducer创建的state。举例来说,上面例子中,我们希望在ClockComponent中使用ClockReducer创建的state

之前在Clock Component我们也看到了,Clock中使用了state.clock访问他自己的state,这是因为,我们在combineReducers时,将clockReducer生成的state放到了clock下面,将likeReducer生成的state放到了like下面。

因此,在component中使用时,可以通过state.clock访问clockReducer生成的state,通过state.like访问likeReducer生成的state

6 什么时候使用Redux

讲了这么半天,估计大家应该也看明白了,既然Redux增加了项目的复杂度,为什么还要使用Redux呢?因为,普通的React应用程序中,很难在Component之间share状态,Redux提供给我们一个方法,可以在各个Component之间share状态,并且,可以进行状态的修改。

希望这篇帖子对大家有帮助,下一篇文章,我们将给大家share如何使用thunk中间件。