https://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_three_react-redux.html

UI组件

UI 组件有以下几个特征。

  • 只负责 UI 的呈现,不带有任何业务逻辑
  • 没有状态(即不使用this.state这个变量)
  • 所有数据都由参数(this.props)提供
  • 不使用任何 Redux 的 API
1
2
const Title =
value => <h1>{value}</h1>;

因为不含有状态,UI 组件又称为”纯组件”,即它纯函数一样,纯粹由参数决定它的值。

容器组件

容器组件的特征

  • 负责管理数据和业务逻辑,不负责 UI 的呈现
  • 带有内部状态
  • 使用 Redux 的 API

总之,只要记住一句话就可以了:UI 组件负责 UI 的呈现,容器组件负责管理数据和逻辑。

如果一个组件既有 UI 又有业务逻辑,那怎么办?

  • 将它拆分成下面的结构:外面是一个容器组件,里面包了一个UI 组件。
  • 前者负责与外部的通信,将数据传给后者,由后者渲染出视图。

React-Redux 规定,所有的 UI 组件都由用户提供,容器组件则是由 React-Redux 自动生成。也就是说,用户负责视觉层,状态管理则是全部交给它。

connect()

  • React-Redux 提供connect方法,用于从 UI 组件生成容器组件。
  • connect的意思,就是将这两种组件连起来。
1
2
import { connect } from 'react-redux'
const VisibleTodoList = connect()(TodoList);

上面代码中,

  • TodoList是 UI 组件,
  • VisibleTodoList就是由 React-Redux 通过connect方法自动生成的容器组件。

但是,因为没有定义业务逻辑,上面这个容器组件毫无意义,只是 UI 组件的一个单纯的包装层。为了定义业务逻辑,需要给出下面两方面的信息。

  • 输入逻辑:外部的数据(即state对象)如何转换为 UI 组件的参数
  • 输出逻辑:用户发出的动作如何变为 Action 对象,从 UI 组件传出去。

因此,connect方法的完整 API 如下。

1
2
3
4
5
6
import { connect } from 'react-redux'

const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
  • 上面代码中,connect方法接受两个参数:mapStateToPropsmapDispatchToProps
  • 它们定义了 UI 组件的业务逻辑。
    • 前者负责输入逻辑,即将state映射到 UI 组件的参数(props),
    • 后者负责输出逻辑,即将用户对 UI 组件的操作映射成 Action。

mapStateToProps()

  • mapStateToProps是一个函数。
  • 它的作用就是像它的名字那样,建立一个从(外部的)state对象到(UI 组件的)props对象的映射关系。(建立一个从redux的state对象到UI组件的Props对象的映射关系)
  • 作为函数,mapStateToProps执行后应该返回一个对象,里面的每一个键值对就是一个映射

说人话:mapStateToProps这个函数允许我们将 redux中的store 中的数据作为 props 绑定到UI组件上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}

const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
default:
throw new Error('Unknown filter: ' + filter)
}
}

上面代码中,mapStateToProps是一个函数,它接受state作为参数,返回一个对象。这个对象有一个todos属性,代表 UI 组件的同名参数后面的getVisibleTodos也是一个函数,可以从state算出 todos 的值。

  • mapStateToProps会订阅 Store,每当state更新的时候,就会自动执行,重新计算 UI 组件的参数,从而触发 UI 组件的重新渲染。
  • mapStateToProps的第一个参数总是state对象,还可以使用第二个参数,代表容器组件的props对象。
1
2
3
4
5
6
7
8
9
10
// 容器组件的代码
// <FilterLink filter="SHOW_ALL">
// All
// </FilterLink>

const mapStateToProps = (state, ownProps) => {
return {
active: ownProps.filter === state.visibilityFilter
}
}

使用ownProps作为参数后,如果容器组件的参数发生变化,也会引发 UI 组件重新渲染。

connect方法可以省略mapStateToProps参数,那样的话,UI 组件就不会订阅Store,就是说 Store 的更新不会引起 UI 组件的更新。

mapDispatchToProps()

mapDispatchToPropsconnect函数的第二个参数,用来建立 UI 组件的参数到store.dispatch方法的映射。也就是说,它定义了哪些用户的操作应该当作 Action,传给 Store。它可以是一个函数,也可以是一个对象。

说人话:mapDispatchToProps,它的功能是,将 action 作为 props 绑定到 MyComp 上。

