Redux 入门教程(三):React-Redux 的用法

作者: 阮一峰

日期: 2016年9月21日

珠峰培训

前两篇教程介绍了 Redux 的基本用法异步操作,今天是最后一部分,介绍如何在 React 项目中使用 Redux。

为了方便使用,Redux 的作者封装了一个 React 专用的库 React-Redux,本文主要介绍它。

这个库是可以选用的。实际项目中,你应该权衡一下,是直接使用 Redux,还是使用 React-Redux。后者虽然提供了便利,但是需要掌握额外的 API,并且要遵守它的组件拆分规范。

一、UI 组件

React-Redux 将所有组件分成两大类:UI 组件(presentational component)和容器组件(container component)。

UI 组件有以下几个特征。

  • 只负责 UI 的呈现,不带有任何业务逻辑
  • 没有状态(即不使用this.state这个变量)
  • 所有数据都由参数(this.props)提供
  • 不使用任何 Redux 的 API

下面就是一个 UI 组件的例子。


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的意思,就是将这两种组件连起来。


import { connect } from 'react-redux'
const VisibleTodoList = connect()(TodoList);

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

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

(1)输入逻辑:外部的数据(即state对象)如何转换为 UI 组件的参数

(2)输出逻辑:用户发出的动作如何变为 Action 对象,从 UI 组件传出去。

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


import { connect } from 'react-redux'

const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

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

四、mapStateToProps()

mapStateToProps是一个函数。它的作用就是像它的名字那样,建立一个从(外部的)state对象到(UI 组件的)props对象的映射关系。

作为函数,mapStateToProps执行后应该返回一个对象,里面的每一个键值对就是一个映射。请看下面的例子。


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

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

下面就是getVisibleTodos的一个例子,用来算出todos


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会订阅 Store,每当state更新的时候,就会自动执行,重新计算 UI 组件的参数,从而触发 UI 组件的重新渲染。

mapStateToProps的第一个参数总是state对象,还可以使用第二个参数,代表容器组件的props对象。


// 容器组件的代码
//    <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是一个函数,会得到dispatchownProps(容器组件的props对象)两个参数。


const mapDispatchToProps = (
  dispatch,
  ownProps
) => {
  return {
    onClick: () => {
      dispatch({
        type: 'SET_VISIBILITY_FILTER',
        filter: ownProps.filter
      });
    }
  };
}

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

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


const mapDispatchToProps = {
  onClick: (filter) => {
    type: 'SET_VISIBILITY_FILTER',
    filter: filter
  };
}

六、<Provider> 组件

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

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

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


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属性,请看源码。


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,代码大致如下。


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
}

React-Redux自动生成的容器组件的代码,就类似上面这样,从而拿到store

七、实例:计数器

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


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的映射。


function mapStateToProps(state) {
  return {
    value: state.count
  }
}

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

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

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


const App = connect(
  mapStateToProps,
  mapDispatchToProps
)(Counter)

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


// 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在根组件外面包一层。


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')
);

完整的代码看这里

八、React-Router 路由库

使用React-Router的项目,与其他项目没有不同之处,也是使用ProviderRouter外面包一层,毕竟Provider的唯一功能就是传入store对象。


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

(完)

一灯学堂

优达学城

留言(66条)

第三点的
三、conect()
拼写错了

@羽风:谢谢指出,已经改正。

有一点不是特别明白:

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

这里store.subscribe时,本来只要state有变化就会执行吧,为啥还要再使用throttle呢?
是有什么考虑吗?

哦,我理解了,是怕性能问题,避免调用太频繁。

简单明了,大赞~

‘容器组件则是由 React-React 自动生成’
这里应该是React-Redux

Redux 入门教程(三)真是通俗易懂,让我茅塞顿开,给阮老师点个赞!么么哒

@Min:谢谢指出,已经改正。

之前在github上看了好几个入门教程,都不是很明白.
当时flux就是从阮老师这儿看懂的.给阮老师点赞.

给阮老师点个赞

峰哥可以搞一个大赏功能,想给你大赏都不知道往哪打

很赞,多谢!阮老师的页面风格看着都比较权威。

这个对象有一个todos属性,代表 UI 组件的同名参数, 这句话怎么理解?

求下一篇有 react redux 后台渲染的文章啊

最后使用Provider在根组件外面包一层的代码,用的是todo的,不是计算器的了

感谢阮老师,茅塞顿开!

引用丢丢的发言:

峰哥可以搞一个大赏功能,想给你大赏都不知道往哪打

支持,确实比其它的资料更容易懂

七:实例 计数器中

const store = createStore(
  counter, //原为todoApp
  persistedState
);

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

React-Redux提供的例子:https://github.com/jackielii/simplest-redux-example 是用python运行的。我把例子代码放到webpack环境下了,地址:https://github.com/cag2050/react_redux_demo161019 ,需要的同学,可以看下。

请问store.subscribe和mapStateToProps有什么关系吗?

阮一峰老师每篇关于react及其技术栈的文章都拯救了我。。!!!!!

