Redux

predictable state container

C. T. Lin

MVC 有什麼問題?

幾年前開始的 Client Side MVC 大戰

UI move fast

MVC doesn't scale

複雜度超過想像

the Facebook codebase has over 15,000 React components

- 2015/10/07

詳細的可以參考這個影片:
Rethinking Web App Development at Facebook

Managing complexity is the most important technical topic in software development. In my view, it’s so important that Software’s Primary Technical Imperative has to be managing complexity.

軟體首要技術使命是管理複雜度。

– Steve McConnell in Code Complete (most-influential-book-every-programmer-should-read)

處理前端複雜架構的訣竅在於

提高可預測性

這就是 React 的目標

Flux

Unidirectional data flow

Flux 運作方式

Classical Flux 的缺點


  • State Mutation
  • Impure Reducer
  • Singleton Issue
  • Boilerplate

Redux

2015/5/30 First commit

原本只是用來準備一個 React EU Conf 的 講題
Live React: Hot Reloading with Time Travel

意外做出最受歡迎的 Flux 架構

所以作者稱為 CDD (Conference Driven Development)

Redux 三大原則

唯一真相來源
(Single source of truth)

State 是唯讀的

變更被寫成 pure functions
(Changes are made with pure functions)

詳細可參考這裡

Action

只是個普通的 JS Object

一定要有一個 type

{
  type: SEND_MESSAGE,
  payload: {
    text: 'Hello world'
  }
}

把資料傳給 Store 的唯一手段

表達修改 State 的意圖

Flux Standard Action

Flux Standard Action 有一個普遍的 action 定義

包括 meta, error 等欄位

type 可以宣告為常數

放在 ActionCreator 或 額外的 actionTypes 檔案

export const ADD_TODO = 'ADD_TODO';
export const COMPLETE_TODO = 'COMPLETE_TODO';
export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER';

詳細可參考這裡

Action Creators

通常寫在 XxxxActions 或 XxxxActionCreators 命名的檔案中

小心盡量不要跟 Action 搞混

一個傳統 Flux 的 Action Creator

會製造 Action 並 dispatch

function sendMessage(text) {
  const action = {
    type: SEND_MESSAGE,
    payload: {
      text
    }
  };
  dispatch(action); // 注意這裡,這裡就不是 Pure 的
}

Redux 中一個普通的 Action Creator

就是製造 Action 的 Pure Function

function sendMessage(text) {
  return {
    type: SEND_MESSAGE,
    payload: {
      text
    }
  };
}

這樣的 Pure Function 很好測試

expect(sendMessage('Hello')).to.deep.equal({
    type: SEND_MESSAGE,
    payload: {
      text: 'Hello'
    }
});

實際 dispatch Action

// 先不管 dispatch 從哪來
dispatch(sendMessage('Hello'));

使用 Middleware

可以 dispatch 除了一般 action 以外的東西

Thunk Middleware (redux-thunk)

Promise Middleware (redux-promise)

Saga Middleware (redux-promise)

這邊先不做敘述

Reducer

給 Store 初始的 State

並根據收到的 Action 回傳新的 State

就像是傳統 Flux 的 Store 的一部分工作

(previousState, action) => newState

// 原理類似這樣
const newState = reducer(previousState, action);

每次收到 dispatch 的 Action 就會觸發

根據原本的 previousState 和 Action,回傳新的 newState

這樣的 Functional 的做法

可以更簡潔的表達如何變更 State

這是個沒有 Side Effect 的 Function

不要去改 previousState

會使 Time Travel 等需要前面 State 的功能壞掉

const initialState = { message: '' };

function messageReducer(state = initialState, action) {
  switch (action.type) {
    case SEND_MESSAGE:
      return {
        message: action.payload.text
      };
    default:
      return state; // 沒有做任何事也要回傳原本的
  }
}

這樣的 Pure Function 很好測試

// Reducer 了解的 Action
expect(messageReducer(null, {
  type: SEND_MESSAGE,
  payload: {
    text: 'Hello'
  }
})).to.deep.equal({ message: 'Hello' });

// Reducer 不了解的 Action
expect(messageReducer({ message: 'Hello' }, {
  type: UNKNOWN,
  payload: {
    text: 'Hello'
  }
})).to.deep.equal({ message: 'Hello' });

在一開始的時候 Redux 會 dispatch

@@redux/INIT Action 來初始化 State

@@namespace/EVENT_NAME

是目前 Private Event 的命名規範

不要試圖去聽這些 Event

詳細可參考這裡

Store

應用程式保存 State 的地方,Redux 只有一個

建立 Store 的方式是把 reducer 傳進去 createStore

import { createStore } from 'redux';
import messageReducer from '../reducers/messages';
const store = createStore(messageReducer);

實際會有很多不同資料的 reducer

所以需要用 combineReducers 來結合他們

// reducers/index.js
import { combineReducers } from 'redux';
import todos from './todos';
import counter from './counter';

export default combineReducers({
  todos,
  counter
});

// App.js
import { createStore } from 'redux';
import reducer from './reducers/index';

const store = createStore(reducer);
// 第二個參數可用來當作初始的 state
const store = createStore(todoApp, window.STATE_FROM_SERVER);