我们可以方便得使用去调用

1
<div onCLick={()=>this.props.del_todo() }>test</div>

如果mapDispatchToProps是一个函数,会得到dispatchownProps(容器组件的props对象)两个参数

1
2
3
4
5
6
7
8
9
10
11
12
13
const mapDispatchToProps = (
dispatch,
ownProps
) => {
return {
onClick: () => {
dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: ownProps.filter
});
}
};
}

从上面代码可以看到,mapDispatchToProps作为函数,应该返回一个对象,该对象的每个键值对都是一个映射,定义了 UI 组件的参数怎样发出 Action。

如果mapDispatchToProps是一个对象,它的每个键名也是对应 UI 组件的同名参数,键值应该是一个函数,会被当作 Action creator ,返回的 Action 会由 Redux 自动发出。举例来说,上面的mapDispatchToProps写成对象就是下面这样。

1
2
3
4
5
6
const mapDispatchToProps = {
onClick: (filter) => {
type: 'SET_VISIBILITY_FILTER',
filter: filter
};
}

总结

  1. UI 组件负责 UI 的呈现,容器组件负责管理数据逻辑

  2. 所以,UI组件就需要两种参数,

    1. 数据:用于层现内容
    2. 逻辑:这里是function,也就是onClick监听事件触发的函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    const Todo = ({ onClick, completed, text }) => (
    <li
    onClick={onClick}
    style={{
    textDecoration: completed ? 'line-through' : 'none'
    }}
    >
    {text}
    </li>
    )

    // todos用于数据层现,toggleTodo就是onClick监听事件触发的函数
    const TodoList = ({ todos, toggleTodo }) => (
    <ul>
    {todos.map(todo => (
    <Todo key={todo.id} {...todo} onClick={() => toggleTodo(todo.id)} />
    ))}
    </ul>
    )
  3. 如果一个组件既有 UI 又有业务逻辑,那怎么办?回答是,将它拆分成下面的结构:外面是一个容器组件,里面包了一个UI 组件。前者负责与外部的通信,将数据传给后者,由后者渲染出视图。

  4. 所以一般都是容器组件的里面包着一个UI组件。同时容器组件又是需要react-redux自动生成的。所以需要容器组件传递给UI组件这两种参数(数据和逻辑)

  5. 所以需要通过mapStateToProps传递数据,通过mapDispatchToProps传递逻辑。

    其具体功能是:

    1. 当state发生改变时(比如{filter:”show_all”}变成{filter:”show_complete”}),会自动执行mapStateToProps,该函数会让容器组件自动产生新的prop(也就是{filter:"show_complete"}),并将props传给UI组件,这样就触发了UI组件的重新渲染。
    2. 当容器组件监听到Onclick事件时,就会自动触发mapDispatchToProps函数,返回Onclick对应的处理函数,并且将这个处理函数传递给UI组件,让UI组件发送Action。
  6. 具体的方式就是返回一个映射

    1. TodoList组件需要数据型参数todos,所以返回属性todos的映射
    2. TodoList组件需要逻辑型参数toggleTodo,所以返回属性toggleTodo的映射
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    // todos用于数据层现,toggleTodo就是onClick监听事件触发的函数
    const TodoList = ({ todos, toggleTodo }) => (
    <ul>
    {todos.map(todo => (
    <Todo key={todo.id} {...todo} onClick={() => toggleTodo(todo.id)} />
    ))}
    </ul>
    )

    const getVisibleTodos = (todos, filter) => {
    switch (filter) {
    case 'SHOW_COMPLETED':
    return todos.filter(t => t.completed)
    case 'SHOW_ACTIVE':
    return todos.filter(t => !t.completed)
    case 'SHOW_ALL':
    default:
    return todos
    }
    }

    const mapStateToProps = state => ({
    todos: getVisibleTodos(state.todos, state.visibilityFilter)
    })

    const mapDispatchToProps = dispatch => ({
    toggleTodo: id => dispatch(toggleTodo(id))
    })

    export default connect(
    mapStateToProps,
    mapDispatchToProps
    )(TodoList)
  7. 场景化描述:

    1. 用户点击了UI组件
    2. UI组件监听了OnClick事件,但是自己没有相关的处理函数,便利用mapDispatchToProps去容器组件里找
    3. 在容器组件找到对应的处理函数后,调用他,该函数会发出一个action
    4. 该action被reduers捕捉,处理,返回新的state
    5. 返回新的state被容器组件接收,自动调用mapStateToProps返回新的prop,然后将新的prop传给UI组件
    6. UI组件得到新的Prop后就可以重新渲染

