第一次使用react-redux时,到网上搜各种资料学习,结果发现一大堆资料全在谈redux,搞的蒙头转向。这些文章的作者忽略了一个问题,对于新手而言,其实更多的是要从自己熟悉的知识体系去延生开,而不是上来搞一堆概念。和这些资料不同,我写这篇文章,反其道而行,直接先将react中使用react-redux,然后慢慢反推redux。对于读者而言,更多的是从已知的react知识推演过去,理解之后再去理解redux。
安装
npm install --save redux react-redux
安装react-redux时要一起安装redux,因为我们要用到redux的一个方法,但这个方法我们只管用,不用深纠。
改造入口文件
一般我们会有一个入口文件,我一般使用index.js,因为react是一个组件的UI构建库,所以对于一个web应用而言,一切都是组件。但是在入口文件里,我不是再写组件,而是在写把组件挂载到DOM里面,所以,我的index.js是这样写的:
import React from 'react' import { render } from 'react-dom' import { HashRouter as Router, Route } from 'react-router-dom' import App from './App' import './styles/index.scss' render( <Router> <Route path='/' component={App} /> </Router> , document.getElementById('app'), )
这样的代码结构就非常简明。就是利用react-dom来渲染到#app这个div中。
react中的组件在我看来有两类,一类是纯粹的UI组件,比如一个弹出框、下拉菜单、tooltip等等,它们是独立的,在任何应用中都可以复用,只要传props就可以了。而另一类,则称之为业务组件,包括整个页面的布局、数据交互等等,这类组件实际上通过对纯UI组件的包装,实现了真正的应用的业务作用。不同的应用,可以用相同的UI组件库,但自己的业务必须靠自己完成。所以,虽然都叫组件,但是业务组件的复用性往往仅限于同一个应用中。
现在我们要改造这个入口index.js文件来使用react-redux。
我们假设改造的目的,是在App组件中加入一个loading效果,这个loading效果可以被App的任何子组件触发,也就是说整个应用只有一个loading,但是在任何地方都可以显示/隐藏。react-redux的作用就是建立一套所有组件都可以访问的state系统,这个系统被称为store。玩过vuex的小伙伴应该秒懂。不懂也不要紧,简单的说就是store里面存放了一堆state,同时store提供了获取state的方法getState,修改state的方法dispatch,以及监听state被改动的方法subscribe。但是这三个方法对于我们使用react-redux的小伙伴而言,只需要用到dispatch方法,我们会把state直接注入到组件的props中,而监听state变化后重新渲染界面react-redux已经自动完成了。
改造后的代码如下:
import React from 'react' import { render } from 'react-dom' import { HashRouter as Router, Route } from 'react-router-dom' import { Provider } from 'react-redux' import { createStore } from 'redux' import App from './App' import './styles/index.scss' const store = createStore((state, action) => { if (action === 'showLoading') return { loading: true } return { loading: false }}) /** * action就是在组件里面调用dispatch方法的时候传入的参数,怎么在组件中调用下文会讲 * state就是全局state,其实和组件内的this.state是一个道理,都是一个对象 * return后面是一个新的state,这个state会覆盖上一个state * 有一点必须注意,这个地方return的state必须是一个新的object,而不能直接通过修改state之后return回来,比如下面这种操作就是错的: *state.loading = false; return state;// 这样做绝对不行,谨记! * 如果你的state有很多个属性,要使用Object.assign进行克隆,再这样操作: * let newState = Object.assign(state); newState.loading = false; return newState; // 这样可以 */ render( <Provider store={store}> <Router> <Route path='/' component={App} /> </Router> </Provider> , document.getElementById('app'), )
Provider组件把你的整个App项目的组件包含起来,那么这些包含起来的组件(以及它们的子组件)就可以被提供react-redux的注入。注意,我这里说的是“可以”,而不是一定,要实现真正使用react-redux,我们还要改造Provider包含起来的组件才行。
改造组件
前面说了,用Provider组件包起来的组件可以被注入react-redux的功能,那么怎么改造呢?用一个叫connect的函数对现有组件进行二次构造,得到一个新的组件,将这个组件export出去。
我们来看下我原来的App组件是怎么写的:
import React, { Component } from 'react' export default class App extends Component { constructor(props) { super(props) this.state = { loading: false, } } toggleLoading() { this.setState({ loading: !this.state.loading, }) } render() { return ( <div> ... <a href="javascript:" onClick={this.toggleLoading.bind(this)}>toggle loading</a> ... <div className={'loading' + this.state.loading ? '' : ' hidden'}>loading...</div> </div> ) }}
上面我用了组件内的state.loading来控制整个页面的loading是否显示。但是,如果我们要在其他组件中使用loading,就必须不断通过子组件的props传递toggleLoading方法,那也实在太蠢了。
解决这个问题的办法有很多,比如通过react提供的props.children来递归注入,使得每一个内部组件都拥有toggleLoading方法的调用权限。但是,我们这里使用react-redux来解决,让每一个经过改造的组件都拥有修改store中的state的能力,通过修改这个state来达到重新渲染App组件中的loading的效果。
改造后如下:
import React, { Component } from 'react' import { connect } from 'react-redux' class App extends Component { constructor(props) { super(props) } toggleLoading() { this.props.dispatch('showLoading') } render() { return ( <div> ... <a href="javascript:" onClick={this.toggleLoading.bind(this)}>toggle loading</a> ... <div className={'loading' + this.state.loading ? '' : ' hidden'}>loading...</div> </div> ) } } function mapStateToProps(state) { return { loading: state.loading, } // 这里的state是react-redux store中的state,前面我们已经写过相关的代码,return { loading: false } } export default connect(mapStateToProps)(App)
红色部分是修改后的起作用代码,其实主要实现了两件事,1)将store中的state.loading作为组件的props.loading的值,2)通过connect实现将dispatch方法注入到组件的props.dispatch。
上面是对App组件的改造,如果你要在其他组件(包括App的子组件)中也想对loading界面进行控制,也可以参照上面的代码进行改造。改造之后,这些组件仅仅和react-redux产生了依赖关系,组件与组件完全解耦,但是却可以通过react-redux作为桥梁,相互控制对方的UI展示。
遗留问题
你可能会想,如果一个组件必须依赖react-redux,那么它的独立性是不是减弱了?是的,这是一定的,所以我前文小字部分已经说了,业务型组件本身就对整个项目的数据结构之类的有依赖。比如说这里改造后的App组件,在mapStateToProps函数中我们直接使用了state.loading,假如state是一个null不就会报错么?那是因为我们在Provider的时候就已经知道state一定有loading属性,所以这肯定是有依赖的。因此,redux的规则里面明确说了要将UI和业务分离开发的思想。
当然,你肯定还会有其他疑虑,比如说state很庞大怎么办?react-redux是怎么做到自动更新界面的?connect具体的用法是什么?等等这样的问题。这些问题本文都不会回答,希望有机会可以深入去写,下面给一些参考资料: