阅读 4799

在 React 中使用 Redux

这是一篇介绍 redux 的入门文章,欲知更多内容请查阅 官方文档

本文会通过三种方式实现一个简单到不能呼吸的计数器小例子,先用 React 实现,再慢慢引入 Redux 的内容,来了解什么是 Redux、为什么要使用 Redux 以及如何简单地使用 Redux。

1、React 实现计数器

上面这个例子用 React 实现起来非常简单,初始化一个 creact-react-app,然后为了页面看起来更美观,在 index.html 加入 bootstrap 的 CDN,并修改 App.js 为如下内容就可以了。

<link href="https://cdn.bootcss.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet">
复制代码
import React, { Component } from 'react';

export default class App extends Component {

  constructor(props) {
    super(props)

    this.state = {
      count: 0
    }
  }

  handleIncrement = () => {
    this.setState({
      count: this.state.count + 1
    })
  }

  handleDecrement = () => {
    this.setState({
      count: this.state.count - 1
    })
  }

  render() {
    return (
      <div className="container">
        <h1 className="text-center mt-5">{this.state.count}</h1>
        <p className="text-center">
          <button onClick={this.handleIncrement} className="btn btn-primary mr-2">Increase</button>
          <button onClick={this.handleDecrement} className="btn btn-danger my-2">Decrease</button>
        </p>
      </div>
    );
  }
}
复制代码

这个例子非常简单,但是我们应该思考一下,React 是如何来改变这个数字的。

有两个关键步骤,首先,它会从 state 中来读取初始值,然后当有点击事件发生了以后会去调用 setState 方法来改变 state 的值并重新渲染页面。这样就能看到页面上数字可以发生改变的效果了。

那么问题来了,React 中一个组件里面维护数据只需要 state 和 setState 就可以轻松搞定。假如多个组件都需要维护这一份数据怎么办呢?

2、为什么要使用 Redux

了解 React 组件之间如何传递数据的人都应该知道,React 传递数据是一级一级地传的。就像如下的左图,绿色组件要想把某个时候的数据传递给红色的组件那么需要向上回调两次,再向下传一次,非常之麻烦。

而 Redux 是怎么做的呢,Redux 有一个非常核心的部分就是 Store,Store 中管理的数据独立于 React 组件之外,如果 React 某个组件中的某个数据在某个时刻改变了(可以称之为状态改变了),就可以直接更改这个 Store 中管理的数据,这样其他组件想要拿到此时的数据直接拿就行了,不需要传来传去。

需要说明的是,react 中有一个 context 也可以实现类似的功能,但它是一种侵入式写法,官方都不推荐,所以本文都不会提到它。

这个过程看上去挺简单的,但是 Redux 为了做好这样一件事也是要经历一个比较复杂的过程的。

接下来就开启 Redux 之旅吧。

3、如何使用 Redux

安装 redux:

$ npm install --save redux
or
$ yarn add redux
复制代码

首先创建一个 src/Reducer.js

Store 通常要和 Reducer 来配合使用,Store 存数据,Reducer 是个纯函数,它接收并更新数据。

先创建一个 Reducer,为了简单,这里直接将需要的初始值写到 reducer 中的 state 中,state = 0 是给它的一个初始化数据(state 的值可以是一个对象,这里直接给一个数字),它还收接一个 action,当 action 的 type 为 'increment' 时就将 state + 1,反之减一。

虽然这里把初始值写到了 reducer 中,但是真正存储这个 state 的还是 store,reducer 的作用是负责接收、更新并返回新的数据。

同时也这里可以看出,Reducer 怎么更新数据,得看传入的 action 的 type 值了。

export default (state = 0, action) => {
  switch (action.type) {
    case "increment":
      return state + 1;
    case "decrement":
      return state - 1;
    default:
      return state;
  }
};
复制代码

然后创建 src/Store.js

有了 Reducer 之后就可以来创建我们需要的 store 了,这个 store 是全局的,任何一个 react 组件想用它都可以引入进去。

import { createStore } from 'redux';
import Reducer from './Reducer';

const store = createStore(Reducer)

export default store;
复制代码

最后来修改我们的 App.js

import React, { Component } from "react";
+ import store from "./Store";

export default class App extends Component {
+  onIncrement = () => {
+    store.dispatch({
+     type: "increment"
+    });
+  };

+  onDecrement = () => {
+    store.dispatch({
+      type: "decrement"
+    });
+  };

