在 React 中使用 Redux

xnng發表於2018-08-16

在 React 中使用 Redux

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

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

1、React 實現計數器

在 React 中使用 Redux

上面這個例子用 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 也可以實現類似的功能,但它是一種侵入式寫法,官方都不推薦,所以本文都不會提到它。

在 React 中使用 Redux

這個過程看上去挺簡單的,但是 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 中成功讀取了:

在 React 中使用 Redux

觸發點選事件後,可以看到 state 的值成功的被改變了,說明用 store.dispatch() 來派發的這個 action 的 type 是成功的。

在 React 中使用 Redux

那麼頁面上的資料為什麼沒有變化呢,這是因為 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 整個工作流程了:

在 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 中讀取到相應的值:

在 React 中使用 Redux

最後,完整的程式碼在這裡:github.com/bgrc/react-…

相關文章