Redux 應用官方教學筆記


Posted by chihyu on 2021-01-25

課程影片流程:

  1. redux:建立一個專門放 redux 東西的資料夾
  2. redux/store.js:建立一個 store.js 檔案 (引入 rootReducer,放入 createStore)
  3. redux/reducers/todos.js:建立一個 reducers 資料夾(放入各個 reducer todos.js)
  4. redux/actionTypes.js:建立一個 actionTypes.js 檔案,把 action type export 出去
  5. redux/actions.js:建立一個 actions.js 檔案,裡面是 action creator(有 type、payload)
  6. redux/reducers/index.js:建立一個 index.js 的檔案,用來把不同的 reducer 結合起來
  7. src/index.js:在 render 整個 app 的地方引入 Provider,把 App 包在裡面,然後把 store 當作 prop 傳下去
  8. 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):更新 state
    function 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

  1. 安裝 redux-thunk
  2. 引入 thunkMiddleware
    import thunkMiddleware from 'redux-thunk'
    

連結 React、Redux

安裝 react-redux
如何連結:

  • useSelector:讓 component 從 store 讀取 state
  • useDispatch:把 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)
    
    // 從 react-redux 引入 useSelector
    import { useSelector } from 'react-redux'
    // 宣告 selectTodos 為要選取想要的 state
    const selectTodos = state => state.todos
    // 在 component 裡面用 useSelector 選到 state,宣告後就可以用了
    const todos = useSelector(selectTodos)
    
    ### 用 Provider 把 store 傳下去
  • 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)

#Web #React #Redux







Related Posts

Binary Search模板

Binary Search模板

Web開發學習筆記16 — OOP(Object Oriented Programming)、Constructor Function、Class

Web開發學習筆記16 — OOP(Object Oriented Programming)、Constructor Function、Class

DAY18:Highest and Lowest

DAY18:Highest and Lowest


Comments