  render() {
+    store.subscribe(() => console.log("Store is changed: " + store.getState()));

    return (
      <div className="container">
+        <h1 className="text-center mt-5">{store.getState()}</h1>
        <p className="text-center">
          <button className="btn btn-primary mr-2" onClick={this.onIncrement}>
            Increase
          </button>
          <button className="btn btn-danger my-2" onClick={this.onDecrement}>
            Decrease
          </button>
        </p>
      </div>
    );
  }
}
复制代码

store.getState() 是用来获取 store 中的 state 值的。 store.subscribe() 方法是用来监听 store 中 state 值的,如果 state 被改变,它就会被触发,所以这个方法接收的是一个函数。subscribe() 方法也可以写到 componentDidMount() 里面。

之前说了,要想改变 store 中 state 的值,就要传入一个 action 的 type 值,redux 规定,这个 action 的值需要由 store 的 dispatch 方法来派发。

所以用 store.dispatch({type: 'increment'}); 这样简单的写法就轻松地给 reducer 传入了想要的值了,这个时候 state 的值就能变化了。

现在就可以验证一下上面的操作了,手动改一下 state 的值,发现页面上的数据也改变了,说明页面上的数据从 store 中成功读取了:

触发点击事件后,可以看到 state 的值成功的被改变了,说明用 store.dispatch() 来派发的这个 action 的 type 是成功的。

那么页面上的数据为什么没有变化呢,这是因为 state 的值虽然被改变了,但是页面并没有重新渲染,之前在用 react 来实现这个功能的时候改变 state 调用了 setState() 方法,这个方法会同时重新渲染 render()。

那么这里其实也是可以借助这个 setState() 方法的

修改 App.js

import React, { Component } from "react";
import store from "./Store";

export default class App extends Component {
+  constructor(props) {
+    super(props);

+    this.state = {
+      count: store.getState()
+    };
+  }

  onIncrement = () => {
    ...
  };

  onDecrement = () => {
    ...
  };

  render() {
+    store.subscribe(() =>
+      this.setState({
+       count: store.getState()
+      })
+    );

    return (
      ...
      );
  }
}
复制代码

这样借助 react 的 setState 方法就可以让 store 中的值改变时也能同时重新渲染页面了。

一个简单的 redux 例子到这里也就完成了。

3.1、抽取 Action

上面的例子中,在 onClick 事件出触发的函数里面用了 store.dispatch() 方法来派发 Action 的 type,这个 Action 其实也可以单独抽取出来

新建 src/Action.js

export const increment = () => {
  return {
      type: "increment"
  };
};

export const decrement = () => {
  return {
      type: "decrement"
  };
};
复制代码

修改 App.js

import React, { Component } from "react";
import store from "./Store";
+import * as Action from './Action'

export default class App extends Component {
  ...

  onIncrement = () => {
+    store.dispatch(Action.increment());
  };

  onDecrement = () => {
+    store.dispatch(Action.decrement());
  };

  render() {
    ...
  }
}
复制代码

这样就把 dispatch 里面的内容单独抽取出来了,Action.js 里面的内容也就代表了用户鼠标进行的的一些动作。

这个 Action.js 是还可以做进一步抽取的,因为 type 的值是个常量,所以可以单独提取出来

新建 ActionTypes.js

export const INCREMENT = 'increment'

export const DECREMENT = 'decrement'
复制代码

然后就可以修改 Actions.js 了

+import * as ActionTypes from './ActionType';

...
复制代码

同样的 Reducer.js 的 type 也可以修改下

+import * as ActionTypes from './ActionType';

export default (state = 0, action) => {
  switch (action.type) {
+    case ActionTypes.INCREMENT:
      return state + 1;
+    case ActionTypes.DECREMENT:
      return state - 1;
    default:
      return state;
  }
};
复制代码

到这里一个包含 Action、Reducer、Store、dispatch、subscribe、view 的完整 redux 例子就实现了,这里的 view 指的是 App.js 所提供的页面也就是 React 组件。

这个时候再来看一眼题图就很非常好理解 Redux 整个工作流程了:

4、如何使用 react-redux

在 react 中使用 redux 其实是还可以更加优雅一点的。redux 还提供了一个 react-redux 插件,需要注意的是这个插件只起到辅助作用并不是用来替代 redux 的。

至于使用了它如何变得更加优雅,这个先从代码开始说起:

安装 react-redux:

$ npm install --save react-redux
or
$ yarn add react-redux
复制代码

每个组件中要用到 store,按之前的方法需要单独引入,这里还可以换一种方式,直接在最顶层组件将它传进去,然后组件想用的时候再接收:

