iT邦幫忙

2017 iT 邦幫忙鐵人賽
DAY 26
0
Modern Web

React - DOM界的彼方系列 第 26

Day 26: Redux篇 - 第一次使用Redux於React應用

intro

今天的主題是Redux使用於React元件之中,這個範例並不是一個真正用於React元件的範例,正確的來說,它只是個為了展示Redux原本的功能,如何整合到像React這樣的函式庫,最初步的一種測試範例,我們要學習的是前一個範例到這個範例的整個整合過程。

這個程式最後的呈現結果,就像下面的動態圖片這樣,重點是在於下面有個Redux DevTools,它有時光旅行除錯的功能,可以倒帶重播你作過的任何資料上的變動:

Redux範例一展示

註: 本文章同步放置於Github庫的這裡,所有的程式碼也在裡面。

程式碼說明

一樣是使用ES6的樣版文件webpack-es6-startkit,要先安裝完redux後,額外再安裝react與react-dom套件,指令如下:

npm i --save react react-dom

因為原本的樣版文件中的babel並沒有附可以編譯react語法的套件,所以也要額外再安裝,指令如下:

npm i babel-preset-react --save-dev

.babelrc檔案也要加入這個preset,像下面這樣的修改:

{
  "presets": [
    "latest",
    "stage-0",
    "react"
  ],
  "plugins": [
    "transform-flow-strip-types"
  ]
}

我們的inde.html更佳的簡單,這是<body>中的程式碼,它就像一般的React應用一樣,只會有一個要讓React應用渲染用的div,程式碼如下:

index.html(body標記內部份)

<h2>Redux Example 2: React with no react-redux</h2>

<div id="root"></div>

<script src="./build/bundle.js"></script>

第一步,仍然從redux中匯入createStore方法,因為用了react與react-dom套件,也一併在這裡匯入:

import { createStore } from 'redux'

import React from 'react'
import ReactDOM from 'react-dom'

第二步,是reducer(歸納函式),這裡我們加了一個用於刪除項目用的處理的程式碼部份,reducer的名稱改為dealItem,在新增項目(ADD_ITEM)時,用payload物件記載{id: id, text: text}傳入,而刪除時只需要id就行了。

刪除項目的語法是使用filter方法,你也可以使用純粹函式的delete項目的寫法,就像之前的TodoApp裡的例子一樣的寫法也行。回傳一個不包含傳入id值項目的陣列,相當於刪除這個id值的項目就是。程式碼如下:

註: 因為項目的id值我們要用+new Date()來產生,這不能使用在reducer裡,所以在元件裡加入的方法中產生然後傳入reducer。

// @Reducer
//
// Add Item: action payload = action.payload
// Del Item: action payload = action.id
// 使用純粹函式的陣列unshift,不能有副作用
// state(狀態)一開始的值是空陣列`state=[]`
function dealItem(state = [], action) {
  switch (action.type) {
    case 'ADD_ITEM':
      {
        return [{
          id: action.payload.id,
          text: action.payload.text,
        }, ...state]
      }

    case 'DEL_ITEM':
      {
        return state.filter(item => item.id !== action.id)
      }

    default:
      return state
  }
}

第三步,是由寫好的reducer,建立store,與之前的都一樣:

// @Store
//
// store = createStore(reducer)
// 使用redux dev tools
// 如果要正常使用是使用 const store = createStore(dealItem)
const store = createStore(dealItem,
   window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__())

第四步,因為我們要使用React,所以要寫一個React應用,當作像之前的render函式之用,寫法與之前的TodoList一樣,程式碼如下:

// React元件 - MyComponent
class MyComponent extends React.Component {
  // flow工具檢查用
  state: { inputValue: ?string }

  constructor(props) {
    super(props)

    // 在文字輸入時使用state
    this.state = { inputValue: '' }
  }


  handleChange = (event: KeyboardEvent) => {
    // flowtype檢查用的
    if (event.target instanceof HTMLInputElement) {
      this.setState({
        inputValue: event.target.value,
      })
    }
  }

  handleClick = () => {
    // store中的狀態值發送要新增的action
    store.dispatch({
      type: 'ADD_ITEM',
      payload: {
        id: +new Date(),
        text: this.state.inputValue,
      },
    })

    // 清空文字框中的文字
    this.setState({
      inputValue: '',
    })
  }

  render() {
    // 測試用
    // console.log(store.getState())
    return (
      <div>
        <div>
          <input
            type="text"
            value={this.state.inputValue}
            onChange={this.handleChange}
          />
          <button
            onClick={this.handleClick}
          >
            Add Item
          </button>
        </div>
        <p>
        {
          store.getState().map(item => (
            <li key={item.id}>
              <input type="checkbox" id={item.id} onClick={() => store.dispatch({ type: 'DEL_ITEM', id: item.id })} />
              {item.text}
            </li>
          ))
        }
        </p>
      </div>
    )
  }
}

// @Render
//
const render = () => ReactDOM.render(
  <MyComponent />, document.getElementById('root'))

這邊要注意的有兩個地方,一個是在按下按鈕時的handleClick方法中,我們要呼叫對應的store.dispatch(action),在刪除單個項目用的勾選盒(checkbox)的onClick事件方法裡,也要呼叫對應的store.dispatch(action),當然項目的資料是從store.getState()得來的。另一個則是把ReactDOM.render方法包在一個函式裡,稱之為render函式。

第五步,第一次呼叫一下render,和上個範例一樣的程式碼:

// 第一次要呼叫一次render,讓網頁呈現資料
render()

第六步,訂閱render函式到store中,和上個範例一樣的程式碼:

// 訂閱render到store,這會讓store中如果有新的state(狀態)時,會重新呼叫一次render()
store.subscribe(render)

