1.创建项目
create-react-app test-redux cd test-redux yarn start yarn eject // 显示配置详细文件
2.在React的思想中,视图的变化对应的是数据的变化,data -> ui
,这里的data可以是组件的新state,或者接收到的新props和context。由于Redux的数据与React组件状态互相独立,这里就需要一个桥梁来连接React与Redux,也就是react-redux。
react-redux这个库提供的api有:Provider、connect、connectAdvanced。
Provider
Provider是一个React组件,这个组件实现很简单,一共55行代码,它只是接收store、children这两个属性,并且定义了包含store和storeSubscription字段的childContext:
// https://github.com/reactjs/react-redux/blob/master/src/components/Provider.js export default class Provider extends Component { getChildContext() { return { store: this.store, storeSubscription: null } } constructor(props, context) { super(props, context) this.store = props.store } render() { return Children.only(this.props.children) } } ... Provider.propTypes = { store: storeShape.isRequired, children: PropTypes.element.isRequired } Provider.childContextTypes = { store: storeShape.isRequired, storeSubscription: subscriptionShape }
Provider的render方法简单的返回了props.children
。Provider的主要作用是提供在组件树的某一个节点通过context获取store的能力。
一般在项目中,会将Provider这个组件放在整个应用的最顶层,然后将使用Redux.createStore
方法创建的store对象传给Provider:
const store = createStore(rootReducer); ReactDOM.render(<Provider store={store}><App/></Provider>, document.getElementById(‘app‘)); // or ReactDOM.render(( <Provider store={store}> <Router history={hashHistory}> <Route path="/"> ... </Route> </Router> </Provider> ), document.getElementById(‘app‘));
connect
在顶层加入Provider组件之后,我们就可以在组件树的节点中通过context获取到store对象了,比如我们将计数器使用React组件重写:
/** * Counter 组件 */ import React, { Component } from ‘react‘; /** * 引入 prop-types * yarn add prop-types */ import PropTypes from ‘prop-types‘; class Counter extends React.Component { render() { // 获取 store const { store } = this.context; // 使用store.getState方法获取保存在store中数据 const count = store.getState(); return ( <div> <p>Count: {count}</p> {/*通过 store.dispatch 触发 action */} <button onClick={() => store.dispatch({type: ‘INCREASE‘})}>+1</button> <button onClick={() => store.dispatch(asyncIncrease())}>+1 async</button> <button onClick={() => store.dispatch({type: ‘DECREASE‘})}>-1</button> </div> ); } } /** * 通过给组件定义一个包含store的contextTypes * 我们就可以在组件内部使用this.context.store获取到store对象 */ Counter.contextTypes = { store: PropTypes.object.isRequired } export default Counter;
在上面的Counter组件中,通过给组件定义一个包含store的contextTypes,我们就可以在组件内部使用this.context.store
获取到store对象,然后使用store.getState
方法获取保存在store中数据并在渲染。
看起来很不错,我们可以将store的数据通过React组件渲染到UI上了,并且同样可以点击按钮来触发action。但是点击按钮你就会发现,显示的Count值一直是0,我们使用store.subscribe
方法来观察store状态的变化:
/** * Counter 组件 */ import React, { Component } from ‘react‘; /** * 引入 prop-types * yarn add prop-types */ import PropTypes from ‘prop-types‘; class Counter extends React.Component { // 生命周期--组件即将加载完毕 componentWillMount(){ // 使用store.subscribe方法来观察store状态的变化 this.context.store.subscribe(() => { console.log(`new store state: ${store.getState()}`); }); } ... } /** * 通过给组件定义一个包含store的contextTypes * 我们就可以在组件内部使用this.context.store获取到store对象 */ Counter.contextTypes = { store: PropTypes.object.isRequired } export default Counter;
打开控制台,按下按钮时,可以发现store里的状态是有变化的:
问题的原因其实在文章的开篇已经提到过了,只有当组件的state、props、context变化时才会引起UI的重新渲染,重新执行render方法。而这里的this.context.store
只是一个包含了getState、dispatch等方法的一个对象,store中状态的更新并不会修改store这个对象,变的只是store.getState()
的结果而已,所以组件Counter组件并不会触发重新渲染。
一个解决办法是,在store.subscribe
中使用this.forceUpdate
方法来强制组件重新渲染:
/** * Counter 组件 */ import React, { Component } from ‘react‘; /** * 引入 prop-types * yarn add prop-types */ import PropTypes from ‘prop-types‘; class Counter extends React.Component { // 生命周期--组件即将加载完毕 componentWillMount(){ // 使用store.subscribe方法来观察store状态的变化 this.context.store.subscribe(() => { console.log(`new store state: ${store.getState()}`); // 在store.subscribe中使用this.forceUpdate方法来强制组件重新渲染 this.forceUpdate(); }); } ... } /** * 通过给组件定义一个包含store的contextTypes * 我们就可以在组件内部使用this.context.store获取到store对象 */ Counter.contextTypes = { store: PropTypes.object.isRequired } export default Counter;
这下结果是对了