修改 index.js,这是个最顶层组件,将 store 在这里引入并向下传递

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
+import store from "./Store";
+import { Provider } from 'react-redux'

import registerServiceWorker from "./registerServiceWorker";

ReactDOM.render(
+  <Provider store={store}>
    <App />
+  </Provider>,
  document.getElementById("root")
);
registerServiceWorker();
复制代码

修改 App.js,在这里引入用 connect 来接收 store,然后就可以用 this.props 来使用 dispatch 了,state 则用一个函数来接收一下就可以使用了。

import React, { Component } from "react";
import * as Action from "./Action";
+import { connect } from "react-redux";
-import store from "./Store";

+class App extends Component {
-  constructor(props) {
-    super(props);

-    this.state = {
-      count: store.getState()
-    };
-  }

  onIncrement = () => {
+    this.props.dispatch(Action.increment());
  };

  onDecrement = () => {
+    this.props.dispatch(Action.decrement());
  };

  render() {
-    store.subscribe(() =>
-      this.setState({
-        count: store.getState()
-      })
-    );

    return (
      <div className="container">
+        <h1 className="text-center mt-5">{this.props.count}</h1>
        <p className="text-center">
          <button className="btn btn-primary mr-2" onClick={this.onIncrement}>
            Increase
          </button>
          <button className="btn btn-danger my-2" onClick={this.onDecrement}>
            Decrease
          </button>
        </p>
      </div>
    );
  }
}

+const mapStateToProps = state => ({
+  count: state
+});

+export default connect(mapStateToProps)(App);
复制代码

你会意外地发现竟然不需要用 setState 方法来重新渲染页面了,redux 已经帮我们做这件事情了。你可能会对 connect()() 这种写法有疑问,它是一个柯里化函数,底层是怎么实现的本文不讨论,现在只需要知道传入什么参数,怎么用它就可以了。

4.1、处理 Action

其实使用了 react-redux 之后我们不仅不需关心如何去重新渲染页面,还不需要去手动派发 Action,直接在 connect 方法中把 Action 传进去,然后直接调用就行了

在上面的基础上再次修改 App.js:

import React, { Component } from "react";
import * as Action from "./Action";
import { connect } from "react-redux";

class App extends Component {
-   onIncrement = () => {
-     this.props.dispatch(Action.increment());
-   };

-   onDecrement = () => {
-     this.props.dispatch(Action.decrement());
-   };

  render() {
+    const { increment, decrement } = this.props;

    return (
      <div className="container">
        <h1 className="text-center mt-5">{this.props.count}</h1>
        <p className="text-center">
+          <button className="btn btn-primary mr-2" onClick={() => increment()}>
            Increase
          </button>
+          <button className="btn btn-danger my-2" onClick={() => decrement()}>
            Decrease
          </button>
        </p>
      </div>
    );
  }
}

const mapStateToProps = state => ({
  count: state
});

+export default connect(mapStateToProps,Action)(App);
复制代码

到这里你应该就能体会到使用 react-redux 插件的便捷性了,其实它还有其他很多优点,这里不再一一举例。

5、如何使用多个 reducer

redux 中只需要有一个全局的 store,那么如果还需要管理其它状态,可能就需要用到多个 reducer,redux 中提供了一个 combineReducers 可以来将多个 reducer 连接起来。

这里再来演示一下 combineReducers 的用法,为了尽量少修改文件,我这里并没有创建文件夹来分类管理,实际使用过程中不同作用的文件应该放到不同的文件夹中。

创建文件 src/Reducer2.js

export default (state = "hello", action) => {
  switch (action.type) {
    default:
      return state;
  }
};
复制代码

创建 src/CombineReducer.js

import { combineReducers } from 'redux';

import count from './Reducer';

import hello from './Reducer2';

const rootReducer = combineReducers({
    count,
    hello
})

export default rootReducer;
复制代码

修改 Store.js

import { createStore } from 'redux';
+import rootReducer from './CombineReducer';

+const store = createStore(rootReducer)

export default store;
复制代码

修改 App.js

    ...
    return (
      <div className="container">
+        <h1 className="text-center mt-5">{this.props.text}{this.props.count}</h1>
        ...
      </div>
    );
  }
}

const mapStateToProps = state => ({
  count: state.count,
+  text: state.hello
});
...
复制代码

效果如下,可以在 store 中读取到相应的值:

最后,完整的代码在这里:github.com/bgrc/react-…

关注下面的标签,发现更多相似文章
评论