mapStateToProps(state, ownProps)

  1. mapStateToProps是一个函数,用于建立组件跟storestate的映射关系
  2. mapStateToProps这个函数允许我们将 store 中的数据作为 props 绑定到组件上。
  3. 作为一个函数,它可以传入两个参数,结果一定要返回一个object
  4. 传入mapStateToProps之后,会订阅store的状态改变,在每次storestate发生变化的时候,都会被调用
  5. ownProps代表组件本身的props,如果写了第二个参数ownProps,那么当prop发生变化的时候,mapStateToProps也会被调用。例如,当 props接收到来自父组件一个小小的改动,那么你所使用的 ownProps 参数,mapStateToProps 都会被重新计算)。
  6. mapStateToProps可以不传,如果不传,组件不会监听store的变化,也就是说Store的更新不会引起UI的更新

mapDispatchToProps

  1. mapDispatchToProps用于建立组件跟store.dispatch的映射关系
  2. mapDispatchToProps的功能是,将 action 作为 props 绑定到 MyComp 上
  3. 可以是一个object,也可以传入函数
  4. 如果mapDispatchToProps是一个函数,它可以传入dispatch,ownProps, 定义UI组件如何发出action,实际上就是要调用dispatch这个方法
  • mapStateToProps:建立一个redux的state对象到UI组件的Props对象的映射关系
  • mapDispatchToProps:建立UI组件的参数到store.dispatch方法的映射。也就是说,定义了那些用户的操作应该被当成action,传给store

<Provider> 组件

connect方法生成容器组件以后,需要让容器组件拿到state对象,才能生成 UI 组件的参数。

一种解决方法是将state对象作为参数,传入容器组件。但是,这样做比较麻烦,尤其是容器组件可能在很深的层级,一级级将state传下去就很麻烦。

React-Redux 提供Provider组件,可以让容器组件拿到state

1
2
3
4
5
6
7
8
9
10
11
12
13
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'

let store = createStore(todoApp);

render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)

上面代码中,Provider在根组件外面包了一层,这样一来,App的所有子组件就默认都可以拿到state了。

它的原理是React组件的context属性,请看源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Provider extends Component {
getChildContext() {
return {
store: this.props.store
};
}
render() {
return this.props.children;
}
}

Provider.childContextTypes = {
store: React.PropTypes.object
}

上面代码中,store放在了上下文对象context上面。然后,子组件就可以从context拿到store,代码大致如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class VisibleTodoList extends Component {
componentDidMount() {
const { store } = this.context;
this.unsubscribe = store.subscribe(() =>
this.forceUpdate()
);
}

render() {
const props = this.props;
const { store } = this.context;
const state = store.getState();
// ...
}
}

VisibleTodoList.contextTypes = {
store: React.PropTypes.object
}

实例:计数器

我们来看一个实例。下面是一个计数器组件,它是一个纯的 UI 组件。

1
2
3
4
5
6
7
8
9
10
11
class Counter extends Component {
render() {
const { value, onIncreaseClick } = this.props
return (
<div>
<span>{value}</span>
<button onClick={onIncreaseClick}>Increase</button>
</div>
)
}
}

上面代码中,这个 UI 组件有两个参数:valueonIncreaseClick。前者需要从state计算得到,后者需要向外发出 Action。

接着,定义valuestate的映射,以及onIncreaseClickdispatch的映射。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function mapStateToProps(state) {
return {
value: state.count
}
}

function mapDispatchToProps(dispatch) {
return {
onIncreaseClick: () => dispatch(increaseAction)
}
}

// Action Creator
const increaseAction = { type: 'increase' }

然后,使用connect方法生成容器组件。

1
2
3
4
const App = connect(
mapStateToProps,
mapDispatchToProps
)(Counter)

然后,定义这个组件的 Reducer。

1
2
3
4
5
6
7
8
9
10
// Reducer
function counter(state = { count: 0 }, action) {
const count = state.count
switch (action.type) {
case 'increase':
return { count: count + 1 }
default:
return state
}
}

