【React全家桶入门之十四】在React中使用Redux

在前面两篇文章中,我们通过一个简单的计数器例子介绍了Redux的基本概念和用法。

在计数器这个例子中,当store的数据更新时,UI就会相应地被更新,这是由于我们使用了store.subscribe方法在触发了action的时候执行了setCount方法来更新UI。

在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.childrenProvider的主要作用是提供在组件树的某一个节点通过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组件重写:

class Counter extends React.Component {
  render () {
    const {store} = this.context;
    const count = store.getState();
    return (
      <div>
        <p>Count: {count}</p>
        <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>
    );
  }
}
Counter.contextTypes = {
  store: PropTypes.object.isRequired
};

在上面的Counter组件中,通过给组件定义一个包含store的contextTypes,我们就可以在组件内部使用this.context.store获取到store对象,然后使用store.getState方法获取保存在store中数据并在渲染。

看起来很不错,我们可以将store的数据通过React组件渲染到UI上了,并且同样可以点击按钮来触发action。但是点击按钮你就会发现,显示的Count值一直是0,我们使用store.subscribe方法来观察store状态的变化:

class Counter extends React.Component {
  componentWillMount () {
    this.context.store.subscribe(() => {
      console.log(`new store state: ${store.getState()}`);
    });
  }
  ...
}
...

打开控制台,按下按钮时,可以发现store里的状态是有变化的(CodePen):

height="265" scrolling="no" title="MpaMrW" width="100%" src="//codepen.io/awaw00/embed/MpaMrW/?height=265&theme-id=0&default-tab=js,result&embed-version=2" allowfullscreen="true">See the Pen <a href="http://codepen.io/awaw00/pen/MpaMrW/">MpaMrW</a> by aaron wang (<a href="http://codepen.io/awaw00">@awaw00</a>) on <a href="http://codepen.io">CodePen</a>.&#10;

问题的原因其实在文章的开篇已经提到过了,只有当组件的state、props、context变化时才会引起UI的重新渲染,重新执行render方法。而这里的this.context.store只是一个包含了getState、dispatch等方法的一个对象,store中状态的更新并不会修改store这个对象,变的只是store.getState()的结果而已,所以组件Counter组件并不会触发重新渲染。

一个解决办法是,在store.subscribe中使用this.forceUpdate方法来强制组件重新渲染:

class Counter extends React.Component {
  componentWillMount () {
    this.context.store.subscribe(() => {
      console.log(`new store state: ${store.getState()}`);
      this.forceUpdate();
    });
  }
  ...
}
...

这下结果是对了(CodePen):

height="265" scrolling="no" title="zZrOOL" width="100%" src="//codepen.io/awaw00/embed/zZrOOL/?height=265&theme-id=0&default-tab=js,result&embed-version=2" allowfullscreen="true">See the Pen <a href="http://codepen.io/awaw00/pen/zZrOOL/">zZrOOL</a> by aaron wang (<a href="http://codepen.io/awaw00">@awaw00</a>) on <a href="http://codepen.io">CodePen</a>.&#10;

可是问题也很严重:应用中每个action都会触发组件的重新渲染。

怎么办?先判断store.getState()的值是否变化,再判断组件所需要的数据是否变化…

好了,差不多该停一停了,上面遇到的问题react-redux早就帮我们想好了解决办法,我们只需要使用它的connect方法:

@connect(
  (state) => ({
    count: state
  }),
  (dispatch) => ({
    increase () {
      dispatch({type: 'INCREASE'});
    },
    decrease () {
      dispatch({type: 'DECREASE'});
    },
    increaseAsync () {
      dispatch(asyncIncrease());
    }
  })
)
class Counter extends React.Component {
  render() {
    const {count, increase, decrease, increaseAsync} = this.props;
    return (
      <div>
        <p>Count: {count}</p>
        <button onClick={increase}>+ 1</button>
        <button onClick={increaseAsync}>+ 1 async</button>
        <button onClick={decrease}>- 1</button>
      </div>
    );
  }
}

上面使用的是ES7提案的装饰器(Decorator)写法,现在可以通过babel提前使用。装饰器的本质其实就是一个函数调用和赋值,所以也可以写成:

class Counter extends React.Component {
  ...
}

Counter = connect(
  (state) => ({ ... }),
  (dispatch) => ({ ... })
)(Counter);

connect方法的第一个参数是mapStateToProps,是一个方法,这个方法会接收到state作为参数(就是store.getState()的执行结果),需要在方法中返回一个普通javascript对象(或者一个返回一个方法,属于高级用法,此处不进行讲解),返回的对象会作为props的一部分传递给组件。

第二个参数是mapDispatchToProps,也是一个方法,接收dispatch作为参数(就是store.dispatch),需要在方法中返回一个普通的javascript对象(同样可以返回一个方法),返回的对象也会作为props的一部分传递给组件。

connect其实是一个高阶组件,它会返回一个名为Connect(COMPONENT_NAME)的组件,并根据mapStateToProps与mapDispatchToProps等参数将合并后的props传递给原组件(此处为Counter)。

connectAdvanced

connectAdvanced这个方法不怎么常用,connect方法就是经过这个方法包装而来的。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值