在 React 中使用 Redux

這是一篇介紹 redux 的入門文章,欲知更多內容請查閱 官方文檔css

本文會經過三種方式實現一個簡單到不能呼吸的計數器小例子,先用 React 實現,再慢慢引入 Redux 的內容,來了解什麼是 Redux、爲何要使用 Redux 以及如何簡單地使用 Redux。html

一、React 實現計數器

上面這個例子用 React 實現起來很是簡單,初始化一個 creact-react-app,而後爲了頁面看起來更美觀,在 index.html 加入 bootstrap 的 CDN,並修改 App.js 爲以下內容就能夠了。react

<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 是如何來改變這個數字的。git

有兩個關鍵步驟,首先,它會從 state 中來讀取初始值,而後當有點擊事件發生了之後會去調用 setState 方法來改變 state 的值並從新渲染頁面。這樣就能看到頁面上數字能夠發生改變的效果了。github

那麼問題來了,React 中一個組件裏面維護數據只須要 state 和 setState 就能夠輕鬆搞定。假如多個組件都須要維護這一份數據怎麼辦呢?npm

二、爲何要使用 Redux

瞭解 React 組件之間如何傳遞數據的人都應該知道,React 傳遞數據是一級一級地傳的。就像以下的左圖,綠色組件要想把某個時候的數據傳遞給紅色的組件那麼須要向上回調兩次,再向下傳一次,很是之麻煩。redux

而 Redux 是怎麼作的呢,Redux 有一個很是核心的部分就是 Store,Store 中管理的數據獨立於 React 組件以外,若是 React 某個組件中的某個數據在某個時刻改變了(能夠稱之爲狀態改變了),就能夠直接更改這個 Store 中管理的數據,這樣其餘組件想要拿到此時的數據直接拿就好了,不須要傳來傳去。bootstrap

須要說明的是,react 中有一個 context 也能夠實現相似的功能,但它是一種侵入式寫法,官方都不推薦,因此本文都不會提到它。app

這個過程看上去挺簡單的,可是 Redux 爲了作好這樣一件事也是要經歷一個比較複雜的過程的。dom

接下來就開啓 Redux 之旅吧。

三、如何使用 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.一、抽取 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 整個工做流程了:

四、如何使用 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.一、處理 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 插件的便捷性了,其實它還有其餘不少優勢,這裏再也不一一舉例。

五、如何使用多個 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-…

相關文章
相關標籤/搜索