当 Redux 和 React 相接合, 就是使用 Redux 进行状态管理, 使用 React 开发页面 UI. 相比传统的 HTML, 使用 React 开发页面, 确实带来了很多好处, 组件化, 代码复用, 但是和 Redux 接合时, 组件化却也带来了一定的问题, 组件层层嵌套, 有成千上百个, 而 store 确只有一个, 组件中怎么才能获取到 store? 页面 UI 就是显示应用程序状态的, 如果获取不到 store 中的 state, 那就没法渲染内容了. 还有一个问题, 就是如果状态发生了变化, 组件怎么做到实时监听, 实时显示最新的状态?
对于第一个问题, React 组件中怎么获取到 store, 你可能想到了, 在整个应用程序的最外层组件中把 store 作为 props 层层向下传递, 对于一个小程序, 还可以接受, 但对于一个大型程序呢, 不可能成千上百个组件中都写上 store 属性吧. 还有一个解决方案就是 context, 把所有组件包含在一个 context 中, context 提供 store 属性, 这样就不用层层传递, 且所有的组件都会获取到 store., 方案可以一试
对于第二个问题, 组件内部想要实时显示最新的状态, 那就要使用 store.subscribe() 方法, 在其里面注册监听函数, 获取最新状态, 然后注入到组件中, 组件更新的方法, 就是调用 setState() 方法, 那我们的每一个组件都变成了有状态的组件. 那 store.subsribe() 方法, 什么时候注册监听函数, 必须组件加载完就要注册, componentDidMounted 里调用 store.subscribe().
根据以上两点分析, 尝试写一下代码, 看不能能实现 Redux 和 React 的接合, 使用 create-react-App 创建项目 react-redux-demo, 然后 cd react-redux-demo && NPM i Bootstrap redux --save, 安装 boostrap 和 redux. 打开项目, 在 index.JS 中引入 boostrap.
- import React from 'react';
- import ReactDOM from 'react-dom';
- import './index.css';
- import App from './App';
- import 'bootstrap/dist/css/bootstrap.css'; // 添加 Bootstrap 样式
- ReactDOM.render(<App />, document.getElementById('root'));
还是最简单的加减 counter 开始, 点击 add 加 1, 点击 minus 减 1, 点击 reset 重置. 看一下 Redux, 由于 Redux 就是 action, reducer, store, 和 React 一点关系都没有, 所以完全把创建 action, 创建 stroe 的内容写成单独的文件, 只暴露出 React 需要的东西给它调用就好了. React 需要 store, 需要 action, 因为它要 dispatch action 来改变状态. 简单起见, 把 redux 的有关内容都放到一个文件中, 在 src 目录下新建一个文件 redux.JS
- import {createStore} from 'redux';
- // state
- const initialState = { counter: 5 };
- // action
- const add = { type: 'ADD' };
- const minus = { type: 'MINUS' };
- const reset = { type: 'RESET' };
- // reducer
- function counter(state = initialState, action) {
- switch(action.type) {
- case 'ADD':
- return {
- ...state,
- counter: state.counter + 1
- }
- case 'MINUS':
- return {
- ...state,
- counter: state.counter - 1
- }
- case 'RESET':
- return {
- ...state,
- counter: 5
- };
- default:
- return state;
- }
- }
- // 创建 store
- const store = createStore(counter);
- // export 出去 store 和 action
- export {store, add, minus, reset};
现在就要写 React, 创建页面 ui, 先不管交互, 先把页面三个按钮和状态的显示画出来, 在 src 下创建一个 ThreeButton.JS,
- import React, { Component } from 'react'
- export default class ThreeButton extends Component {
- render() {
- return (
- <div style={{textAlign: "center"}}>
- <h1 id="counter">0</h1>
- <button type="button" className="btn btn-primary" style={{marginRight: '10px'}}>Add</button>
- <button type="button" className="btn btn-success" style={{marginRight: '10px'}}>Minus</button>
- <button type="button" className="btn btn-danger">Reset</button>
- </div>
- )
- }
- }
然后在 App.JS 中引入
- import React from 'react';
- import ThreeButton from './ThreeButton';
- function App() {
- return (
- <ThreeButton></ThreeButton>
- );
- }
- export default App;
准备实现 React 和 Redux 的接合, 实现页面的交互. 首先就是要把 store 注入到 React 中, 使用 React 的 context API. context 使用的最开始, 是使用 createContext 创建一个 context, 在 src 目录下新建一个 storeContext.JS
- import React from 'react';
- const storeContext = React.createContext({
- store: {
- }
- })
- export {
- storeContext
- };
storeContext 有一个属性 Provider, 它是一个组件, 有一个 value 属性, 提供真正的组件共享数据, 这里就是 Redux 创建的 store 了. 然后用 Provider 把组件包起来, 该组件和它的子组件都能够获取到共享数据, 那就把 App 包起来, 那就在 index.JS 中把 Redux 的 store 和 storeContext.JS 引入
- import React from 'react';
- import ReactDOM from 'react-dom';
- import './index.css';
- import App from './App';
- import 'bootstrap/dist/css/bootstrap.css'; // 添加 Bootstrap 样式
- import { store } from './redux'; // 引入 store
- import { storeContext } from './storeContext'; // 引入 storeContext
- // 提供 sotre 作为共享数据, App 及其子组件都能获取到 store
- const Provider = <storeContext.Provider value={{store: store}}>
- <App />
- </storeContext.Provider>
- ReactDOM.render(Provider, document.getElementById('root'));
那 ThreeButton.JS 就可以获取到 store, 那具体是怎么获取到 store 的呢? 首先还是引入 storeContext, 然后在类中加一个静态属性 contextType, 它赋值为 storeContext, 然后组件中就可以使用 this.context 获取到 store 了.
- import React, { Component } from 'react';
- import { storeContext } from './storeContext'; // 引入 storeContext
- export default class ThreeButton extends Component {
- static contextType = storeContext; // 加静态属性 contextType, 赋值为 storeContext
- componentDidMount() {
- let {store} = this.context; // this.context 获取到 store
- console.log(store);
- }
- render() {
- return (
- <div style={{textAlign: "center"}}>
- <h1 id="counter">0</h1>
- <button type="button" className="btn btn-primary" style={{marginRight: '10px'}}>Add</button>
- <button type="button" className="btn btn-success" style={{marginRight: '10px'}}>Minus</button>
- <button type="button" className="btn btn-danger">Reset</button>
- </div>
- )
- }
- }
可以看到控制台打印出了 store. 组件终于获取到 store, 那就要从 store 中获取 state, 注入组件中, 那组件就要声明一个状态 allState 来接收 store 中的 state, 同时在 componentDidMounted 的时候, 调用 setState 给它赋值
- static contextType = storeContext; // 加静态属性 contextType, 赋值为 storeContext
- state = {
- allState: {}
- }
- componentDidMount() {
- let {store} = this.context; // this.context 获取到 store
- this.setState({
- allState: store.getState()
- })
- }
把 h1 中 0 改为从状态获取
<h1 id="counter">{this.state.allState.counter}</h1>
页面中显示为 5, 没有问题, 表明从 store 中获取的状态没有问题. 那就要给三个按钮添加 click 事件了, dispatch action 来改变状态, 那就添加三个函数. 首先从 redux.JS 中引入三个 action , 然后声明三个函数 dipatch action, 最后就是给按钮添加上 click 事件.
- import { add, minus, reset } from './redux';
- .....
- add = () => {
- let {store} = this.context;
- store.dispatch(add);
- }
- minus = () => {
- let {store} = this.context;
- store.dispatch(minus);
- }
- reset = () => {
- let {store} = this.context;
- store.dispatch(reset);
- }
- ...
- <button type="button" className="btn btn-primary" style={{marginRight: '10px'}}
- onClick={this.add}>Add</button>
- <button type="button" className="btn btn-success" style={{marginRight: '10px'}}
- onClick={this.minus}>Minus</button>
- <button type="button" className="btn btn-danger"
- onClick={this.reset}>Reset</button>
点击了按钮, 页面的状态并没有刷新, 那就是没有 subscribe 监听状态的改变, 还是在 componentDidMounted 页面里面调用 store.subscribe, 它的回调函数也很简单, 就是获取状态, 调用 setState()
- componentDidMount() {
- let {store} = this.context; // this.context 获取到 store
- this.setState({
- allState: store.getState()
- })
- store.subscribe(() => {
- this.setState({
- allState: store.getState()
- })
- })
- }
至此, react 和 redux 算是接合成功了. 整个 threeButton.JS 如下
- import React, { Component } from 'react';
- import { storeContext } from './storeContext'; // 引入 storeContext
- import { add, minus, reset } from './redux';
- export default class ThreeButton extends Component {
- static contextType = storeContext; // 加静态属性 contextType, 赋值为 storeContext
- state = {
- allState: {}
- }
- componentDidMount() {
- let {store} = this.context; // this.context 获取到 store
- this.setState({
- allState: store.getState()
- })
- store.subscribe(() => {
- this.setState({
- allState: store.getState()
- })
- })
- }
- add = () => {
- let {store} = this.context;
- store.dispatch(add);
- }
- minus = () => {
- let {store} = this.context;
- store.dispatch(minus);
- }
- reset = () => {
- let {store} = this.context;
- store.dispatch(reset);
- }
- render() {
- return (
- <div style={{textAlign: "center"}}>
- <h1 id="counter">{this.state.allState.counter}</h1>
- <button type="button" className="btn btn-primary" style={{marginRight: '10px'}}
- onClick={this.add}>Add</button>
- <button type="button" className="btn btn-success" style={{marginRight: '10px'}}
- onClick={this.minus}>Minus</button>
- <button type="button" className="btn btn-danger"
- onClick={this.reset}>Reset</button>
- </div>
- )
- }
- }
现在回想一下组件中获取 store, dipatch aciton , 和实现实时监听的步骤, 你会发现当我们再创建另外一个组件的时候, 它也有好多相同的步骤 ,
1 , 添加静态属性 contextType, 使我们整个组件都能够获取到 store, 肯定相同
2, 添加 state 来接受 store 中的 state, 肯定相同.
3,componentDidMounted 下获取 state , 监听 state, 肯定相同
4, dispatch action, 这个几乎不相同, 因为每一个组件触发的 action 不同
5,render state, 就是页面的 ui, 这个也几乎不同.
我们可以把这个组件分为三个部分, 相同的部分不动, 那不同的部分要怎么处理? 对于不同的部分, 通常都是使用函数, 不同的部分通过传参的形式传递进来, 那就要写一个函数, 返回这个组件. 由于 action 和 ui 是两个不同类型的东西, 可以分为两种不同的参数, 那这个函数接受 action 返加一个函数, 返加函数再接受一个 ui 组件, 再返回一个组件, 这个组件包含相同的部分. 相当于
- function connect(action) {
- return function(Componnet) {
- return class extends React.componnet {
- // 相同的部分
- render() {
- return <Componnet {...this.state}></Componnet>
- }
- }
- }
- }
只要把这个函数封装起来, 以后直接调用这个函数, 就实现了组件自动获取到 store, 自动监听变化, 我们只要写 ui 和 action, 然后传递进去就可以了. 这个函数其实第三方组件已经封装好了, 那就是 react-redux 库, 它提供了一个 connect 方法, 看一下它的 API, 最常用的就是下面的方式
connect(mapStateToProps, mapDispatchToProps)(MyComponent).
和我们自己写的 connect 函数使用方法一致, 只不过它的第一个函数可以接受更多的参数, mapStateToProps, 把 state 转化成 props, 因为 connect 返回的组件中能够获取到 store 中的 state, 它要把 state 传递给 myComponnet, 因为 myCompoent 才是负责渲染 ui, 对于 myComponnet 来说, 它就是 props. 同理也适用于 mapDispatchToProps, 在 connect 的组件它是能获取到 store 中的 dispatch, 当传递给 myComponent 的时候, 它就变成了 Props. 再看一下这两个参数怎么使用, 首先它们是函数, 然后返回对象. 为什么要这样设计呢? 只有函数, 才能调用, 才能通过参数把 state 和 disptch 进行注入, 返回对象, 便于对象的合并, 把所有对象进行合并, 形成 props 传递给 myComponnet.
对于 mapStateToProps 来说, 它接受一个 state 作为参数, 返回一个对象, 这个对象中的属性就可以在 myComponet 中使用 props 进行获取并使用, 值呢? 就是 参数 state 中的属性, myComponent 组件要用到 state 中的哪个属性, 就读取 state 中的哪个属性作为参数.
- const mapStateToProps = state => ({
- counter: state.counter
- })
mapDispatchToProps, 则相对麻烦一点, 它接受一个 dispatch 作为参数, 返回的对象中属性也是可以在 myComponet 中使用 props 进行获取并使用, 值呢, 是一个函数, 参数可以接受也可以不接受, 函数体则是 dispatch action
- const mapDispatchToProps = dispatch => ({
- add: () => dispatch({type: 'ADD'})
- })
React-Redux 除了 connect 函数外, 还提供了 Provider 组件, 和我们自定义的 storeContext.Provider 一致, 不过它的使用方式是直接提供属性, 组件身上的属性都能被子组件获取到. NPM i react-redux --save 使用 react-redux 重写组件.
首先把 storeContext.JS 文件去掉, 然后在 index.JS 中从 React-Redux 引入 Provider 组件, 包含 App
- import React from 'react';
- import ReactDOM from 'react-dom';
- import './index.css';
- import App from './App';
- import 'bootstrap/dist/css/bootstrap.css'; // 添加 Bootstrap 样式
- import { store } from './redux'; // 引入 store
- import { Provider } from 'react-redux';
- // 提供 sotre 作为共享数据, App 及其子组件都能获取到 store
- const ProviderWrapper = <Provider store={store}>
- <App />
- </Provider>
- ReactDOM.render(ProviderWrapper, document.getElementById('root'));
然后 ThreeButton.JS 就要分为两部分了, 一个部分是 connect 函数中的第一部分 connect(mapStateToProps, mapDispatchToProps), 主要的作用就是把 store 获取, 转化为 props.
另一部分是 connect 的第二部分 myComponent, 它呢, 就是接受到 props, 渲染组件. 在一个文件中也可以, 分两个文件也没有问题. 我们就在一个文件中写了,
- import React from 'react';
- import { add, minus, reset } from './redux';
- import { connect } from 'react-redux';
- // 纯渲染组件
- function ThreeButton(props) {
- return (
- <div style={{textAlign: "center"}}>
- <h1 id="counter">{props.counter}</h1>
- <button type="button" className="btn btn-primary" style={{marginRight: '10px'}}
- onClick={props.add}>Add</button>
- <button type="button" className="btn btn-success" style={{marginRight: '10px'}}
- onClick={props.minus}>Minus</button>
- <button type="button" className="btn btn-danger"
- onClick={props.reset}>Reset</button>
- </div>
- )
- }
- // 把 store 中的 state 转化为纯渲染组件 props
- const mapStateToProps = state => ({
- counter: state.counter
- })
- // 获取 store 中的 dispatch, 同时和 action 接合, 组成纯渲染组件 props, 渲染组件中, 直接调用对象的属性, 就可以 dispatch action 了
- const mapDispatchToProps = dispatch => ({
- add: () => dispatch(add),
- minus: () => dispatch(minus),
- reset: () => dispatch(reset)
- })
- // connect 函数把它们接合起来, ThreeButton 就可以使用 props 来使用 mapStateToProps 和 mapDispatchToProps 中返回的对象属性
- // 同时返回一个组件, 可以在父组件 App.JS 中直接调用
- export default connect(
- mapStateToProps,
- mapDispatchToProps,
- )(ThreeButton)
来源: http://www.bubuko.com/infodetail-3161290.html