最后,生成store对象,并使用Provider在根组件外面包一层。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { loadState, saveState } from './localStorage';

const persistedState = loadState();
const store = createStore(
todoApp,
persistedState
);

store.subscribe(throttle(() => {
saveState({
todos: store.getState().todos,
})
}, 1000))

ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);

bindActionCreators

  • 把 action creators 转成拥有同名 keys 的对象,但使用 dispatch 把每个 action creator 包围起来,这样可以直接调用它们。
1
2
3
4
5
6
7
8
const mapDispatchToProps = (dispatch) => {
return {
handleLoginSuccess: bindActionCreators(loginSuccessCreator, dispatch),
};
};

// 不需要从state中获取什么, 所以传一个null
export default connect(null, mapDispatchToProps)(Login);
1
2
3
4
5
6
7
const mapDispatchToProps = (dispatch) => {
return {
handleLoginSuccess: (name) => {
dispatch(loginSuccessCreator(name))
}
}
};

上面的代码有什么区别?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// TodoActionCreators.js
export function addTodo(text) {
return {
type: 'ADD_TODO',
text
}
}

export function removeTodo(id) {
return {
type: 'REMOVE_TODO',
id
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 针对TodoActionCreators不使用bindActionCreators的话,我们需要这样写:
const mapDispatchToProps = (dispatch) => {
return {
addTodo: (text) => {
dispatch(TodoActionCreators.addTodo(text))
},
removeTodo: (id) => {
dispatch(TodoActionCreators.removeTodo(id))
}
}
};

// 使用bindActionCreators
const mapDispatchToProps = (dispatch) => {
return bindActionCreators(TodoActionCreators, dispatch);
};

Action和reducer的连接

  • store.dispatch(action)是 View 发出 Action 的唯一方法。

  • 如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    store.dispatch(addTodo('Learn Redux'));
    // 把store作为第一参数(store)传给reducer
    // 把addTodo('Learn Redux')作为第二参数(action)传给recuder

    // reducer
    const reducer = function (state, action) {
    // ...
    return new_state;
    };

    下面是一个实际的例子。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const defaultState = 0;
    const reducer = (state = defaultState, action) => {
    switch (action.type) {
    case 'ADD':
    return state + action.payload;
    default:
    return state;
    }
    };

    const state = reducer(1, {
    type: 'ADD',
    payload: 2
    });

props和state

state 的主要作用是用于组件保存、控制、修改自己的可变状态。state 在组件内部初始化,可以被组件自身修改,而外部不能访问也不能修改。你可以认为 state 是一个局部的、只能被组件自身控制的数据源。state 中状态可以通过 this.setState 方法进行更新,setState 会导致组件的重新渲染。

props 的主要作用是让使用该组件的父组件可以传入参数来配置该组件。它是外部传进来的配置参数,组件内部无法控制也无法修改。除非外部组件主动传入新的 props,否则组件的 props 永远保持不变。

stateprops 有着千丝万缕的关系。它们都可以决定组件的行为和显示形态。一个组件的 state 中的数据可以通过 props 传给子组件,一个组件可以使用外部传入的 props 来初始化自己的 state。但是它们的职责其实非常明晰分明:**state 是让组件控制自己的状态,props 是让父组件对自己进行配置**。

如果你觉得还是搞不清 stateprops 的使用场景,那么请记住一个简单的规则:尽量少地用 state,尽量多地用 props

没有 state 的组件叫无状态组件(stateless component),设置了 state 的叫做有状态组件(stateful component)。因为状态会带来管理的复杂性,我们尽量多地写无状态组件,尽量少地写有状态的组件。这样会降低代码维护的难度,也会在一定程度上增强组件的可复用性。前端应用状态管理是一个复杂的问题,我们后续会继续讨论。

React.js 非常鼓励无状态组件,在 0.14 版本引入了函数式组件——一种定义不能使用 state 组件,例如一个原来这样写的组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class HelloWorld extends Component {
constructor() {
super()
}

sayHi () {
alert('Hello World')
}

render () {
return (
<div onClick={this.sayHi.bind(this)}>Hello World</div>
)
}
}

用函数式组件的编写方式就是:

1
2
3
4
5
6
const HelloWorld = (props) => {
const sayHi = (event) => alert('Hello World')
return (
<div onClick={sayHi}>Hello World</div>
)
}

以前一个组件是通过继承 Component 来构建,一个子类就是一个组件。而用函数式的组件编写方式是一个函数就是一个组件,你可以和以前一样通过 <HellWorld /> 使用该组件。不同的是,函数式组件只能接受 props 而无法像跟类组件一样可以在 constructor 里面初始化 state。你可以理解函数式组件就是一种只能接受 props 和提供 render 方法的类组件。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Link需要父组件传入active,children和onClick三个属性
class Link extends React.Component {
static propTypes = {
active: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired,
onClick: PropTypes.func.isRequired
}

render() {
const { active, children, onClick } = this.props
console.log(111,children)
return (
<button
onClick={onClick}
disabled={active}
style={{
marginLeft: '4px'
}}
>
{children}
</button>
)
}
}


export default Link
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Link的容器组件通过mapStateToProps赋予active属性,通过mapDispatchToProps赋予onClick属性
import { connect } from 'react-redux'
import { setVisibilityFilter } from '../actions'
import Link from '../components/Link'

const mapStateToProps = (state, ownProps) => ({
active: ownProps.filter === state.visibilityFilter
})

const mapDispatchToProps = (dispatch, ownProps) => ({
onClick: () => dispatch(setVisibilityFilter(ownProps.filter))
})

export default connect(
mapStateToProps,
mapDispatchToProps
)(Link)
1
2
3
4
5
6
7
8
9
10
11
12
13
// 调用方给FilterLink赋予children属性(ALL,Active,Completed)
import React from 'react'
import FilterLink from '../containers/FilterLink'
import { VisibilityFilters } from '../actions'

const Footer = () => (
<div>
<span>Show: </span>
<FilterLink filter={VisibilityFilters.SHOW_ALL}>All</FilterLink>
<FilterLink filter={VisibilityFilters.SHOW_ACTIVE}>Active</FilterLink>
<FilterLink filter={VisibilityFilters.SHOW_COMPLETED}>Completed</FilterLink>
</div>
)

示例2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// components/TodoList.js
import React from 'react'
import PropTypes from 'prop-types'
import Todo from './Todo'
import { connect } from 'react-redux'
import { toggleTodo } from '../actions'

class TodoList extends React.Component {
static propTypes = {
todos: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
completed: PropTypes.bool.isRequired,
text: PropTypes.string.isRequired
}).isRequired
).isRequired,
toggleTodo: PropTypes.func.isRequired
}

