# 第七週 本頁面連結:https://hackmd.io/UL3k5CHxRl2C87HPnI2Tcw ## 完課獎勵說明 完整獎勵說明:https://hackmd.io/ry4GF6KWROKbx9cLf51bkA RPG 獎勵說明連結:https://rpg.hexschool.com/#/training/12062543649513962870/board/content/12062543649513962883_12062543649513962886 - 2/16 前 50%:超過 100 人挑戰成功,全班直播加碼授權一年 - 前完成課程報到 -> 第四週主線 - 2/23 前 60%:框架菁英班直播 → 主題:Next.js 起步走 - 完成課程報到 -> 第五週主線 或 第六週主線 + 心得牆 - 3/9 前 80%:框架菁英班錄影(目前以 Vue 為主,React 可參考主題包含:從零建構 API、Cloud flare、前端工程師如何寫出好履歷) - 完成課程報到 -> 第七週主線 或 第六週主線 + 心得牆 - 100%:數位完賽獎狀 目前第四週已經有超過百人完成,但請記得完成 “報到任務” 喔~ 心得牆任務:https://rpg.hexschool.com/#/training/12062543649513962870/board/content/12062543649513962881_12062543649513962910 心得牆素材:https://hackmd.io/OcMERTDpT86G-5PS8NDVjg ## 提醒:作業繳交說明 - 所有繳交期限 3/23 - **最終延長繳交條件**:3/23 前完整繳交(沒有被直接退件),可延長批改至 4/13 - 直接退件的原因: - 助教下載後,`npm run dev` 無法運行 - 作品不完整(作品內容有缺、有大量假字) 主要介紹: - 為什麼要跨元件狀態 - 跨元件計數器 - Redux toolkit - Redux 非同步處理方式 - Redux toolkit 專案管理方式 ## 跨元件狀態 遇到的問題: - 狀態都要由父層處理(透過 Props),專案一大會顯得混亂 - 非同步處理時,也會在父層加入大量的程式碼 - 如果資料操作複雜時,複層會更顯得混亂 - useContext 在大型專案管理會更加複雜 ## 什麼是 Redux Toolkit **名詞介紹:Store** - **Slice:** 一個用儲存狀態的容器。 - **Store: 全域的狀態儲存**,用於保存所有相關的 Slice 的狀態。 - **Provider:** 用於將 Store 的數據傳遞給其他元件 - **State**: 狀態本身 - **Action:** 動作或操作,用於觸發 Reducer 更新 State - **Reducer:** 處理 Actions 並更新 State 的函式。每個 Reducer 接收兩個參數:Action 和當前 State,然後返回新的 State **元件方法** - **useSelector:** 用於在元件中存取特定的 State - **useDispatch:** 用於在元件中觸發 actions,進行State更新。透過 useDispatch 可以直接呼叫 Store 的更新方法。 ![空白](https://hackmd.io/_uploads/S1yxoh2YJx.png) ## 從零建構 Redux **一、安裝環境** https://redux-toolkit.js.org/introduction/getting-started#an-existing-app ```jsx npm install @reduxjs/toolkit npm install react-redux ``` **二、建立第一個 Slice(store 中一定需要 slice)** 路徑: `src/slice/counterSlice.js` - name, initialState 是必要的屬性 - 匯出該 Slice 的 reducer(這是固定命名 ```jsx import { createSlice } from "@reduxjs/toolkit"; export const counterSlice = createSlice({ name: "counter", initialState: { count: 0 }, }); export default counterSlice.reducer; ``` > 小型的資料模組,可以用來管理資料、操作資料的方法等 > **三、建立 Store(管理 Slice 使用)** 路徑: `src/store.js` 1. 匯入 configureStore 的方法,並用它來建立 store 2. 從 Slice 中匯入 reducer ```jsx import { configureStore } from "@reduxjs/toolkit"; import counterReducer from "./slice/counterSlice"; export const store = configureStore({ reducer: { // 必要加入 reducer counter: counterReducer } }); ``` > 這裡的 reducer 算是一個集合管理器,將 slice 匯入後統一進行管理 > **四、在專案中加入 RTK** 檔案:`main.jsx` - 需要匯入 store, Provider - 將 Provider 包在 App 或是 RouterProvider 外層(看有沒有使用 ReactRouter ```jsx import { store } from './store' import { Provider } from 'react-redux' createRoot(document.getElementById('root')).render( <Provider store={store}> <RouterProvider router={router} /> </Provider> ) ``` > 將 store 整個加入至專案中,讓內層所有的元件都能存取 store 所管理的資料 > **五、將數據從 store 中取得** - 匯入 useSelector 方法 - 定義資料,並從 store 中取得 ```jsx import { useSelector } from 'react-redux'; function CounterRender() { const count = useSelector((state) => { console.log(state); // 可以看到全部的 store 數據 return state.counter.count }); return (<div>RTK: {count}</div> ); } ``` > useSelector 就像是筷子,直接從 slice 存取你的資料 且當狀態更新時,useSelector 可以協助刷新元件 > > 到這個步驟,基礎環境已經建立完成 > **六、建立 Actions** 路徑: `src/slice/counterSlice.js` ```jsx import { createSlice } from "@reduxjs/toolkit"; export const counterSlice = createSlice({ // ... reducers: { // 新增一個 reducers 物件,並添加方法 addCount: (state) => { state.count += 1; } } }); export const { addCount } = counterSlice.actions; // 從 slice 中的 reducers 中匯出該方法 ``` > 這個 reducers 跟外層有點不同,可以理解成更新當前 slice 的方法集 > **七、匯入 actions** 兩個方法皆可運作 1. 直接使用 `dispatch({type: {reducerName} / {actionName}})` ```jsx import { useDispatch } from 'react-redux'; function CounterBtn() { const addCountByClick = () => { **dispatch({ type: 'counter/addCount' });** }; return ( <button onClick={() => addCountByClick()}>RTK:點我加一 {count}</button> ); } ``` 2. 從 slice 帶入 action 方法,直接呼叫他 `dispatch({action()})` ```jsx import { useDispatch } from 'react-redux'; import { addCount } from '../slice/counterSlice'; function CounterBtn() { const addCountByClick = () => { **dispatch(addCount());** }; return ( <button onClick={() => addCountByClick()}>RTK:點我加一 {count}</button> ); } ``` > 第二種方式比較不會拼錯 > **八、傳入參數** 1. 在 slice 中建構方法,並在 action 中取得參數 action.payload 中,可以找到傳遞的參數,在此可以先透過 console 檢視 另外新建立的方法,也一樣需要透過 export 進行匯出 ```jsx export const counterSlice = createSlice({ // ... reducers: { addCountByAmount: (state, action) => { console.log('addCountByAmount', action); state.count += action.payload; } } }); export const { addCount, addCountByAmount } = counterSlice.actions; ``` 1. 元件中,透過以下兩種方式擇一觸發 ```jsx // 直接傳遞 payload dispatch({ type: 'counter/addCountByAmount', payload: 5 }); // 透過呼叫 action 加入參數 dispatch(addCountByAmount(5)); ``` > 很好,再來建構一次相減的方法 > 流程複習: 1. 在 slice 中建構相對應 reducer → **建構 reducer** 2. 元件內匯入特定方法 → **匯入方法** 3. 呼叫、測試運作是否正確 → **呼叫、測試** ## useSelector 簡化方法 如果不想要每次 useSelector 都寫很長的 callback function,那麼可以… 1. 在 slice 檔案,就先寫好 callback function ```jsx export const selectCount = (state) => state.counter.count; ``` 2. 匯入使用(這個結果與前者相同 ```jsx const count = useSelector(selectCount); ``` 3. 你也可以在此做複雜的處理(單一值為主) ```jsx // slice.js export const selectCount2 = (state) => state.counter.count * 2; // component const count2 = useSelector(selectCount2); ``` 如果匯出資料,需要整合多個 State… ## Redux 與非同步 - reducers **本身僅能處理同步的狀態** - 如果在 reducers 的方法加入非同步事件,如 setTimeout 或 ajax,則會出現 `*Cannot perform 'get' on a proxy that has been revoked` 錯誤* #### 解決方案一:createAsyncThunk 使用另一個方法 createAsyncThunk 處理完非同步後,在交回給 reducers 處理 1. 建構 createAsyncThunk 方法 參數包含: createAsyncThunk(自定義名稱, 非同步函式(值, 參數)) 名稱通常使用 `sliceName/actionName` 的格式,且需要是唯一的 ```jsx export const addAsyncCountByCreateAsyncThunk = createAsyncThunk( 'counter/addAsyncCountByCreateAsyncThunk', // params 中包含 dispatch 等方法 async (amount, params) => { // (傳入的值, asyncThunk參數) console.log('addAsyncCount:', amount, params); setTimeout(() => { params.dispatch(addCountByAmount(amount)); }, 1000); }, ); ``` 2. 呼叫該方法 ```jsx import { addAsyncCountByCreateAsyncThunk } from '../slice/counterSlice'; 元件中... addAsyncCountByCreateAsyncThunk(...) ``` 優化: 1. 使用常數命名 2. 解構出 dispatch 做使用 ```jsx export const ADD_COUNT_BY_ASYNC = createAsyncThunk( 'counter/ADD_COUNT_BY_ASYNC', async (amount, {dispatch}) => { setTimeout(() => { dispatch(addCountByAmount(amount)); }, 1000); }, ); ``` #### 解決方案二:在元件裡處理完非同步 就...,不要在 Redux 中處理非同步 ## 透過 JSON PlaceHolder 練習 Redux 1. 從零建構一個新的 slice 2. 了解在目前流程中,會有哪些? 1. 控制資料的行為 → Reducers 2. API 行為 → createAsyncThunk ## 本週額外挑戰 - 作業連結:https://rpg.hexschool.com/#/training/12062543649513962870/board/content/12062543649513962871_12062543649513962892?tid=12062543649528575021 - 助教作業解說(週日上午 10:00) ![image](https://hackmd.io/_uploads/BkXeKpsSke.png) - 團隊任務: - https://rpg.hexschool.com/#/training/12062543649513962870/board/content/12062543649513962876_12062543649513962913