使用選擇器衍生資料
- 為何良好的 Redux 架構會將狀態維持在最小值,並衍生其他資料
- 使用選擇器函式衍生資料和封裝查詢的原則
- 如何使用 Reselect 函式庫撰寫備忘選取器進行最佳化
- 使用 Reselect 的進階技巧
- 用於建立選取器的其他工具和函式庫
- 撰寫選取器的最佳實務
衍生資料
我們特別建議 Redux 應用程式應 讓 Redux 狀態保持最小,並儘可能從該狀態衍生其他值。
這包括計算過濾清單或加總值等事項。舉例來說,待辦事項應用程式會在狀態中保留待辦事項物件的原始清單,但會在每次更新狀態時在狀態外部衍生已過濾的待辦事項清單。類似地,也可以在儲存區外部計算所有待辦事項是否已完成的檢查,或計算剩餘待辦事項的數量。
這有幾個好處
- 實際狀態較易讀取
- 計算這些其他值並讓它們與其他資料保持同步所需的邏輯較少
- 原始狀態仍作為參考存在,且不會被取代
這也是 React 狀態的一個好原則!許多時候,使用者嘗試定義一個 useEffect
勾子,等待狀態值變更,然後使用一些衍生值(例如 setAllCompleted(allCompleted)
)設定狀態。相反地,可以在渲染過程中衍生該值並直接使用,而完全不需要將值儲存到狀態中
function TodoList() {
const [todos, setTodos] = useState([])
// Derive the data while rendering
const allTodosCompleted = todos.every(todo => todo.completed)
// render with this value
}
使用選取器計算衍生資料
在典型的 Redux 應用程式中,用於衍生資料的邏輯通常寫成我們稱為選取器的函式。
選取器主要用於封裝從狀態中查詢特定值的邏輯、實際衍生值的邏輯,以及透過避免不必要的重新計算來提升效能。
您不需要對所有狀態查詢使用選取器,但它們是一種標準模式,且廣泛使用。
基本選擇器概念
「選擇器函數」是任何接受 Redux 儲存狀態(或部分狀態)作為參數,並傳回基於該狀態的資料的函數。
選擇器不一定要使用特殊函式庫撰寫,而且無論您使用箭頭函數或 function
關鍵字撰寫都沒有關係。例如,以下都是有效的選擇器函數
// Arrow function, direct lookup
const selectEntities = state => state.entities
// Function declaration, mapping over an array to derive values
function selectItemIds(state) {
return state.items.map(item => item.id)
}
// Function declaration, encapsulating a deep lookup
function selectSomeSpecificField(state) {
return state.some.deeply.nested.field
}
// Arrow function, deriving values from an array
const selectItemsWhoseNamesStartWith = (items, namePrefix) =>
items.filter(item => item.name.startsWith(namePrefix))
選擇器函數可以取用您想要的任何名稱。不過,我們建議選擇器函數名稱加上 select
字首,並結合所選取值的描述。這類型的典型範例包括 selectTodoById
、selectFilteredTodos
和 selectVisibleTodos
。
如果您使用 React-Redux 的 useSelector
勾子,您可能已經熟悉選擇器函數的基本概念 - 傳遞給 useSelector
的函數必須是選擇器
function TodoList() {
// This anonymous arrow function is a selector!
const todos = useSelector(state => state.todos)
}
選擇器函數通常在 Redux 應用程式的兩個不同部分定義
- 在切片檔案中,與 reducer 邏輯並列
- 在元件檔案中,在元件外部或
useSelector
呼叫中內嵌
可以在任何有權存取整個 Redux 根狀態值的地方使用選擇器函數。這包括 useSelector
勾子、connect
的 mapState
函數、中介軟體、thunk 和 saga。例如,thunk 和中介軟體有權存取 getState
參數,因此您可以在這裡呼叫選擇器
function addTodosIfAllowed(todoText) {
return (dispatch, getState) => {
const state = getState()
const canAddTodos = selectCanAddTodos(state)
if (canAddTodos) {
dispatch(todoAdded(todoText))
}
}
}
通常無法在 reducer 內部使用選擇器,因為切片 reducer 只可以存取 Redux 狀態中的切片,而大多數選擇器預期會將整個 Redux 根狀態作為參數傳遞。
使用選擇器封裝狀態形狀
使用選擇器函數的第一個原因是封裝和重複使用,以處理 Redux 狀態形狀。
假設你的其中一個 useSelector
鉤子對 Redux 狀態的一部分進行非常具體的查詢
const data = useSelector(state => state.some.deeply.nested.field)
這是合法的程式碼,而且會執行良好。但是,在架構上可能不是最好的主意。想像一下,你有幾個元件需要存取該欄位。如果你需要變更該狀態區塊所在的位置,會發生什麼事?現在你必須變更每個參照該值的 useSelector
鉤子。因此,就像 我們建議使用動作建立器來封裝建立動作的詳細資料 一樣,我們建議定義可重複使用的選擇器來封裝有關特定狀態區塊所在位置的知識。然後,你可以在程式碼庫中多次使用給定的選擇器函式,在應用程式需要擷取特定資料的任何位置。
理想情況下,只有你的簡化器函式和選擇器應該知道確切的狀態結構,因此如果你變更某些狀態所在的位置,你只需要更新這兩個邏輯區塊.
因此,通常建議在區塊檔案中直接定義可重複使用的選擇器,而不是總是將它們定義在元件內部。
選擇器的常見描述之一是它們就像「對你的狀態進行查詢」。你不在乎查詢如何產生你需要的資料,只在乎你要求提供資料並取得結果。
使用記憶化最佳化選擇器
選擇器函式通常需要執行相對「昂貴」的計算,或建立新的物件和陣列參照的衍生值。這可能會影響應用程式的效能,原因有幾個
- 與
useSelector
或mapState
一起使用的選擇器會在每個派送的動作後重新執行,無論 Redux 根狀態的哪個區段實際上已更新。當輸入狀態區段未變更時重新執行昂貴的計算會浪費 CPU 時間,而且輸入資料在大部分時間內不太可能變更。 useSelector
和mapState
仰賴回傳值的===
參考相等性檢查,以判斷元件是否需要重新渲染。如果選取器總是回傳新的參考,它將強制元件重新渲染,即使衍生的資料實際上與上次相同。這在陣列操作(例如map()
和filter()
)中特別常見,這些操作會回傳新的陣列參考。
舉例來說,這個元件寫得很糟糕,因為它的 useSelector
呼叫總是回傳新的陣列參考。這表示元件會在每個發派的動作後重新渲染,即使輸入的 state.todos
切片沒有變更
function TodoList() {
// ❌ WARNING: this _always_ returns a new reference, so it will _always_ re-render!
const completedTodos = useSelector(state =>
state.todos.map(todo => todo.completed)
)
}
另一個範例是一個需要執行一些「昂貴」的工作來轉換資料的元件
function ExampleComplexComponent() {
const data = useSelector(state => {
const initialData = state.data
const filteredData = expensiveFiltering(initialData)
const sortedData = expensiveSorting(filteredData)
const transformedData = expensiveTransformation(sortedData)
return transformedData
})
}
類似地,這個「昂貴」的邏輯會在每個發派的動作後重新執行。它不僅可能會建立新的參考,而且這項工作在 state.data
實際變更前是不需要執行的。
因此,我們需要一種方法來撰寫最佳化的選取器,如果傳入相同的輸入,就能避免重新計算結果。這就是記憶化概念的由來。
記憶化是一種快取形式。它包含追蹤函式的輸入,以及儲存輸入和結果以供以後參考。如果函式使用與之前相同的輸入呼叫,函式可以跳過實際的工作,並回傳上次收到這些輸入值時產生的相同結果。這透過僅在輸入變更時執行工作,以及在輸入相同時持續回傳相同的結果參考,來最佳化效能。
接下來,我們將探討一些撰寫記憶化選取器的選項。
使用 Reselect 撰寫記憶化選取器
Redux 生態系統傳統上使用名為 Reselect 的函式庫來建立記憶化選取器函式。還有其他類似的函式庫,以及 Reselect 的多種變體和包裝器 - 我們稍後將探討這些內容。
createSelector
概觀
Reselect 提供一個名為 createSelector
的函式,用於產生記憶化選取器。createSelector
接受一個或多個「輸入選取器」函式,加上一個「輸出選取器」函式,並回傳一個新的選取器函式供你使用。
createSelector
包含在 我們的官方 Redux Toolkit 套件 中,並重新匯出以方便使用。
createSelector
可以接受多個輸入選擇器,這些選擇器可以作為單獨的參數或陣列提供。所有輸入選擇器的結果會作為單獨的參數提供給輸出選擇器
const selectA = state => state.a
const selectB = state => state.b
const selectC = state => state.c
const selectABC = createSelector([selectA, selectB, selectC], (a, b, c) => {
// do something with a, b, and c, and return a result
return a + b + c
})
// Call the selector function and get a result
const abc = selectABC(state)
// could also be written as separate arguments, and works exactly the same
const selectABC2 = createSelector(selectA, selectB, selectC, (a, b, c) => {
// do something with a, b, and c, and return a result
return a + b + c
})
當你呼叫選擇器時,Reselect 會使用你提供的參數執行你的輸入選擇器,並查看傳回的值。如果任何結果與之前不同,它會重新執行輸出選擇器,並將這些結果作為參數傳遞。如果所有結果與上次相同,它會跳過重新執行輸出選擇器,並只傳回之前快取的最終結果。
這表示「輸入選擇器」通常應該只擷取並傳回值,而「輸出選擇器」應該執行轉換工作。
一個常見的錯誤是撰寫一個擷取值或執行一些推導的「輸入選擇器」,以及一個只傳回其結果的「輸出選擇器」
// ❌ BROKEN: this will not memoize correctly, and does nothing useful!
const brokenSelector = createSelector(
state => state.todos,
todos => todos
)
任何只傳回其輸入的「輸出選擇器」都是不正確的!輸出選擇器應該始終具有轉換邏輯。
類似地,快取選擇器絕不應該使用 state => state
作為輸入!這會強制選擇器始終重新計算。
在典型的 Reselect 使用中,你會將頂層「輸入選擇器」寫成純函式,並使用 createSelector
來建立快取選擇器,以查詢巢狀值
const state = {
a: {
first: 5
},
b: 10
}
const selectA = state => state.a
const selectB = state => state.b
const selectA1 = createSelector([selectA], a => a.first)
const selectResult = createSelector([selectA1, selectB], (a1, b) => {
console.log('Output selector running')
return a1 + b
})
const result = selectResult(state)
// Log: "Output selector running"
console.log(result)
// 15
const secondResult = selectResult(state)
// No log output
console.log(secondResult)
// 15
請注意,當我們第二次呼叫 selectResult
時,「輸出選擇器」沒有執行。由於 selectA1
和 selectB
的結果與第一次呼叫相同,因此 selectResult
能夠傳回第一次呼叫的快取結果。
createSelector
行為
請務必注意,預設情況下,createSelector
僅快取最新的一組參數。這表示如果你重複呼叫一個選擇器並使用不同的輸入,它仍然會傳回結果,但它必須重新執行輸出選擇器來產生結果
const a = someSelector(state, 1) // first call, not memoized
const b = someSelector(state, 1) // same inputs, memoized
const c = someSelector(state, 2) // different inputs, not memoized
const d = someSelector(state, 1) // different inputs from last time, not memoized
此外,你可以將多個參數傳遞到選擇器中。Reselect 會使用這些確切的輸入呼叫所有輸入選擇器
const selectItems = state => state.items
const selectItemId = (state, itemId) => itemId
const selectItemById = createSelector(
[selectItems, selectItemId],
(items, itemId) => items[itemId]
)
const item = selectItemById(state, 42)
/*
Internally, Reselect does something like this:
const firstArg = selectItems(state, 42);
const secondArg = selectItemId(state, 42);
const result = outputSelector(firstArg, secondArg);
return result;
*/
因此,你提供的「輸入選擇器」都應該接受相同類型的參數非常重要。否則,這些選擇器將會中斷。
const selectItems = state => state.items
// expects a number as the second argument
const selectItemId = (state, itemId) => itemId
// expects an object as the second argument
const selectOtherField = (state, someObject) => someObject.someField
const selectItemById = createSelector(
[selectItems, selectItemId, selectOtherField],
(items, itemId, someField) => items[itemId]
)
在此範例中,selectItemId
預期其第二個引數會是某個簡單值,而 selectOtherField
則預期第二個引數為物件。如果您呼叫 selectItemById(state, 42)
,selectOtherField
會中斷,因為它嘗試存取 42.someField
。
Reselect 使用模式和限制
巢狀選擇器
可以採用使用 createSelector
產生的選擇器,並將它們用作其他選擇器的輸入。在此範例中,selectCompletedTodos
選擇器用作 selectCompletedTodoDescriptions
的輸入
const selectTodos = state => state.todos
const selectCompletedTodos = createSelector([selectTodos], todos =>
todos.filter(todo => todo.completed)
)
const selectCompletedTodoDescriptions = createSelector(
[selectCompletedTodos],
completedTodos => completedTodos.map(todo => todo.text)
)
傳遞輸入參數
Reselect 產生的選擇器函式可以呼叫任意個引數:selectThings(a, b, c, d, e)
。不過,重新執行輸出的重點不在於引數數量,或引數本身是否已變更為新的參照。重點在於定義的「輸入選擇器」,以及它們的結果是否已變更。同樣地,「輸出選擇器」的引數完全基於輸入選擇器傳回的內容。
這表示如果您想要傳遞其他參數至輸出選擇器,您必須定義輸入選擇器,從原始選擇器引數中萃取這些值
const selectItemsByCategory = createSelector(
[
// Usual first input - extract value from `state`
state => state.items,
// Take the second arg, `category`, and forward to the output selector
(state, category) => category
],
// Output selector gets (`items, category)` as args
(items, category) => items.filter(item => item.category === category)
)
然後您可以像這樣使用選擇器
const electronicItems = selectItemsByCategory(state, "electronics");
為了保持一致性,您可能想要考慮將其他參數傳遞至選擇器,例如 selectThings(state, otherArgs)
,然後從 otherArgs
物件中萃取值。
選擇器工廠
createSelector
僅有一個預設快取大小 1,而且這是針對每個選擇器獨特執行個體。當單一選擇器函式需要在多個位置重複使用,且輸入不同時,這會造成問題。
一種選項是建立「選擇器工廠」,這是一個執行 createSelector()
的函式,並在每次呼叫時產生新的獨特選擇器執行個體
const makeSelectItemsByCategory = () => {
const selectItemsByCategory = createSelector(
[state => state.items, (state, category) => category],
(items, category) => items.filter(item => item.category === category)
)
return selectItemsByCategory
}
當多個類似 UI 元件需要根據道具衍生資料的不同子集時,這特別有用。
替代選擇器函式庫
儘管 Reselect 是與 Redux 搭配使用最廣泛的選擇器函式庫,但還有許多其他函式庫可以解決類似的問題,或擴充 Reselect 的功能。
proxy-memoize
proxy-memoize
是一個相對較新的快取選擇器函式庫,它使用獨特的實作方法。它依賴 ES2015 Proxy
物件來追蹤巢狀值的嘗試讀取,然後只比較稍後呼叫的巢狀值,以查看它們是否已變更。在某些情況下,這可以提供比 Reselect 更好的結果。
一個很好的範例是一個選擇器,它衍生出一個待辦事項描述陣列
import { createSelector } from 'reselect'
const selectTodoDescriptionsReselect = createSelector(
[state => state.todos],
todos => todos.map(todo => todo.text)
)
不幸的是,如果 state.todos
內部的任何其他值變更,例如切換 todo.completed
旗標,這將重新計算衍生陣列。衍生陣列的內容是相同的,但由於輸入 todos
陣列已變更,因此它必須計算新的輸出陣列,而該陣列具有新的參考。
使用 proxy-memoize
的相同選擇器可能如下所示
import { memoize } from 'proxy-memoize'
const selectTodoDescriptionsProxy = memoize(state =>
state.todos.map(todo => todo.text)
)
與 Reselect 不同,proxy-memoize
可以偵測只有 todo.text
欄位被存取,並且只有在其中一個 todo.text
欄位變更時才會重新計算其餘欄位。
它還有一個內建的 size
選項,它允許您設定單一選擇器執行個體的所需快取大小。
它與 Reselect 有些取捨和差異
- 所有值都作為單一物件引數傳入
- 它要求環境支援 ES2015
Proxy
物件(不支援 IE11) - 它更神奇,而 Reselect 則更明確
- 關於基於
Proxy
的追蹤行為有一些邊緣案例 - 它較新且使用較不廣泛
綜上所述,我們正式鼓勵考慮使用 proxy-memoize
作為 Reselect 的可行替代方案。
re-reselect
https://github.com/toomuchdesign/re-reselect 透過允許您定義「金鑰選擇器」來改善 Reselect 的快取行為。這用於在內部管理 Reselect 選擇器的多個執行個體,這有助於簡化跨多個元件的使用。
import { createCachedSelector } from 're-reselect'
const getUsersByLibrary = createCachedSelector(
// inputSelectors
getUsers,
getLibraryId,
// resultFunc
(users, libraryId) => expensiveComputation(users, libraryId)
)(
// re-reselect keySelector (receives selectors' arguments)
// Use "libraryName" as cacheKey
(_state_, libraryName) => libraryName
)
reselect-tools
有時很難追蹤多個 Reselect 選擇器如何彼此關聯,以及是什麼導致選擇器重新計算。 https://github.com/skortchmark9/reselect-tools 提供一種追蹤選擇器依賴關係的方法,以及它自己的 DevTools 來幫助視覺化這些關係並檢查選擇器值。
redux-views
https://github.com/josepot/redux-views 類似於 re-reselect
,它提供一種方式來為每個項目選取唯一的鍵,以進行一致的快取。它被設計為 Reselect 的近乎直接替換,並實際上被建議為 Reselect 版本 5 的潛在選項。
Reselect v5 提案
我們在 Reselect 儲存庫中開啟了一個路線圖討論,以找出對 Reselect 未來版本的潛在增強功能,例如改善 API 以更好地支援較大的快取大小、使用 TypeScript 重寫程式碼庫,以及其他可能的改善。我們歡迎在該討論中獲得更多社群回饋
使用 React-Redux 的選取器
使用參數呼叫選取器
通常會想要將其他引數傳遞給選取器函式。但是,useSelector
始終使用一個引數呼叫提供的選取器函式 - Redux 根 state
。
最簡單的解決方案是將匿名選取器傳遞給 useSelector
,然後立即使用 state
和任何其他引數呼叫真正的選取器
import { selectTodoById } from './todosSlice'
function TodoListitem({ todoId }) {
// Captures `todoId` from scope, gets `state` as an arg, and forwards both
// to the actual selector function to extract the result
const todo = useSelector(state => selectTodoById(state, todoId))
}
建立唯一的選取器實例
在許多情況下,需要在多個元件中重複使用選取器函式。如果元件都將使用不同的引數呼叫選取器,它將會中斷記憶化 - 選取器從未連續看到相同的引數多次,因此永遠無法傳回快取值。
此處的標準方法是在元件中建立記憶化選取器的唯一實例,然後使用 useSelector
。這允許每個元件持續將相同的引數傳遞給其自己的選取器實例,而該選取器可以正確記憶化結果。
對於函式元件,這通常使用 useMemo
或 useCallback
來完成
import { makeSelectItemsByCategory } from './categoriesSlice'
function CategoryList({ category }) {
// Create a new memoized selector, for each component instance, on mount
const selectItemsByCategory = useMemo(makeSelectItemsByCategory, [])
const itemsByCategory = useSelector(state =>
selectItemsByCategory(state, category)
)
}
對於使用 connect
的類別元件,這可以使用 mapState
的進階「工廠函式」語法來完成。如果 mapState
函式在第一次呼叫時傳回新的函式,則會將其用作真正的 mapState
函式。這提供了一個封閉,您可以在其中建立新的選取器實例
import { makeSelectItemsByCategory } from './categoriesSlice'
const makeMapState = (state, ownProps) => {
// Closure - create a new unique selector instance here,
// and this will run once for every component instance
const selectItemsByCategory = makeSelectItemsByCategory()
const realMapState = (state, ownProps) => {
return {
itemsByCategory: selectItemsByCategory(state, ownProps.category)
}
}
// Returning a function here will tell `connect` to use it as
// `mapState` instead of the original one given to `connect`
return realMapState
}
export default connect(makeMapState)(CategoryList)
有效使用選取器
雖然選取器是 Redux 應用程式中常見的模式,但它們經常被誤用或誤解。以下是正確使用選取器函式的部分準則。
在 Reducer 旁定義 Selector
Selector 函式通常定義在 UI 層,直接在 useSelector
呼叫內部。然而,這表示在不同檔案中定義的 Selector 可能會重複,而且函式是匿名的。
如同任何其他函式,你可以將匿名函式從元件外部抽取出來,並為其命名
const selectTodos = state => state.todos
function TodoList() {
const todos = useSelector(selectTodos)
}
然而,應用程式的多個部分可能想要使用相同的查詢。此外,在概念上,我們可能想要將 todos
狀態如何組織的知識保留在 todosSlice
檔案內的實作細節中,以便將所有內容集中在一處。
因此,建議在對應的 Reducer 旁定義可重複使用的 Selector。在此情況下,我們可以從 todosSlice
檔案匯出 selectTodos
import { createSlice } from '@reduxjs/toolkit'
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
todoAdded(state, action) {
state.push(action.payload)
}
}
})
export const { todoAdded } = todosSlice.actions
export default todosSlice.reducer
// Export a reusable selector here
export const selectTodos = state => state.todos
如此一來,如果我們碰巧更新 todos 片段狀態的結構,相關的 Selector 就會在這裡,而且可以同時更新,而應用程式的任何其他部分只需進行極少的變更。
平衡 Selector 使用
有可能在應用程式中加入過多 Selector。針對每個欄位加入獨立的 Selector 函式並非好主意!這樣最終會將 Redux 變成類似 Java 類別的東西,每個欄位都有 getter/setter 函式。這不會改善程式碼,而且可能會讓程式碼更糟 - 維護所有這些額外的 Selector 需要大量額外的工夫,而且會更難追蹤在何處使用哪些值。
類似地,不要讓每個 Selector 都備忘!。只有在你真正衍生結果且衍生結果可能會每次都建立新的參照時,才需要備忘。執行直接查詢並傳回值的 Selector 函式應該是純函式,而非備忘函式。
以下是一些何時備忘、何時不備忘的範例
// ❌ DO NOT memoize: will always return a consistent reference
const selectTodos = state => state.todos
const selectNestedValue = state => state.some.deeply.nested.field
const selectTodoById = (state, todoId) => state.todos[todoId]
// ❌ DO NOT memoize: deriving data, but will return a consistent result
const selectItemsTotal = state => {
return state.items.reduce((result, item) => {
return result + item.total
}, 0)
}
const selectAllCompleted = state => state.todos.every(todo => todo.completed)
// ✅ SHOULD memoize: returns new references when called
const selectTodoDescriptions = state => state.todos.map(todo => todo.text)
根據元件需要調整狀態
Selector 不必侷限於直接查詢 - 它們可以在內部執行任何必要的轉換邏輯。這對於協助準備特定元件所需的資料特別有價值。
Redux 狀態通常以「原始」形式儲存資料,因為狀態應該保持最少,而且許多元件可能需要以不同的方式呈現相同的資料。你可以使用 Selector 不僅萃取狀態,而且根據此特定元件的需求調整狀態。這可能包括從根狀態的多個片段中提取資料、萃取特定值、將資料的不同部分合併在一起,或任何其他有用的轉換。
如果元件也具備部分此邏輯,這沒問題,但將所有這些轉換邏輯抽取到獨立的 Selector 中,對於更好的重複使用和可測試性是有益的。
如果需要,請將選擇器全球化
撰寫區段縮減器和選擇器之間存在固有的不平衡。區段縮減器只知道狀態的一小部分 - 對縮減器來說,它的 state
就是全部,例如 todoSlice
中的待辦事項陣列。另一方面,選擇器通常被寫成以整個 Redux 根狀態作為其參數。這表示它們必須知道根狀態中此區段的資料儲存在哪裡,例如 state.todos
,即使這在建立根縮減器之前並未真正定義(通常在應用程式範圍的儲存設定邏輯中)。
典型的區段檔案通常同時具有這兩種模式。這很好,尤其是在小型或中型應用程式中。但是,根據應用程式的架構,您可能希望進一步抽象化選擇器,讓它們不知道區段狀態儲存在哪裡 - 必須將其傳遞給它們。
我們將此模式稱為「全球化」選擇器。「全球化」選擇器接受 Redux 根狀態作為參數,並知道如何尋找相關的狀態區段來執行實際邏輯。「本地化」選擇器預期只有一個狀態片段作為參數,而不知道或不關心它在根狀態中的位置
// "Globalized" - accepts root state, knows to find data at `state.todos`
const selectAllTodosCompletedGlobalized = state =>
state.todos.every(todo => todo.completed)
// "Localized" - only accepts `todos` as argument, doesn't know where that came from
const selectAllTodosCompletedLocalized = todos =>
todos.every(todo => todo.completed)
「本地化」選擇器可以透過將它們包裝在一個知道如何擷取正確的狀態區段並將其傳遞出去的函式中,轉換成「全球化」選擇器。
Redux Toolkit 的 createEntityAdapter
API 是此模式的一個範例。如果您呼叫 todosAdapter.getSelectors()
,且沒有參數,它會傳回一組「本地化」選擇器,這些選擇器預期實體區段狀態作為其參數。如果您呼叫 todosAdapter.getSelectors(state => state.todos)
,它會傳回一組「全球化」選擇器,這些選擇器預期以Redux 根狀態作為其參數被呼叫。
擁有「本地化」版本的選擇器也可能還有其他好處。例如,假設我們有一個進階場景,在儲存體中保留多個 createEntityAdapter
資料的巢狀結構,例如追蹤房間的 chatRoomsAdapter
,然後每個房間定義都有 chatMessagesAdapter
狀態來儲存訊息。我們無法直接查詢每個房間的訊息 - 我們必須先擷取房間物件,然後從中選取訊息。如果我們有一組針對訊息的「本地化」選擇器,這會更容易。
進一步的資訊
- 選擇器函式庫
- Reselect:https://github.com/reduxjs/reselect
proxy-memoize
:https://github.com/dai-shi/proxy-memoizere-reselect
:https://github.com/toomuchdesign/re-reselectreselect-tools
:https://github.com/skortchmark9/reselect-toolsredux-views
:https://github.com/josepot/redux-views
- Reselect v5 路線圖討論:目標和 API 設計
- Randy Coulman 有一系列出色的部落格文章,探討選擇器架構和全球化 Redux 選擇器時的不同方法,並列出其權衡利弊