render() {
const { todos, toggleTodo } = this.props
return (
<ul>
{todos.map(todo => (
<Todo key={todo.id} {...todo} onClick={() => toggleTodo(todo.id)} />
))}
</ul>
)
}
}

const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
case 'SHOW_ALL':
default:
return todos
}
}

// mapStateToProps的全称是:map redux's state to this UIcomponent props
const mapStateToProps = state => ({
todos: getVisibleTodos(state.todos, state.visibilityFilter)
})

const mapDispatchToProps = dispatch => ({
toggleTodo: id => dispatch(toggleTodo(id))
})

export default connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
  1. mapStateToProps的全称是map redux's state to this UIcomponent props,也就是说,先拿取redux中的state,然后进行一些函数的处理,返回出state。然后将这个state传给UI组件

  2. 所以下面的代码执行顺序是:

    1
    2
    3
    {todos.map(todo => (
    <Todo key={todo.id} {...todo} onClick={() => toggleTodo(todo.id)} />
    ))}
    1. mapStateToProps返回todos变量,赋给UI组件的todos这个props
    2. todos是一个array,遍历取出单个元素,得到todo.id
    3. mapDispatchToProps返回toggleTodo交给UI组件的toggleTodo变量这个props
    4. 调用toggleTodo(todo.id)

示例 3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// containers/AddTodo.js
import React from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../actions'


class AddTodo extends React.Component {

render() {
const { dispatch } = this.props
let input
return (
<div>
<form
onSubmit={e => {
e.preventDefault()
if (!input.value.trim()) {
return
}
dispatch(addTodo(input.value))
input.value = ''
}}
>
<input ref={node => (input = node)} />
<button type="submit">Add Todo</button>
</form>
</div>
)
}
}

export default connect()(AddTodo)
  1. 一个UI组件,要想获取dispatch能力的话,就可以使用connect()(AddTodo)
  2. 这样,就可以通过const { dispatch } = this.props获取dispatch函数