課程影片流程:
redux
:建立一個專門放 redux 東西的資料夾redux/store.js
:建立一個 store.js 檔案 (引入 rootReducer,放入 createStore)redux/reducers/todos.js
:建立一個 reducers 資料夾(放入各個 reducer todos.js)redux/actionTypes.js
:建立一個 actionTypes.js 檔案,把 action type export 出去redux/actions.js
:建立一個 actions.js 檔案,裡面是 action creator(有 type、payload)redux/reducers/index.js
:建立一個 index.js 的檔案,用來把不同的 reducer 結合起來src/index.js
:在 render 整個 app 的地方引入 Provider,把 App 包在裡面,然後把 store 當作 prop 傳下去component/App.js
:用 useSelector 把 redux 的 state 拿出來
基本
設定 state 的結構
Redux 的 state 是一個 object
思考有哪些值是需要放進 state
- todo item (id、、text、completed、color)
- filter (completed、color)
const todoAppState = { todos: [ { id: 0, text: 'Learn React', completed: true }, { id: 1, text: 'Learn Redux', completed: false, color: 'purple' }, { id: 2, text: 'Build something fun!', completed: false, color: 'blue' } ], filters: { status: 'Active', colors: ['red', 'blue'] } }
設定 Actions
Action 是一個 object,需要有 type,用在 app 會發生的事件
{type: '類別/要做的事', payload: 參數}
可以列個清單解釋會發生甚麼:
- 新增使用者輸入的 todo
{type: 'todos/todoAdded', payload: todoText}
- toggle 完成未完成狀態
{type: 'todos/todoToggled', payload: todoId}
- 為 todo 選一個顏色
{type: 'todos/colorSelected', payload: todoId, color}
- 刪除 todo
{type: 'todos/todoDeleted', payload: todoId}
- 標記所有 todo 為完成
{type: 'todos/allCompleted'}
- 清除所有完成的 todo
{type: 'todos/completedCleared'}
- filter-新增顏色
- filter-移除顏色
{type: 'filters/colorFilterChanged', payload: color, changeType}
- filter-選擇不同狀態
{type: 'filters/statusFilterChanged', payload: filterValue}
寫 Reducer
前面知道 state 的結構、actions 之後,就可以來寫 reducer
Reducer 是一個 function,接收 state、action 為參數,回傳新的 state (state, action) => newState
在 src 裡面新增 reducer.js,宣告 initial state(放入設定好的 state 的結構),然後把它引用在 reducer 的 function
export default function appReducer(state = initialState, action) {
switch(action.type) {
// 依據 action type 決定要做的事情
case:'action 的 type': {
//要做的事
return {
... state,
todos: [
...state.todos,
{
id:
text:
completed:
}
]
}
}
default:
// 如果沒有 action type 就執行 default
return state
}
}
把 reducer 細分出來
可以根據特性分出 todos 的 reducer 跟 filter 的 reducer
新增資料夾 feature,每個特性的檔案是一個 slice 檔案,裡面是 reducer 的邏輯
- todos 的部份 -> feature/todos/todoSlice.js
- filter -> feature/filter/filter.js
把 reducer 整合
在 reducer.js 引入 slice 的檔案
rootReducer 回傳 todos、filter 的 state
從 redux 引入 combineReducers,定義 key name 作為 state object 的名字
reducer.js
import { combineReducers } from 'redux'
import todosReducer from './features/todos/todosSlice'
import filtersReducer from './features/filters/filtersSlice'
const rootReducer = combineReducers({
// 設定 top-lavel state,帶入各自的 reducer
// key: reducer
// state.todos 就會回傳 todo reducer 的值
todos: todosReducer,
filters: filtersReducer
})
export default rootReducer
建立 store
一個 app 只會有一個 store
store 把 state、actions、reducer 整合在一起
store 的 method(store 裡面的 function):
- 存取 current state
store.getState()
:取得 current state,(注意不會複製 state,所以不要用 getState 去任意更改 state 的值)function getState() { return state }
store.dispatch(actions)
:更新 statefunction dispatch(action) { state = reducer(state, action) // 執行 reducer,更新 state 的值 listeners.forEach(listener => listener()) // 執行 subscribe() 裡面自訂的 function,也就是 listener }
store.subscribe(listener)
:執行 listener,state 改變就會執行,return 一個 function 用來移除新的 callback(listener?)
// 存放 listener
const listeners = []
// listener 是自定義的 function ex.function subscribe(state => console.log(state))
function subscribe(listener) {
listeners.push(listener) // 把 listener 加到 listeners 這個 array
return function unsubscribe() {
// 回傳值是把 listener 從 listeners 移除
const index = listeners.indexOf(listener)
listeners.splice(index, 1)
}
}
新建 store.js 檔案,引入 createStore,接收 rootReducer 為參數
store.js
import { createStore } from 'redux';
import rootReducer from './reducer';
const store = createStore(rootReducer)
export default store
也可以接收 preloadedState
作為第二個參數,當 store 被建立時加上初始資料,這個資料指的像是從 server 傳來的值或是 loacalStorage 裡的值
let preloadedState
const persistedTodoString = localString.getItem('todos')
if (persistedTodoString) {
preloadedState = {
todos: JSON.parse(persistedTodoString)
}
}
const store = createStore(rootReducer, preloadedState)
發送 Actions
每次呼叫 store.dispatch(action),就會:
- 呼叫 rootReducer(state, action)
- 儲存新的 state value
- 呼叫所有 listener 綁定的 callback
Configuring Store
在 createStore 加入第三個參數,客製化 store 的功能
用來客製化的東西叫做 store enhancer
可以覆蓋或取代 store 的 method(getState、dispatch、subscribe)
用 compose 可以把兩個 enhancer 結合
const composedEnhancer = compose(sayHiOnDispatch, includeMeaningOfLife)
const store = createStore(rootReducer, undefined, composedEnhancer)
Middleware
用來客製化 dispatch 的 function
發生在發送 action 後,到達 reducer 之前,提供一個第三方擴展點
可以有 side effect 的邏輯
應用:logging, crash reporting, talking to an asynchronous API, routing
由非常特殊的 store enhancer 實現,叫做 applyMiddleware
import { createStore, applyMiddleware } from 'redux'
import rootReducer from './reducer'
import { print1, print2, print3 } from './exampleAddons/middleware'
const middlewareEnhancer = applyMiddleware(print1, print2, print3)
// Pass enhancer as the second arg, since there's no preloadedState
const store = createStore(rootReducer, middlewareEnhancer)
export default store
寫客製化 Middleware
三個嵌套 function 構成
function exampleMiddleware(storeAPI) {
return function wrapDispatch(next) {
return function handleAction(action) {
// Do anything here: pass the action onwards with next(action),
// or restart the pipeline with storeAPI.dispatch(action)
// Can also use storeAPI.getState() here
return next(action)
}
}
}
exampleMiddleware
:會被 applyMiddleware 呼叫,接收 storeAPI 物件(包含 dispatch、getState),如果呼叫這個 dispatch,就會發送 action 到 middleware pipeline 的開始(只會呼叫一次)wrapDispatch
:接收 next 這個 function 作為參數,是 pipeline 的 next middleware,呼叫 next(action),就會把 middleware 傳到下個 middleware(只會呼叫一次)handleAction
:接收 current action 為參數,每次 aciton dipatch 就會被呼叫
Redux Thunk Middleware
thunk 是一個 funciton,會包住一個 expression 來延遲執行
A thunk is a function that wraps an expression to delay its evaluation.
Redux Thunk middleware 是一個 async function middleware,所以可以在 dispatch 傳入 function
- 安裝 redux-thunk
- 引入 thunkMiddleware
import thunkMiddleware from 'redux-thunk'
連結 React、Redux
安裝 react-redux
如何連結:
useSelector
:讓 component 從 store 讀取 stateuseDispatch
:把 action 發送到 store<Provider store={store}>
讓 component 可以存取到 store
用 useSelector 從 store 讀取 state
useSelector
:是一個 React-Redux hook,讓 react component 可以從 store 讀取資料- 把
useSelector
寫在 component 裡面,接收一個 selector function 作為參數,當 component render 的時候就會呼叫這個 function - useSelector 會自動 subscribe store,所以當 dispatch action 時,就會呼叫 selector function
const todos = useSelector(state => state.todos)
### 用 Provider 把 store 傳下去// 從 react-redux 引入 useSelector import { useSelector } from 'react-redux' // 宣告 selectTodos 為要選取想要的 state const selectTodos = state => state.todos // 在 component 裡面用 useSelector 選到 state,宣告後就可以用了 const todos = useSelector(selectTodos)
- 用
Provider
包住整個 App,並傳入 store 為 prop,讓所有 component 都可以存取 store// 從 React-Redux 引入 Provider import { Provider } from 'react-redux' // 在 render App 的地方使用 Provider ReactDOM.render( <React.StrictMode> <Provider store={store}> <App /> </Provider> </React.StrictMode>, document.getElementById('root') )
用 useDispatch 發送 actions
useDispatch
:是一個 React-redux hook,讓我們可以在 component 裡面呼叫 useDispatch 來發送 action// 從 react-redux 引入 useDispatch import { useDispatch } from 'react-redux' // 在 component 裡面宣告 dispatch 為 useDispatch const dispatch = useDispatch() // 在要執行的 funcion component 裡面使用 dispatch dispatch({ type: 'todos/todoAdded', payload: trimmedText })
Connect() (另一個連結 React redux 的方法)
function connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?)
mapStateToProps
:處理 redux store 的 state
mapDispatchToProps
:處理 redux store 的 dispatch
React-Redux Pattern
Global State 會存到 Redux Store,local State 會存在 React Component
shallowEqual
:可以比對 array 裡面的東西是否相同
當 todo list 的其中一個 item 有變動時,就不會所有 item 都 re-render,只有變動的那個 item 會 re-render
// 從 react-redux 引入 shallowEqual
import { useSelector, shallowEqual } from 'react-redux'
// 選取 todo 的 Id
const selectTodoIds = state => state.todos.map(todo => todo.id)
// 在 component 用 useSelector,並在第二個參數使用 shallowEqual
const todoIds = useSelector(selectTodoIds, shallowEqual)