ch1oechao / code

Code notes, update posts in issues
https://github.com/zchen9/code/issues
42 stars 6 forks source link

React + Redux 入坑指南 #7

Open ch1oechao opened 7 years ago

ch1oechao commented 7 years ago

Redux

原理

1. 单一数据源

all states => Store

2. 单向数据流

dispatch(actionCreator) => Reducer => (state, action) => state

// actionType
export const ACTION_TYPE = 'ACTION_TYPE';

// actionCreator
let actionCreator = (config) => {
    return {
        type: ACTION_TYPE, // 必须定义 type
        config // 传递参数 => reducer
    }
}

2. Action 异步解决方法

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';

let store = createStore(
  reducers,
  applyMiddleware(thunk)
);
// 接收方法
let receiveSomething = (res) => {
    return {
        type: RECEIVE_SOME,
        res
    }
}

// 获取数据方法
export let fetchSomething = (args) => {
    return dispatch => {
        return fetch(args).then((res) => {
            return dispatch(receiveSomething(res))
        })
    }
}

Reducer

import { ACTION_A, ACTION_B } from '../actions';

let initialState = { ... }

function example(state = initialState, action) {
    switch(action.type) {
        case ACTION_A:
          return Object.assign({}, state, action.config)
        case ACTION_B:
          return Object.assign({}, state, action.config)
    }
}

2.对 Action 传递的数据多加一层处理

let doSomething = (config) => {
    let { a, b } = config;
    // do something with a, b
    return { a, b }
}

function example(state = initialState, action) {
    switch(action.type) {
        case ACTION_TYPE:
          return Object.assign({}, 
          state, 
          doSomething(action.config))
    }
}

3.合并多个 Reducer

通过 redux 提供的 combineReducers 将不同处理逻辑的 reducer 合并起来。

import { combineReducers } from 'redux';

export default combineReducers({
  reducerA,
  reducerB
});

// or

export let reducer = (state = initialState, action) {
    a: processA(state.a, action),
    b: processB(state.b, action)
}

Store

1. 将 Store 绑定 React

使用 react-redux 提供的 Provider 可以将 Store 注入到 react 中。

Store 将合并后的 reducers 通过 createStore 创建,此外下面示例代码还使用中间件加入了一层 react-thunk 处理。

import ReactDOM from 'react-dom';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux'
import thunk from 'redux-thunk';
import reducers from './reducers';

let store = createStore(
  reducers,
  applyMiddleware(thunk)
);

ReactDOM.render((
  <Provider store={store}>
   // ...
  </Provider>
), document.querySelector('#app'));

2. 将 state 绑定到 Component

使用 react-redux 提供的 connect 方法 将组件和所需数据绑定。

需要注意的是,Store 创建时接收的是合并后的 reducers, 因此不同 reducer 上的处理数据绑定在了不同 reducer 对象上,而不是全部挂载在 Store 上。

mapStateToProps 将组件内部所需数据通过 props 传入组件内部。更多绑定机制,具体可参考connect

import React, { Component } from 'react';
import { connect } from 'react-redux';

class ComponentA extends Component {
   //...
}

let mapStateToProps = (state) => {
  // attention !!!
  let { reducerA, reducerB } = state;
  return { 
    propA: reducerA.propA,
    propB: reducerB.propB
  }
};

export default connect(mapStateToProps)(ComponentA);

Component

1. 概念

React bindings for Redux embrace the idea of separating presentational and container components.

Redux 的 React 绑定库包含了 容器组件和展示组件相分离 的开发思想。

  • Presentational Components 展示型组件
  • Container Components 容器型组件

展示型组件和容器型组件的区别在官方文档中已经给出很详细的解释了,但是中文文档的翻译有误,所以直接看英文比较更容易懂。

Presentational Components Container Components
Purpose How things look (markup, styles) How things work (data fetching, state updates)
Aware of Redux No Yes
To read data Read data from props Subscribe to Redux state
To change data Invoke callbacks from props Dispatch Redux actions
Are written By hand Usually generated by React Redux

组件类型区分的模糊点在于怎么界定组件的内部功能规划。如果判定一个组件为展示型组件,那么它所需数据和处理方法都应该从父级传入,保持组件内部“纯净”。

在实际开发中,一个组件的逻辑跟业务紧密相关。如果需要将数据和方法从外部传入,那么父级组件所做的事情会很多,多重的子组件也会把父级逻辑弄乱,这就不是 redux 的初衷了。

中文文档翻译的意思是:容器组件应该为路由层面的组件,但这样既不符合实际开发需要,也违背了 redux 思想。真正界定两种组件的因素是:

当组件 connect 后,dispatch 方法已经注入到 props 中,所以触发 Action 可以从 props 获取 dispatch 方法。

import React, { Component } from 'react';
// actionCreator
import { actionA, actionB } from 'actions/actionA'

class ComponentA extends Component {
    constructor(props) {
        super(props);
    }
    componentDidMount() {
        let { dispatch } = this.props;
        dispatch(actionA())
    }
}
export default connect()(ComponentA);

组件内部所需的渲染数据都已经绑定在了 props 上,直接获取即可。

需要注意的是,在事件监听中触发 Action,需要用一个匿名函数封装,否则 React 在渲染时就会执行事件绑定事件,而不是当事件发生再执行。

render() {
  let { dispatch, propA, propB } = this.props;

    return (
      <section>
        // Attention !!!
        <input type="text" onClick={(ev) => dispatch(actionB(ev))} />
        <p className={propA}>{propB}</p>
      </section>
    )
}

容器型组件需要连接 Redux,使用 dispatch 触发 actionCreator。 展示型组件需要用到的方法调用在容器型组件内定义好,通过 props 传入到展示型组件中。

// get actionCreator
import { actionA } from './actions/actionA';

class Parent extends Component {
  handleCallback(data) {
    // use dispatch
    let { dispatch } = this.props;
    dispatch(actionA(data));
  }
  render() {
    return (
      <Child onSomethingChange={this.handleCallback} />
    )
  }
}
// connet Redux
export default connect()(Parent);

展示型组件不需要用到 Redux 的一切,它的 props 仅仅存在于父级传入的数据和方法。

// don't need action/dispatch/connect
class Child extends Component {
  handleSomething(data) {
    // handle anything with props
    this.props.onSomethingChange(data);
  }
  render() {
    return (
     // just markup & style
      <input onChange={handleSomething} />
    )
  }
}

Conclusion

图示箭头代表各概念之间的相互关系,不代表数据流。( 能理解下面这张图,这篇文章就没白看了 -。- )

参考文档

END.

Wanlima commented 7 years ago

是不是每个组件都需要导出一个 connect 函数用来将组件和store进行绑定?

ch1oechao commented 7 years ago

你可以看组件那一节 展示型组件不需要绑定 redux 展示数据通过父级组件props传入就可以了

webxzy commented 7 years ago

不错,逻辑很清晰。

sujiap commented 7 years ago

刚学的时候完全看不懂,学会了再看这个,嗯,很清晰

mamba-1024 commented 7 years ago

写的很不错,让我的思路更加清晰

wangbolin225522 commented 6 years ago

思路清晰了

catt-wuyang commented 6 years ago

写 redux 有一段时间了,并没有深刻理解其中 dispatch 手动自动绑定的含义,看了很多文章,很感谢在你这里得到了一个理解,👍