參考來源:Redux 官方文件
Redux 簡介
甚麼是 Redux
是一個可以透過 actions 來管理和更新 state 的東西
- 當在不同環境(client、server、native)執行時,也能保持一致
- 容易測試
- 提供良好的開發體驗
### 為甚麼需要 Redux - 管理 global 的 state
- 可以清楚知道 state 更新的原因,就可以知道後續處理的邏輯
甚麼時候使用 Redux
- 有很多地方需要共同用到很多 state 的時候
- state 的更新很頻繁
- 更新 state 的邏輯很複雜
- codebase 很大、多人工作時
Redux Libraries、Tools
- React-Redux:讓 React 的 component 可以跟 Redux 的 store 結合,透過發送 action 來更新 store
- React Toolkit:用來寫 Redux 的邏輯
- Redux DevTools Extension:追蹤 Redux store 裡 state 的歷史紀錄,可以讓 degug 更有效率
(ex. time-travel-debugging)
Redux 術語、概念
state 的管理
單向 data flow 在規模大的時候,因為會有好幾個 component 共用同一個 state 的狀況,這樣可能會壞掉
解決方法:把 global state 集中到一個地方,利用特定模式把 state 更新
Immutability (不變性)
object、array 都是 mutable,要 immutable 更新值的話,必須先複製(解構) object、array,再去更新複製的值
Redux 更新值 immutably
術語
Actions
:是一個 object,想像成 event,描述在 app 裡面發生的事情
type: domain/eventName
payload: information
const AddTodoAction = {
type: 'todos/todoAdded', // 類別 / 事件名稱
payload: 'Buy milk' // 發生的事
}
Action Creators
:是一個 function,用來新建和回傳 actions 的 object
const addTodo = text => {
return {
type: 'todos/todoAdded',
payload: text
}
}
Reducers
:是一個 function,用來接收 current state 跟 action 的 object,並回傳更新的 state
Reducers 依循的規則:
- 根據 state 跟 action 來計算 state 的值
- 複製 state 來更新 state 的值,以 immutable 方式更新 state
- 不能執行非同步的邏輯、計算 random values、造成 side effect
Reducer 執行的步驟:
- 查看 action
- 如果 action 符合條件就複製 state
- 更新這個複製的 state 的值
const initialState = { value: 0 } function conterReducer(state = initialState, action) { // 查看 action if (action.type === 'counter/increment') { // 符合條件就複製 state return { ...state // 更新 state 的值 value: state.value + 1 } } return state }
Store
:是一個 object,用來存放 Redux 的 state
用 .getState
取得 state 的值
Dispatch
:store 的一個 method,用 store.dispatch()
來更新 state
在 dispatch 裡面傳入 action creator,reducer 會作為 event listener 根據 action 來更新 state
Selector
:是一個 function 用來選取 store 裡 state value 的特定資訊
Redux 的 data flow
初始設定
- root reducer function 建立一個 redux store
- store 呼叫一次 root reducer,儲存回傳值當作初始的 state
- 當 UI 第一次 render 時,UI component 根據 Redux store 的 current state 來決定要 render 甚麼
更新
- app 有甚麼變動的時候(ex. 使用者點擊按鈕)
- app code 發送(dispatch) action 到 Redux Store
- store 執行 reducer fucntion (之前的 state + 現在 action),產生的 value 會成為新的 state
- store 會通知所有跟這個 store 有關的 UI,讓他們知道 store 有更新
- 每個 UI 元件會檢查他們的所需的 state 的 data 是否改變
- 如果元件發現他們的 data 有變,就會 re-render 成新的 data
Redux App 結構
安裝 counter expample
npx create-react-app redux-essentials-example --template redux
App 的 內容
創建 redux store
- 透過 Toolkit 的 configureStore 建立 store
- 在 configureStore 寫入 reducer 的 key 跟 reducer function
- 透過 key 來更新 state
store 的範例:
import { configureStore } from '@reduxjs/toolkit'
import usersReducer from '../features/users/usersSlice'
import postsReducer from '../features/posts/postsSlice'
import commentsReducer from '../features/comments/commentsSlice'
export default configureStore({
reducer: {
users: usersReducer, // key: reducer
posts: postsReducer,
comments: commentsReducer
}
})
- state.users、state.posts、state.comments 是個別的 slice
- 每個 key 對應的 reducer(slice reducer function) 用來更新對應的 slice
創建 Slice reducer functions、Actions
- Toolkit 的 createSlice 處理 action type strings, action creator functions, and action objects
- action type 的第一個字對應到 createSlice 的 name,第二個字則對應到 reducer function 的 key
Reducer 的規則
規則:
- 根據 state、action 計算新的 state value
- 不能直接更新現有的 state,要先複製 state,然後更新這個複製的 state
- 不能做任何非同步的事情或其他 side effect
理由:
- 比較容易知道程式碼怎麼跑的、知道如何測試
Reducer、immutable 更新
不可 mutate state 的原因:
- 會產生 Bug,因為 UI 無法正確更新,顯示最新的 values
- 很難去了解 state 是因為甚麼或是如何更新的
- 很難寫測試
- 無法正確發揮 time-travel debugging 的功能
- 違反 Redux 的可預測性和使用方式
如何更新 state:copy original -> mutate the copy
用 thunk 編寫非同步邏輯
thunk 是一個特殊的 rudux function,可以包含非同步的邏輯
- inside thunk fucntion: 取得 dispatch、getState 作為參數
- outside creator function:建立並回傳 thunk function
React counter component
useSelector
:是一個 hook,可以取得 state 裡面所需的值- 每一次 dispatch 或是 store 更新的時候,useSelector 就會重新跑一次 selector fucntion,如果 selector 回傳的值不同,useSelector 就會把 component rerender 成新的值
useDispatch
:是一個 hook,幫我們進入 store,取得實際的 dispatch method
Component State、Form
- 會在整個 app 用到的 Global state 才需要放到 Redux store,如果只會在一個地方用的 state,就只需要放到 component state
- Global state --> Redux Store / Local state --> React Component
可以放到 Redux 的規則:
- 是否有其他部分會在意這個 data ?
- 是否會有這個 data 衍伸的 data ?
- 相同的 data 是否被用來驅動多個 component?
- 把 state 回復到一個給定的時間對你有價值嗎?ex.time travel debugging
- 是否需要 cache data?
- 是否想要在 hot-reloading UI component 時保持 data 一致?
提供 Store
<Provider></Provider>
:把 store 傳下去,讓 useSelector、useDispatch 可以跟 provider 的 store 溝通
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
基礎 Redux data flow
Redux 的 state 由 reducer function 更新:
- Reducer 計算新的 state,複製現有 state 的值,並把複製 state 的值改成新的值
createSlice
function 會產生 slice reducer function,用來寫變更的 code,並以immutable 安全的更新- slice reducer functions 會被加到 configureStore 的 reducer field,並定義 Store 裡 data 和 state field 的名字
Component 透過 useSelector hook 去讀取 store 裡面的 data
- Selector function 接收整個 state object,並回傳 value
- 當 Redux store 更新時,Selectors 會重新跑,如果回傳的data 有變,component 就會 re-render
Component 透過 useDispatch hook 去 dispatch action 並更新 store
- createSlice 會幫我們把每個添加到 slice 的 reducer 產生 action creator function
- 在 component 呼叫 dispatch(someActionCreator()),去發送 action
- reducer 執行並檢查 action 是否相關,如果有就會回傳新的值
- 臨時的 data(form 的 input value) 會被存在 react component state,當完成 form 時,就會發送 action 更新 store
使用 Redux Data
任何 React Component 可以使用 Redux Store 的 data
- 任何 component 可以讀取任何在 Redux store 裡的 data
- 多個 component 可以同時讀取同個 data
- component 應該取所需的 data 最小數量
- component 可以結合 props、state、store 的值來決定要 render 甚麼,可以從 store 中讀取多個 data,並根據需要調整 data 以顯示
- 任何 component 可以 dispatch action 來更新 state
Redux action creator 可以準備內容正確的 action object
- createSlice、createAction 可以接受回傳 action payload 的 prepare callback
- 獨特 ID 和其他隨機值應該要放到 action,而不是在 reducer 裡計算
Reducer 包含實際 state 的更新邏輯
- Reducer 可以包含用來計算下個 state 的任何邏輯
- Action object 應該包含剛剛好的訊息來描述發生的事
Async Logic and Data Fetching
用重複使用的 function 來從 state 讀取值
- Selector 是一個 function,用 state 作為參數,並回傳一些 data
Redux 使用 middleware plugin 來實現 async 邏輯
- 標準 async middleware 稱為 redux-thunk,已經包含在 Redux Toolkit 裡面
- thunk function 接收 dispatch、getState 為參數,並用在 async 的邏輯
你可以發送額外的 action 來追蹤 API 的 loading status
- 在呼叫 api 之前發送 pending action,success 包含 data、failure 包含 error
- loading state 應該存成 enum
Redux Toolkit 有一個 createAsyncThunk 的 API,可以發送這些 action:
- createAsyncThunk 會接受一個會回傳 Promise 的 "payload creator" callback,並自動產生
pending/fulfilled/rejected
的 action types - fetchPosts 會根據回傳的 promise 發送 actions
- 利用 extraReducers 去監聽 createSlice 裡的 action types,並根據 action 來更新 reducer 的 state
- Action creator 可以自動填入 extraReducers object 的 key,這樣 slice 就可以知道要監聽哪個 action
性能、規格化 data
Memoized selector functions 可以優化性能
- Redux Toolkit 從 Reselect 重新 export createSelector function,產生 memoized selectors
- 如果 input selector 回傳新的值,memoized selectors 就會重新計算結果
- 記憶化可以跳過龐大的計算,也能確保回傳的結果來源相同
用 Redux 優化 React component 的 patterns
- 避免在 useSelector 新增 object/array references,會造成不必要的 re-render
- Memoized selector functions 可以傳進 useSelector 來優化渲染
- useSelector 可以接受一個 alternate comparison function(ex. shallowEqual),代替 reference equality
- component 可以放在 React.memo() 裡,以便只有在 props 改變時 re-render
- 可以透過 parent component 讀取 children 的 ID,透過 ID 取得 children 的 item
推薦用 Normalized state 結構儲存 items
- "Normalization" 指不複製 data,用 item ID 把 item 存在一個 lookup table(查找表)
- Normalized state shape usually looks like {ids: [], entities: {}}
Redux Toolkit's createEntityAdapter API helps manage normalized data in a slice
- item ID 可以用 sortComparer 分類排序
- adapter object 包含:
- adapter.getInitialState:可以接受額外的 state field(ex. loading state)
- prebuilt reducer:setAll, addMany, upsertOne, and removeMany
- adapter.getSelectors:會產生 selector 像 selectAll、selectById