第七步,觸發事件的時候要呼叫store.dispatch(action)。不用寫了,因為已經寫在React應用中了。

像上面這樣子,就把React的應用接到Redux上了。

不過,這個應用有一些很明顯的問題。首先,Redux並沒有掌控到整個React應用裡的state(狀態),反而像是各自為政,Redux裡的store所記錄的狀態值,雖然的確是所有項目的資料,但React元件裡依然有另一個state值,這個state值還是有它的作用在,假設說這個是一個複雜的應用的情況下,有很多層的元件在React裡面,Redux對於React中的狀態變化會變得一無所知,只有靠有呼叫到store.dispatch(action)時,Redux才知道原來要進行更動自己所管控的store裡面的狀態資料。

另外,把React的這個ReactDOM.render作為render函式,然後訂閱到store也是一個很有問題的方式。每次store有更動,就會呼叫ReactDOM.render來作重新渲染的動作,雖然看起來似乎與在React應用內部,呼叫setState然後觸發重新渲染的結果一樣,但實際上這個方式並不是好的方式。在這篇Stackoverflow上的問答,Redux的作者的答覆是這樣的說的:

當你只有很少的元件時沒什麼太大差異,但在應用變大(有很多元件)時,這樣子會導致變慢(效率變差)...所以結論是我們不建議你直接用store.subscribe(),有個完整的函式庫叫React Redux,它可以協助你作訂閱(subscribe)的動作,當你使用裡面的connect()後,它會包裝setState()的邏輯進來...而且React Redux也會比你自己寫更有效率,因為它包含許多的最佳化程式碼。

好的,其實下一章的內容就是要使用React Redux,它是Redux開發團隊專門為React打造的綁定函式庫(bindings),說到後來,Redux也是因應React而開發出來的,怎麼能不完美的整合在一起呢。


其他的補充說明

單向資料流

Redux的資料流設計的本質是一個嚴格的單向資料流(strict unidirectional data flow),依照官網的說明文件,它的資料流剛好是我們撰寫流程的倒過來看,資料的流動如下步驟:

1. 事件觸發,呼叫store.dispatch(action)

在事件觸發時,或是像Ajax、計時器之類的API中,你呼叫了store.dispatch(action)方法,這會發送一個用於描述動作的actionaction是一個單純的物件值。例如:

{ type: 'ADD_TODO', text: 'Read the Redux docs.' }

因為action只能是單純的物件值,原先的Flux架構中,另外設計了Action Creator(動作建立器),它的定義如下(出自這裡的文件):

Action Creators(動作建立器)是一種輔助方法,收集整理到一個函式庫中,這個函式庫會從方法的傳入參數建立一個action,指定它一個type(類型),以及提供它給dispatcher(發送器)

下面這是原先的(傳統的)Flux中,Action Creator(動作建立器)的一個範例:

function addTodoWithDispatch(text) {
  const action = {
    type: ADD_TODO,
    text
  }
  dispatch(action)
}

Redux簡化了Flux架構,在Redux中的Action Creator(動作建立器)不需要作發送(dispatch)的工作,動作是直接用store.dispatch()來作。當然Redux與Flux架構有一些本質設計上的不同。也因為如此,我們在本章的例子與上一個例子中,都沒有看到使用Action Creator(動作建立器)的程式碼,實際上如果寫出來也是很簡單的,像下面這樣:

export function addTodo(text) {
  return { type: ADD_TODO, text }
}

當要發送時就改用像store.dispatch(addTodo(text))這樣的語句來取代。

2. Redux的store會呼叫你給的reducer函式

Redux的reducer(歸納器)函式,是一個唯一可以更動store中的所記錄的資料,也就是應用程式狀態(state)的方式。這個設計的概念與React中的setState非常相似,還記得我們上一章說的,reducer的的公式是像下面這樣(出自這裡的文件):

(previousState, action) => newState

如果你有仔細看過React中的setState的API文件說明,它也有個說明的公式如下(出自這裡的文件):

function(state, props) => newState

只不過差在setState是以props作為目前狀態(state)的的歸納傳入參數,而Redux的reducer(歸納器)是以action作為傳入參數。

React的setState會這樣設計,是因為它需要比對目前的state與新加入的狀態值,才能經過diffing演算法,計算出真實的DOM元素中真正要進行更動的元素,用最有效率的方式來作重新渲染。

Redux的reducer(歸納器)則是為了建構出store與處理store中的記錄的狀態如何被更動,reducer(歸納器)的要求是完全的純粹函式與無副作用,幾乎是不能在裡面呼叫任何的外部API,例如使用Math.random()或Date.now()方法等等。它只作單純一件事,如果有action傳入時,store中的記錄狀態值該如何更動,而這些動作是在使用了dispatch(發送)方法時才作的。

3. 根reducer可以合併多個reducers為單一個狀態(state)樹

Redux另外提供了combineReducers()方法,可以合成多個reducer(歸納器)來使用,在小型的應用可能用不到,但在複雜的應用中,我們可能會把負責不同狀態值的reducers分割開來,例如專門處理記錄目前應用程式設定值的,與專門處理記錄應用程式中的項目值的。

4. Redux的store會儲存由根reducer回傳的整個狀態(state)樹

store.subscribe(listener)方法,是在有狀態更動時,會自動被呼叫,在我們的範例中,是用rener函式來作為監聽者。監聽者可以由store.getState()方法取到目前已更動過的狀態資料,以此來呈現出對應的輸出結果。


上一篇
Day 25: Redux篇 - 介紹 與 第一個範例
下一篇
Day 27: Redux篇 - 使用react-redux綁定Redux與React
系列文
React - DOM界的彼方30

尚未有邦友留言

立即登入留言