Redux 很大部分 受到 Flux 的啟發(fā),并且最常見的關(guān)于 Flux 抱怨是它如何使得你寫了一大堆的模板。在這個技巧中,我們將考慮 Redux 如何使得我們選擇我們的代碼會變得怎樣繁復(fù),取決于個人樣式,團隊選項,長期可維護等等。
Actions 是描述了在 app 中所發(fā)生的,以單獨方式描述對象變異意圖的服務(wù)的一個普通對象。很重要的一點是 你必須分發(fā)的 action 對象并不是一個模板,而是 Redux 的一個基本設(shè)計選項.
有些框架生成自己和 Flux 很像,不過缺少了 action 對象的概念。為了變得可預(yù)測,這是一個從 Flux or Redux 的倒退。如果沒有可串行的普通對象 action,便無法記錄或重放用戶會話,或者無法實現(xiàn) 帶有時間旅行的熱重載。如果你更喜歡直接修改數(shù)據(jù),那么你并不需要 Redux 。
Action 一般長這樣:
{ type: 'ADD_TODO', text: 'Use Redux' }
{ type: 'REMOVE_TODO', id: 42 }
{ type: 'LOAD_ARTICLE', response: { ... } }
一個約定俗成的是 actions 擁有一個定值 type 幫助 reducer (或 Flux 中的 Stores ) 識別它們。我們建議的你使用 string 而不是 Symbols 作為 action type ,因為 string 是可串行的,而使用 Symbols 的話你會把記錄和重演變得比所需要的更難。
在 Flux 中,傳統(tǒng)上認為你將每個 action type 定義為string定值:
const ADD_TODO = 'ADD_TODO';
const REMOVE_TODO = 'REMOVE_TODO';
const LOAD_ARTICLE = 'LOAD_ARTICLE';
這么做的優(yōu)勢?人們通常聲稱定值不是必要的,對于小的項目可能是正確的。 對于大的項目,將action types定義為定值有如下好處:
undefined
。當你納悶 action 被分發(fā)出去而什么也沒發(fā)生的時候,一個拼寫錯誤更容易被發(fā)現(xiàn)。你的項目的約定取決與你自己。你開始的時候可能用的是inline string,之后轉(zhuǎn)為定值,也許之后將他們歸為一個獨立文件。Redux 不會給予任何建議,選擇你自己最喜歡的。
另一個約定是,你創(chuàng)建生成 action 對象的函數(shù),而不是在你分發(fā)的時候內(nèi)聯(lián)生成它們。
例如,用文字對象取代調(diào)用 dispatch
:
// somewhere in an event handler
dispatch({
type: 'ADD_TODO',
text: 'Use Redux'
});
你可以在單獨的文件中寫一個 action creator ,然后從 component 里導(dǎo)入:
actionCreators.js
export function addTodo(text) {
return {
type: 'ADD_TODO',
text
};
}
AddTodo.js
import { addTodo } from './actionCreators';
// event handler 里的某處
dispatch(addTodo('Use Redux'))
Action creators 總被當作模板受到批評。好吧,其實你并不用把他們寫出來!如果你覺得更適合你的項目你可以選用對象文字 然而,你應(yīng)該知道寫 action creators 是存在某種優(yōu)勢的。
假設(shè)有個設(shè)計師看完我們的原型之后回來說,我們需要允許三個 todo 不能再多了。我們可以使用 redux-thunk 中間件添加一個提前退出,把我們的 action creator 重寫成回調(diào)形式:
function addTodoWithoutCheck(text) {
return {
type: 'ADD_TODO',
text
};
}
export function addTodo(text) {
// Redux Thunk 中間件允許這種形式
// 在下面的 “異步 Action Creators” 段落中有寫
return function (dispatch, getState) {
if (getState().todos.length === 3) {
// 提前退出
return;
}
dispatch(addTodoWithoutCheck(text));
}
}
我們剛修改了 addTodo
action creator 的行為,對調(diào)用它的代碼完全不可見。我們不用擔心去看每個添加 todo 的地方保證他們有了這個檢查 Action creator 讓你可以解耦額外的分發(fā) action 邏輯與實際的 components 發(fā)送這些 actions,而且當你在重開發(fā)經(jīng)常要改變需求的時候也會非常有用。
某些框架如 Flummox 自動從 action creator 函數(shù)定義生成 action type 定值。這個想法是說你不需要 ADD_TODO
定值和 addTodo()
action creator兩個都自己定義。這樣的方法在底層也生成 action type 定值,但他們是隱式生成的,也就是間接級。
我們不建議用這樣的方法。如果你寫像這樣簡單的 action creator 寫煩了:
export function addTodo(text) {
return {
type: 'ADD_TODO',
text
};
}
export function removeTodo(id) {
return {
type: 'REMOVE_TODO',
id
};
}
你可以寫一個生成 action creator 的函數(shù):
function makeActionCreator(type, ...argNames) {
return function(...args) {
let action = { type };
argNames.forEach((arg, index) => {
action[argNames[index]] = args[index];
});
return action;
}
}
export const addTodo = makeActionCreator('ADD_TODO', 'todo');
export const removeTodo = makeActionCreator('REMOVE_TODO', 'id');
參見 redux-action-utils 和 redux-actions 獲得更多介紹這樣的常用工具。
注意這樣的工具給你的代碼添加了魔法。魔法和間接聲明真的值得多寫一兩行代碼么?
中間件 讓你注入一個定制邏輯,可以在每個 action 對象分發(fā)出去之前解釋。異步 actions 是中間件的最常見用例。
沒有中間件的話,dispatch
只能接收一個普通對象。所以我們在 components 里面進行 AJAX 調(diào)用:
actionCreators.js
export function loadPostsSuccess(userId, response) {
return {
type: 'LOAD_POSTS_SUCCESS',
userId,
response
};
}
export function loadPostsFailure(userId, error) {
return {
type: 'LOAD_POSTS_FAILURE',
userId,
error
};
}
export function loadPostsRequest(userId) {
return {
type: 'LOAD_POSTS_REQUEST',
userId
};
}
UserInfo.js
import { Component } from 'react';
import { connect } from 'react-redux';
import { loadPostsRequest, loadPostsSuccess, loadPostsFailure } from './actionCreators';
class Posts extends Component {
loadData(userId) {
// 調(diào)用 React Redux `connect()` 注入 props :
let { dispatch, posts } = this.props;
if (posts[userId]) {
// 這里是被緩存的數(shù)據(jù)!啥也不做。
return;
}
// Reducer 可以通過設(shè)置 `isFetching` 反應(yīng)這個 action
// 因此讓我們顯示一個 Spinner 控件。
dispatch(loadPostsRequest(userId));
// Reducer 可以通過填寫 `users` 反應(yīng)這些 actions
fetch(`http://myapi.com/users/${userId}/posts`).then(
response => dispatch(loadPostsSuccess(userId, response)),
error => dispatch(loadPostsFailure(userId, error))
);
}
componentDidMount() {
this.loadData(this.props.userId);
}
componentWillReceiveProps(nextProps) {
if (nextProps.userId !== this.props.userId) {
this.loadData(nextProps.userId);
}
}
render() {
if (this.props.isLoading) {
return <p>Loading...</p>;
}
let posts = this.props.posts.map(post =>
<Post post={post} key={post.id} />
);
return <div>{posts}</div>;
}
}
export default connect(state => ({
posts: state.posts
}))(Posts);
然而,不久就需要再來一遍,因為不同的 components 從同樣的 API 端點請求數(shù)據(jù)。而且,我們想要在多個components 中重用一些邏輯(比如,當緩存數(shù)據(jù)有效的時候提前退出)。
中間件讓我們寫的更清楚M的潛在的異步 action creators. 它使得我們分發(fā)普通對象之外的東西,并且解釋它們的值。比如,中間件能 “捕捉” 到已經(jīng)分發(fā)的 Promises 并把他們變?yōu)橐粚φ埱蠛统晒?失敗 actions.
最簡單的中間件例子是 redux-thunk. “Thunk” 中間件讓你把 action creators 寫成 “thunks”,也就是返回函數(shù)的函數(shù)。 這使得控制被反轉(zhuǎn)了: 你會像一個參數(shù)一樣取得 dispatch
,所以你也能寫一個多次分發(fā)的 action creator 。
注意
Thunk 只是中間件的一個例子。中間件不是關(guān)于 “讓你分發(fā)函數(shù)” 的:它是關(guān)于讓你分發(fā)你用的特定中間件知道如何處理的任何東西的。Thunk 中間件添加了一個特定的行為用來分發(fā)函數(shù),但這實際上取決于你用的中間件。
考慮上面的代碼用 redux-thunk 重寫:
actionCreators.js
export function loadPosts(userId) {
// 用 thunk 中間件解釋:
return function (dispatch, getState) {
let { posts } = getState();
if (posts[userId]) {
// 這里是數(shù)據(jù)緩存!啥也不做。
return;
}
dispatch({
type: 'LOAD_POSTS_REQUEST',
userId
});
// 異步分發(fā)原味 actions
fetch(`http://myapi.com/users/${userId}/posts`).then(
response => dispatch({
type: 'LOAD_POSTS_SUCCESS',
userId,
respone
}),
error => dispatch({
type: 'LOAD_POSTS_FAILURE',
userId,
error
})
);
}
}
UserInfo.js
import { Component } from 'react';
import { connect } from 'react-redux';
import { loadPosts } from './actionCreators';
class Posts extends Component {
componentDidMount() {
this.props.dispatch(loadPosts(this.props.userId));
}
componentWillReceiveProps(nextProps) {
if (nextProps.userId !== this.props.userId) {
this.props.dispatch(loadPosts(nextProps.userId));
}
}
render() {
if (this.props.isLoading) {
return <p>Loading...</p>;
}
let posts = this.props.posts.map(post =>
<Post post={post} key={post.id} />
);
return <div>{posts}</div>;
}
}
export default connect(state => ({
posts: state.posts
}))(Posts);
這樣打得字少多了!如果你喜歡,你還是可以保留 “原味” action creators 比如從一個 “聰明的” loadPosts
action creator 里用到的 loadPostsSuccess
。
最后,你可以重寫中間件 你可以把上面的模式泛化,然后代之以這樣的異步 action creators :
export function loadPosts(userId) {
return {
// 要在之前和之后發(fā)送的 action types
types: ['LOAD_POSTS_REQUEST', 'LOAD_POSTS_SUCCESS', 'LOAD_POSTS_FAILURE'],
// 檢查緩存 (可選):
shouldCallAPI: (state) => !state.users[userId],
// 進行?。? callAPI: () => fetch(`http://myapi.com/users/${userId}/posts`),
// 在 actions 的開始和結(jié)束注入的參數(shù)
payload: { userId }
};
}
解釋這個 actions 的中間件可以像這樣:
function callAPIMiddleware({ dispatch, getState }) {
return function (next) {
return function (action) {
const {
types,
callAPI,
shouldCallAPI = () => true,
payload = {}
} = action;
if (!types) {
// 普通 action:傳走
return next(action);
}
if (
!Array.isArray(types) ||
types.length !== 3 ||
!types.every(type => typeof type === 'string')
) {
throw new Error('Expected an array of three string types.');
}
if (typeof callAPI !== 'function') {
throw new Error('Expected fetch to be a function.');
}
if (!shouldCallAPI(getState())) {
return;
}
const [requestType, successType, failureType] = types;
dispatch(Object.assign({}, payload, {
type: requestType
}));
return callAPI().then(
response => dispatch(Object.assign({}, payload, {
response: response,
type: successType
})),
error => dispatch(Object.assign({}, payload, {
error: error,
type: failureType
}))
);
};
};
}
在傳給 applyMiddleware(...middlewares)
一次以后,你能用相同方式寫你的 API-調(diào)用 action creators :
export function loadPosts(userId) {
return {
types: ['LOAD_POSTS_REQUEST', 'LOAD_POSTS_SUCCESS', 'LOAD_POSTS_FAILURE'],
shouldCallAPI: (state) => !state.users[userId],
callAPI: () => fetch(`http://myapi.com/users/${userId}/posts`),
payload: { userId }
};
}
export function loadComments(postId) {
return {
types: ['LOAD_COMMENTS_REQUEST', 'LOAD_COMMENTS_SUCCESS', 'LOAD_COMMENTS_FAILURE'],
shouldCallAPI: (state) => !state.posts[postId],
callAPI: () => fetch(`http://myapi.com/posts/${postId}/comments`),
payload: { postId }
};
}
export function addComment(postId, message) {
return {
types: ['ADD_COMMENT_REQUEST', 'ADD_COMMENT_SUCCESS', 'ADD_COMMENT_FAILURE'],
callAPI: () => fetch(`http://myapi.com/posts/${postId}/comments`, {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ message })
}),
payload: { postId, message }
};
}
Redux 用函數(shù)描述邏輯更新減少了模版里大量的 Flux stores 。函數(shù)比對象簡單,比類更簡單得多。
考慮這個 Flux store:
let _todos = [];
export default const TodoStore = assign({}, EventEmitter.prototype, {
getAll() {
return _todos;
}
});
AppDispatcher.register(function (action) {
switch (action.type) {
case ActionTypes.ADD_TODO:
let text = action.text.trim();
_todos.push(text);
TodoStore.emitChange();
}
});
用了 Redux 之后,同樣的邏輯更新可以被寫成 reducing function:
export function todos(state = [], action) {
switch (action.type) {
case ActionTypes.ADD_TODO:
let text = action.text.trim();
return [...state, text];
default:
return state;
}
}
switch
語句 不是 真正的模版。真正的 Flux 模版是概念性的:發(fā)送更新的需求,用 Dispatcher 注冊 Store 的需求,Store 是對象的需求 (當你想要一個哪都能跑的 App 的時候復(fù)雜度會提升)。
不幸的是很多人仍然靠文檔里用沒用 switch
來選擇 Flux 框架。如果你不愛用 switch
你可以用一個單獨的函數(shù)來解決,下面會演示。
讓我們寫一個函數(shù)使得我們將 reducers 表達為 action types 到 handlers 的映射對象。例如,在我們的 todos
reducer 里這樣定義:
export const todos = createReducer([], {
[ActionTypes.ADD_TODO](state, action) {
let text = action.text.trim();
return [...state, text];
}
}
我們可以寫下面的幫忙函數(shù)來完成:
function createReducer(initialState, handlers) {
return function reducer(state = initialState, action) {
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](state, action);
} else {
return state;
}
}
}
不難對吧?Redux 沒有默認提供這樣的幫忙函數(shù),因為有好多種寫的方法??赡苣阆胍詣影哑胀?JS 對象變成不可變對象通過濕化服務(wù)器狀態(tài)??赡苣阆牒喜⒎祷貭顟B(tài)和當前狀態(tài)。有很多方法 “獲取所有” handler。這些都取決于你為你的團隊在特定項目中選擇的約定。
Redux reducer 的 API 是 (state, action) => state
,但是怎么創(chuàng)建這些 reducers 由你來定。
更多建議: