Redux
predictable state container
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 的缺點
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
預設是這樣:
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 優點
而且他的 API 很少,核心只有 createStore 的 150 行程式碼
容易學、好擴充
開發工具
Thanks for listening