減少樣板程式碼
Redux 部分受到 Flux 的啟發,而對 Flux 最常見的抱怨是它會讓您撰寫大量的樣板程式碼。在此食譜中,我們將探討 Redux 如何讓我們選擇程式碼的詳細程度,具體取決於個人風格、團隊偏好、長期可維護性等。
動作
動作是描述應用程式中發生事件的純粹物件,也是描述變更資料意圖的唯一方式。重要的是,動作是您必須發送的物件,這不是樣板,而是 Redux 的基本設計選擇之一。
有些宣稱類似 Flux 的架構,但沒有動作物件的概念。在可預測性方面,這比 Flux 或 Redux 退步了。如果沒有可序列化的純粹物件動作,就不可能記錄和重播使用者會話,或實作時光旅行熱重載。如果您寧願直接修改資料,您不需要 Redux。
動作看起來像這樣
{ type: 'ADD_TODO', text: 'Use Redux' }
{ type: 'REMOVE_TODO', id: 42 }
{ type: 'LOAD_ARTICLE', response: { ... } }
一個常見的慣例是,動作有一個常數類型,有助於簡化器(或 Flux 中的儲存)辨識它們。我們建議您使用字串,而不是符號作為動作類型,因為字串是可序列化的,而使用符號會讓記錄和重播變得比必要的更困難。
在 Flux 中,傳統上認為您會將每個動作類型定義為字串常數
const ADD_TODO = 'ADD_TODO'
const REMOVE_TODO = 'REMOVE_TODO'
const LOAD_ARTICLE = 'LOAD_ARTICLE'
這有什麼好處?人們常說常數是不必要的,對於小型專案來說,這可能是正確的。對於較大的專案,將動作類型定義為常數有一些好處
- 它有助於保持命名一致,因為所有動作類型都集中在同一個地方。
- 有時您希望在處理新功能之前,先查看所有現有的動作。團隊中的某人可能已經新增您需要的動作,但您不知道。
- 在 Pull Request 中新增、移除和變更的動作類型清單,有助於團隊中的每個人追蹤新功能的範圍和實作。
- 如果您在匯入動作常數時輸入錯誤,您會得到
undefined
。Redux 會在發送此類動作時立即拋出錯誤,您會更快找到錯誤。
由您選擇專案的慣例。您可以先使用內嵌字串,然後再轉換為常數,最後再將它們分組到一個檔案中。Redux 在這裡沒有任何意見,因此請自行判斷。
動作建立器
另一個常見的慣例是,您不會在發送動作的地方內嵌建立動作物件,而是建立產生動作的函式。
例如,不要使用物件文字呼叫 dispatch
// somewhere in an event handler
dispatch({
type: 'ADD_TODO',
text: 'Use Redux'
})
您可以在一個獨立的檔案中撰寫一個動作建立器,並將它匯入您的元件
actionCreators.js
export function addTodo(text) {
return {
type: 'ADD_TODO',
text
}
}
AddTodo.js
import { addTodo } from './actionCreators'
// somewhere in an event handler
dispatch(addTodo('Use Redux'))
動作建立器經常被批評為樣板。嗯,你不必撰寫它們!如果你覺得這樣比較適合你的專案,你可以使用物件字面值。然而,撰寫動作建立器有一些好處,你應該知道。
假設一位設計師在檢閱我們的原型後回來,並告訴我們我們最多需要允許三個待辦事項。我們可以透過使用 redux-thunk 中介軟體將我們的動作建立器改寫為回呼表單,並新增一個早期退出,來強制執行這一點
function addTodoWithoutCheck(text) {
return {
type: 'ADD_TODO',
text
}
}
export function addTodo(text) {
// This form is allowed by Redux Thunk middleware
// described below in “Async Action Creators” section.
return function (dispatch, getState) {
if (getState().todos.length === 3) {
// Exit early
return
}
dispatch(addTodoWithoutCheck(text))
}
}
我們剛剛修改了 addTodo
動作建立器的行為,對呼叫程式碼完全隱形。我們不必擔心查看每個新增待辦事項的地方,以確保它們有這個檢查。動作建立器讓你將發送動作周圍的附加邏輯,與發出這些動作的實際元件分離。當應用程式處於繁重開發階段,而且需求經常變更時,這非常方便。
產生動作建立器
像 Flummox 這樣的某些架構會自動從動作建立器函式定義產生動作類型常數。這個想法是你不必同時定義 ADD_TODO
常數和 addTodo()
動作建立器。在底層,這樣的解決方案仍然會產生動作類型常數,但它們是隱含建立的,因此它是一種間接層次,可能會造成混淆。我們建議你明確建立你的動作類型常數。
撰寫簡單的動作建立器可能會很累人,而且經常會產生多餘的樣板程式碼
export function addTodo(text) {
return {
type: 'ADD_TODO',
text
}
}
export function editTodo(id, text) {
return {
type: 'EDIT_TODO',
id,
text
}
}
export function removeTodo(id) {
return {
type: 'REMOVE_TODO',
id
}
}
你隨時可以撰寫一個產生動作建立器的函式
function makeActionCreator(type, ...argNames) {
return function (...args) {
const action = { type }
argNames.forEach((arg, index) => {
action[argNames[index]] = args[index]
})
return action
}
}
const ADD_TODO = 'ADD_TODO'
const EDIT_TODO = 'EDIT_TODO'
const REMOVE_TODO = 'REMOVE_TODO'
export const addTodo = makeActionCreator(ADD_TODO, 'text')
export const editTodo = makeActionCreator(EDIT_TODO, 'id', 'text')
export const removeTodo = makeActionCreator(REMOVE_TODO, 'id')
還有一些實用程式庫可以協助產生動作建立器,例如 redux-act 和 redux-actions。這些可以協助減少樣板程式碼,並強制遵守標準,例如 Flux Standard Action (FSA)。
非同步動作建立器
中介軟體 讓你注入自訂邏輯,在發送每個動作物件之前對其進行詮釋。非同步動作是中介軟體最常見的用例。
沒有任何中介軟體,dispatch
只接受一個純粹的物件,因此我們必須在我們的元件內部執行 AJAX 呼叫
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) {
// Injected into props by React Redux `connect()` call:
const { dispatch, posts } = this.props
if (posts[userId]) {
// There is cached data! Don't do anything.
return
}
// Reducer can react to this action by setting
// `isFetching` and thus letting us show a spinner.
dispatch(loadPostsRequest(userId))
// Reducer can react to these actions by filling the `users`.
fetch(`http://myapi.com/users/${userId}/posts`).then(
response => dispatch(loadPostsSuccess(userId, response)),
error => dispatch(loadPostsFailure(userId, error))
)
}
componentDidMount() {
this.loadData(this.props.userId)
}
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.loadData(this.props.userId)
}
}
render() {
if (this.props.isFetching) {
return <p>Loading...</p>
}
const posts = this.props.posts.map(post => (
<Post post={post} key={post.id} />
))
return <div>{posts}</div>
}
}
export default connect(state => ({
posts: state.posts,
isFetching: state.isFetching
}))(Posts)
然而,這很快就會變得重複,因為不同的元件會從同一個 API 端點請求資料。此外,我們想要從許多元件重複使用這些邏輯(例如,當有快取資料可用時提早退出)。
中間件讓我們撰寫更具表達力、潛在非同步的動作建立器。它讓我們傳送純粹物件以外的東西,並詮釋這些值。例如,中間件可以「捕捉」傳送的 Promise,並將它們轉換成一組要求和成功/失敗動作。
最簡單的中間件範例是 redux-thunk。「Thunk」中間件讓您可以將動作建立器寫成「thunk」,也就是傳回函式的函式。這會反轉控制權:您會取得 dispatch
作為引數,因此您可以撰寫一個會多次傳送的動作建立器。
注意
Thunk 中間件只是中間件的一個範例。中間件的重點不是「讓您傳送函式」。重點在於讓您傳送特定中間件知道如何處理的任何東西。Thunk 中間件在您傳送函式時會新增特定行為,但這真的取決於您使用的中間件。
考慮使用 redux-thunk 重新編寫上述程式碼
actionCreators.js
export function loadPosts(userId) {
// Interpreted by the thunk middleware:
return function (dispatch, getState) {
const { posts } = getState()
if (posts[userId]) {
// There is cached data! Don't do anything.
return
}
dispatch({
type: 'LOAD_POSTS_REQUEST',
userId
})
// Dispatch vanilla actions asynchronously
fetch(`http://myapi.com/users/${userId}/posts`).then(
response =>
dispatch({
type: 'LOAD_POSTS_SUCCESS',
userId,
response
}),
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))
}
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.props.dispatch(loadPosts(this.props.userId))
}
}
render() {
if (this.props.isFetching) {
return <p>Loading...</p>
}
const posts = this.props.posts.map(post => (
<Post post={post} key={post.id} />
))
return <div>{posts}</div>
}
}
export default connect(state => ({
posts: state.posts,
isFetching: state.isFetching
}))(Posts)
這樣鍵入的內容就少很多了!如果您願意,您仍然可以使用「純粹」動作建立器,例如 loadPostsSuccess
,您可以從容器 loadPosts
動作建立器使用它。
最後,您可以撰寫自己的中間件。假設您想要概括上述模式,並像這樣描述您的非同步動作建立器
export function loadPosts(userId) {
return {
// Types of actions to emit before and after
types: ['LOAD_POSTS_REQUEST', 'LOAD_POSTS_SUCCESS', 'LOAD_POSTS_FAILURE'],
// Check the cache (optional):
shouldCallAPI: state => !state.posts[userId],
// Perform the fetching:
callAPI: () => fetch(`http://myapi.com/users/${userId}/posts`),
// Arguments to inject in begin/end actions
payload: { userId }
}
}
詮釋此類動作的中間件可能如下所示
function callAPIMiddleware({ dispatch, getState }) {
return next => action => {
const { types, callAPI, shouldCallAPI = () => true, payload = {} } = action
if (!types) {
// Normal action: pass it on
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 callAPI 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,
type: successType
})
),
error =>
dispatch(
Object.assign({}, payload, {
error,
type: failureType
})
)
)
}
}
在將它傳遞給 applyMiddleware(...middlewares)
一次後,您可以用相同的方式撰寫所有呼叫 API 的動作建立器
export function loadPosts(userId) {
return {
types: ['LOAD_POSTS_REQUEST', 'LOAD_POSTS_SUCCESS', 'LOAD_POSTS_FAILURE'],
shouldCallAPI: state => !state.posts[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.comments[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 }
}
}
Reducer
Redux 透過將更新邏輯描述為函式,大幅減少了 Flux 儲存的樣板程式碼。函式比物件簡單,也比類別簡單許多。
考慮這個 Flux 儲存
const _todos = []
const TodoStore = Object.assign({}, EventEmitter.prototype, {
getAll() {
return _todos
}
})
AppDispatcher.register(function (action) {
switch (action.type) {
case ActionTypes.ADD_TODO:
const text = action.text.trim()
_todos.push(text)
TodoStore.emitChange()
}
})
export default TodoStore
使用 Redux,相同的更新邏輯可以描述為一個簡化器函數
export function todos(state = [], action) {
switch (action.type) {
case ActionTypes.ADD_TODO:
const text = action.text.trim()
return [...state, text]
default:
return state
}
}
switch
陳述式不是真正的樣板。Flux 的真正樣板是概念性的:需要發出更新、需要使用調度器註冊儲存,需要儲存是一個物件(以及當您想要一個通用應用程式時產生的複雜性)。
很不幸的是,許多人仍然根據文件是否在 switch
陳述式中使用來選擇 Flux 架構。如果您不喜歡 switch
,您可以使用單一函數來解決此問題,如下所示。
產生簡化器
讓我們撰寫一個函數,讓我們可以將簡化器表示為從動作類型到處理常式的物件對應。例如,如果我們希望我們的 todos
簡化器定義如下
export const todos = createReducer([], {
[ActionTypes.ADD_TODO]: (state, action) => {
const text = action.text.trim()
return [...state, text]
}
})
我們可以撰寫以下輔助程式來完成此操作
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 預設不提供這樣的輔助函數,因為有許多方法可以撰寫它。也許您希望它自動將純 JS 物件轉換為不變物件,以補充伺服器狀態。也許您想將回傳的狀態與目前的狀態合併。對於「全部處理」處理常式可能有不同的方法。所有這些都取決於您在特定專案中為您的團隊選擇的慣例。
Redux 簡化器 API 是 (state, action) => newState
,但您如何建立這些簡化器取決於您。