阮老师好~ 想请教个问题: 当组件第一次渲染的时候 并没有看见哪里触发了dispatch,那是怎么更新的UI呢? 是react默认第一次会自动触发dispatch吗?

引用chris的发言:

请问store.subscribe和mapStateToProps有什么关系吗?

是在高阶组件(connect 函数返回的组件)里的 componentDidMount 订阅 store 更新,和 componentWillUnmount 取消订阅。(还有声明 contextTypes...)

安利下我的学习笔记...~= ̄ω ̄=~

http://buptsteve.github.io/blog/2016/10/25/7.react-and-redux-learning-note-basics/


AddTodo
像这种需要将input的内容dispatch出去的要如何划分成ui组件和容器组件呢?

引用steve young的发言:

是在高阶组件(connect 函数返回的组件)里的 componentDidMount 订阅 store 更新,和 componentWillUnmount 取消订阅。(还有声明 contextTypes...)

安利下我的学习笔记...~= ̄ω ̄=~

http://buptsteve.github.io/blog/2016/10/25/7.react-and-redux-learning-note-basics/

感谢,关于官网的real-world示例,有一些问题,不知道可否请教下~

const mapDispatchToProps = {
onClick: (filter) => {
type: 'SET_VISIBILITY_FILTER',
filter: filter
};
}

这一句的话,箭头函数返回一个对象对象不用()包起来不会报错吗?

mapStateToProps会订阅 Store,每当state更新的时候,就会自动执行,重新计算 UI 组件的参数,从而触发 UI 组件的重新渲染。

阮老师,在react-redux v4.4.5版本的connect函数中,并没有找到mapStateToProps订阅Store的行为,请问您上面说的那句话该如何理解?

比如一个简单的状态 现实隐藏某个组件 类似这种状态也要放到给redux管理吗 这个应该属于ui组件 具体在项目中应该如何划分呢?

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

var num = 0;
ReactDom.render((
<Provider store={store}>
<MyCounter num={num}/>
</Provider>
), document.getElementById('app'));

setTimeout(function() {
num =1;
}, 1000)


我这样测,发现,页面并没有渲染呀。希望老师抽空能解答一下,不胜感激!

redux大法看的好吃力

乏味,且不明所以。感觉作者自己也不懂

感谢阮老师的付出,阮老师的博文总是清晰易懂。

阮老师为什么不在文章末尾放个支付宝或者微信的二维码呢?我想给阮老师买杯咖啡☕️ ~ 相信很多人也会有这想法的,毕竟我们从阮老师这里学到了很多~

最近对React-Native大感兴趣,大概地撸了一遍阮老师的博客,虽然不是前端出身,但理解起来不难,好文章。

第三章说了异步操作,可是你 react-redux 里面没有说,怎么异步操作啊。是参照第二章吗?我现在使用react-redux 请教,怎么选择异步fetch 返回数据啊。

如果你省略这个 mapDispatchToProps 参数,默认情况下,dispatch 会注入到你的组件props 中。


请问阮老师, Counter例子中的createStore代码:

const store = createStore(
todoApp,
persistedState
);

是不是有问题? createStore接受的第一个参数应该是reducer(本例中是counter), 而不是UI组件。

总觉得代码部分用粉红有点突兀,影响阅读,颜色是否可以弱化点

引用nicemayi的发言:

请问阮老师, Counter例子中的createStore代码:

const store = createStore(
todoApp,
persistedState
);

是不是有问题? createStore接受的第一个参数应该是reducer(本例中是counter), 而不是UI组件。

这里的createStore确实应该是传参数名错了

虽然没太看懂,打算再看一遍 ,谢谢阮老师的良心教程

const store = createStore(
todoApp,
persistedState
);

这里的确不是Counter的代码,源代码大家可以看阮老师的源代码,应该是这样的:
// Store
const store = createStore(counter);

收益匪浅啊,感谢阮老师。

谈一下我阅读了阮老师三篇文章后的理解,很有可能是有偏差的,不足请指出,谢谢!


阮老师上文中关于Provider的解释,缺少了对context的介绍,我补充一下吧:

为了让子组件能够获得context属性,React强制要求根组件(此处为Provider组件)提供getChildContext实例方法,以及类属性childContextTypes。而子组件想要获取context,也必须定义类级别的Counter. contextTypes属性。定义是双向的,如果缺少了任何一块,子组件都获取不到context属性。

我认为父组件的那块定义是在Provider的代码中实现的,而子组件的那部分是在connect方法中实现的。

因此connect方法为Counter组件添加的context属性实质上是由Provider传下来的,这样在mapStatesToProps方法里的state参数实质上就是this.context.store.getState()方法获得的。

然后看一下页面首次加载以及之后有互动行为之后整个逻辑的流程:

当第一次渲染页面时,store里的初始state是怎么获得的呢?
代码一开始一般就是
createStore(reducers,defaultParams)的调用,其中reducers可以使一个reducer,也可是redux.combineReducers过的reducer的集合。

createStore方法会对每个reducer去dispatch一个action.type=@@redux/INIT类型的action,而这个action一般在reducer的代码里不会被handle,直接掉入default块,于是就返回了state的初始状态。

然后一般就会ReactDom.render(将应用渲染出来,每个子组件的容器组件通过传入this.context.store.getState()方法获得的state对象, 以及容器组件上自带的ownProps给mapStatesToProperties方法,来构建props,最后将props应用到子组件的UI组件上。

当在子组件上发生交互行为,如click时,mapDispatchToProps会定义click触发时应该dispatch哪一个action的映射。
然后store接收到这个action后会进行reduce,得到最新的state,然后再调用所有的子组件的mapStatesToProps方法生成新的props。
最后对Provider进行重新渲染,当然上面的事件计算出来的很多state可能都不会发生变化,所以diff算法不会去修改这些没有发生变化的组件,因此性能也比较好。

疑问开头的UI和container的划分:
(UI只负责呈现,不包含业务逻辑;container只负责业务逻辑不包含UI呈现)

对于一个页面, 真的能划分只有UI和container两种组件吗?
假设一个页面对应一个container,我的情况是总会出现在页面级container下混合UI+其他container。。
求解。。

请教大神或各位个问题,
在ui组件中,如果行为中需要传参该怎么做?
比如目前是 ;
但比如有多个button,需要sendClickAction时传递入参id,怎么做?


三篇看下来有点累,什么时候歇好了再来看,一峰老师写得好!

五的最后:
const mapDispatchToProps = {
onClick: (filter) => {
type: 'SET_VISIBILITY_FILTER',
filter: filter
};
}
onClick的返回值应该加上()。({type: xxx})

建议阮一峰老师增加子组件如何通过context 获取到store的样例,对新手有较大帮助,我是看了官网文档之后才知道方法.
https://facebook.github.io/react/docs/context.html

引用finalljx的发言:

建议阮一峰老师增加子组件如何通过context 获取到store的样例,对新手有较大帮助,我是看了官网文档之后才知道方法.
https://facebook.github.io/react/docs/context.html

貌似官方不建议使用context。
It is an experimental API and it is likely to break in future releases of React.

阮大神讲的果然通俗易懂,解决了我多年的困惑!感谢老师

然道就我一个人看不懂嘛....

懵逼中。。。。继续第二遍

const mapDispatchToProps = {
onClick: (filter) => {
type: 'SET_VISIBILITY_FILTER',
filter: filter
};
}

这个函数是不是应该写成
const mapDispatchToProps = {
onClick: (filter) => ({
type: 'SET_VISIBILITY_FILTER',
filter: filter
});
}

箭头函数直接返回对象的话要加上括号

const { store } = this.context;

然而并不行

求ruan老师贴代码的时候保证可执行,免得误导人!

最近在学react,看了很多,就这篇最有效,讲的太到位了。尤其是redux底层的原理,简直不能更赞。

好屌啊!看着阮大神的代码,长大的!

获益匪浅

引用a的发言:

const { store } = this.context;

然而并不行

求ruan老师贴代码的时候保证可执行,免得误导人!

<Provider store={store}>
<App />
</Provider>

App所有子组件无法获取到store,因为this.context空值,请大师指教

react 的props不是应该放一些不变的东西吗?那现在右container的state每次提供不同的数据,不是和react的props设计思想有出入吗

引用Andy的发言:

感谢阮老师的付出,阮老师的博文总是清晰易懂。

阮老师为什么不在文章末尾放个支付宝或者微信的二维码呢?我想给阮老师买杯咖啡☕️ ~ 相信很多人也会有这想法的,毕竟我们从阮老师这里学到了很多~

你是托把

有人能帮我看下我的这个问题吗
是不是reducers写的有问题,
这是效果 https://amanzyw.github.io/react-redux/dist/

项目地址在:https://github.com/amanzyw/react-redux/tree/gh-pages

似懂非懂,写个demo先

阮老师,在实例的第四行文字解释是不是写错了?
“接着,定义value到state的映射,以及onIncreaseClick到dispatch的映射。”
应该是 “定义state到value的映射”吧

讲解的非常细致,自己看文档很多东西走马观花,理解不透彻。做项目的时候容易出现问题,再看看阮老师的文档有种茅塞顿开的感觉。

阮老师 果然厉害 学习redux 好几天都在蒙圈中。看来后立马感觉豁然开朗。

引用Neil的发言:

const mapDispatchToProps = {
onClick: (filter) => {
type: 'SET_VISIBILITY_FILTER',
filter: filter
};
}

这个函数是不是应该写成
const mapDispatchToProps = {
onClick: (filter) => ({
type: 'SET_VISIBILITY_FILTER',
filter: filter
});
}

箭头函数直接返回对象的话要加上括号

这个代码放到 IDE 里就会提示 error,可能是阮老并不用 IDE ...

const mapDispatchToProps = {
onClick: (filter) => {
type: 'SET_VISIBILITY_FILTER',
filter: filter
};
}
此处应该改为:
const mapDispatchToProps = ({
onClick: (filter) => {
type: 'SET_VISIBILITY_FILTER',
filter: filter
}
})
去掉一个;和加了括号返回对象

我要发表看法

«-必填

«-必填,不公开

«-我信任你,不会填写广告链接