dispatch

dispatch 是 Store 比較 Low-level 的 API

不過還是值得一提

雖然通常並不會直接這樣寫

store.dispatch(sendMessage('Hello'));

getState

可以取得 store 現在的 state

store.getState();

subscribe

可以監聽 store state 的改變

// 每次 state 變更,就印出它
const unsubscribe = store.subscribe(() =>
  console.log(store.getState())
)

詳細可參考這裡

Middleware

用來延伸 dispatch 的功能

使用 applyMiddleware util

import { createStore, applyMiddleware } from 'redux';

const store = createStore(
  reducer,
  applyMiddleware(promise, thunk, observable)
);
// applyMiddleware 可以 decorate createStore

dispatch => dispatch'

redux-thunk 這個 Middleware

讓 Store 可以 dispatch Thunk

function sendMessageAsync(message) {
  return dispatch => {
    setTimeout(() => {
      dispatch(sendMessage(message));
    }, 1000);
  };
}

redux-promise 這個 middleware

讓 Store 可以 dispatch Promise

function sendMessageAsync(message) {
  return dispatch => {
      dispatch(new Promise((resolve, reject)) => {
        setTimeout(() => {
            resolve(sendMessage(message))
        }, 1000);
      });
  };
}

非同步的地方讓 ActionCreator 去做

Reducer 是完全同步的

View

Redux 本身並不依賴特定 View Layer

所以可以跟任何 View Layer 去結合

react-redux 就是 react 跟 redux 的介接

提供 Provider 和 connect 兩個東西

Provider

Provider 用來在 RootComponent 外再包一層

並把 Store 用 React Context 傳下去

// 包在 Provider 才能 connect Store
<Provider store={store}>
  <MyRootComponent>
</Provider>

connect

把特定的 State 從 Context 裡的 Store Select 出來

並把它們當 props 傳下去

class MyComponent extends React.Component {
  render() {
    const { dispatch, user } = this.props;
    // connect 沒給第二個參數時 dispatch 會預設當 props 傳下來
    // ..
  }
}

export default connect(state => ({
  user: state.user
}))(MyComponent);

直接包裝 import 進來的 Component

import PresentationalComponent from '../components/PresentationalComponent';

// 從 State 抽取要傳下去的 Props
function mapStateToProps(state) {
  return {
    user: state.user
  };
}

export default connect(mapStateToProps)(PresentationalComponent);

傳遞 ActionCreator 下去

function mapStateToProps(state) {
  return { user: state.user };
}

// 把 dispatch 變成 Handler 當成 Props 傳下去
function mapDispatchToProps(dispatch) {
  return {
    sendMessage: (msg) => { dispatch(sendMessage(msg)) }
  };
}

class MyComponent extends React.Component {
  render() {
    const { user, sendMessage } = this.props;
    // sendMessage 也會當 props 傳下來
    // ..
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(MyComponent);

bindActionCreators

Redux 還有這個可以方便使用 ActionCreator 的 util

import { bindActionCreators } from 'redux';
import MessageActions from '../actions/MessageActions';

function mapStateToProps(state) {
  return { user: state.user };
}

// 把 dispatch 變成 Handler 當成 Props 傳下去
function mapDispatchToProps(dispatch) {
  return bindActionCreators(MessageActions, dispatch);
} // 把整個 ActionCreator 的 Handler 傳下去

class MyComponent extends React.Component {
  render() {
    const { user, sendMessage, deleteMessage } = this.props;
    // 把整個 ActionCreator 的 Handler 都當 props 傳下來
    // ..
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(MyComponent);

connect 其實不只這樣...

connect 有三個可選參數

mapStateToProps, mapDispatchToProps, mergeProps

  • mapStateToProps: 跟之前一樣,把 Store State 的特定部分當成 Props
  • mapDispatchToProps: 把 dispatch 傳進去也變成 Props,通常用來把綁定的 actionCreators 傳下去
  • mergeProps: 可以指定 stateProps, dispatchProps, parentProps 的合併方式

預設是這樣:

const defaultMergeProps = (stateProps, dispatchProps, parentProps) => ({
  ...parentProps,
  ...stateProps,
  ...dispatchProps
});

最後可以改變傳下去的 Props 的機會

function mergeProps(stateProps, dispatchProps, parentProps) {
  return {
    ...stateProps, // 這樣就可以讓 parentProps 蓋掉其他兩個
    ...dispatchProps,
    ...parentProps
  }
}

export default connect(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps
)(MyDumbComponent)

Redux 優點

  1. 所有東西都 Hot Reloadable
  2. 容易做 Universal,沒用到 Singleton 而且資料可以 rehydrated
  3. 可以用任何形式保存資料:JS Objects, Arrays, ImmutableJS..
  4. 簡化至一個 Store
  5. 提供 redux-devtools Time travel 功能
  6. 可以用 Middleware 擴充 dispatch
  7. 不用 Mock 就很容易測試

而且他的 API 很少,核心只有 createStore 的 150 行程式碼

容易學、好擴充

開發工具

  • redux-devtools
  • redux-logger

THE END

Thanks for listening