Redux 是一個開源的 JavaScript 狀態管理庫,通常與 React 或其他 JavaScript 框架一起使用,來管理應用程式的狀態。它最初是由 Dan Abramov 和 Andrew Clark 於 2015 年開發的,並迅速成為處理應用程式狀態管理的一個熱門工具。Redux 的設計理念是將應用程式的所有狀態儲存在一個單一的、不可變的數據結構中,稱為 store。 另外要注意,如果要在 React 中使用 Redux,必須搭配 react-redux 這個套件。React-Redux 是專為 React 開發的綁定庫,負責在 React 應用中更方便連接 Redux 的 store,並處理訂閱和狀態更新。 ## Redux 的核心概念 ### Store (儲存): Redux 將應用程式的所有狀態儲存在一個全局的 store 中。這個 store 是一個單一的 JavaScript 對象,包含了應用程式的所有狀態。每次更新狀態時,都會創建一個新的 store,而不是直接修改原有的 store。Redux store 通常會存儲應用程式的所有數據(例如用戶資訊、UI 狀態等)。 ### Action (動作): Action 是一個普通的 JavaScript 對象,表示一個操作,通常包含類型 (type) 和可選的負載 (payload)。 每當我們希望更新 store 中的狀態時,就會派發 (dispatch) 一個 action。Action 必須有一個 type 屬性來描述操作的類型(例如 'INCREMENT'、'DECREMENT' 等)。 ```js const incrementAction = { type: 'INCREMENT', payload: 1 }; ``` ### Reducer (處理器): Reducer 是一個純函數,它決定了如何根據收到的 action 更新 store 中的狀態。每個 reducer 都會接收兩個參數:當前的 state 和 action。根據 action 的類型,reducer 會返回一個新的 state。 在 Redux 中,狀態的更新是不可變的,因此每次更新都會返回一個新的對象。 ```js function counterReducer(state = { count: 0 }, action) { switch (action.type) { case 'INCREMENT': return { ...state, count: state.count + action.payload }; case 'DECREMENT': return { ...state, count: state.count - action.payload }; default: return state; } } ``` ### Dispatch (派發): dispatch 是用來發送 action 到 store 的方法。每當我們想要改變 store 中的狀態時,必須調用 dispatch 來發送 action。 當 action 被發送後,reducer 會根據 action 類型來更新 store。 ```js store.dispatch(incrementAction); ``` ### State (狀態): Redux 的核心理念之一是應用程式的狀態應該是可預測的。所有狀態保存在 Redux 的 store 中,並且可以通過 getState 方法讀取當前狀態。 ```js const currentState = store.getState(); ``` ## Redux 工作流程 1. 應用程式啟動時:Redux store 初始化,並包含預設狀態。 2. 使用者操作:當使用者觸發某個事件(例如點擊按鈕),就會發送一個 action,並通過 dispatch 傳遞這個 action。 3. Reducer 處理 Action:根據 action 的 type,reducer 會更新 state 並返回一個新的 state。 4. 更新 UI:React 等前端框架會根據最新的狀態更新 UI。通常會使用 connect 或 useSelector 來訂閱 Redux store 的變更,並將狀態傳遞到組件中。 ## Redux Toolkit redux Toolkit 是官方推薦的 Redux 開發工具,旨在簡化 Redux 的使用方式,減少樣板代碼,提高開發效率。相比於傳統的 Redux,Redux Toolkit 提供了一些功能: 1. 簡化配置:使用 configureStore 來輕鬆設置 store,內建了 Redux DevTools 支援。 2. 免手動撰寫樣板代碼:createSlice 可以同時生成 reducer 和 actions。 3. 內建異步工具:支持 createAsyncThunk 簡化異步邏輯的處理。 4. 內建中介軟體:內建了 redux-thunk,可以直接處理異步操作。 在 Redux Toolkit 中,createSlice 是最常用的 API。 ```js // src/store/counterSlice.js import { createSlice } from '@reduxjs/toolkit'; const counterSlice = createSlice({ name: 'counter', initialState: { count: 0 }, reducers: { increment: (state) => { state.count += 1; }, decrement: (state) => { state.count -= 1; }, incrementByAmount: (state, action) => { state.count += action.payload; } } }); export const { increment, decrement, incrementByAmount } = counterSlice.actions; export default counterSlice.reducer; ``` 用 configureStore 來建立 store,並加入 reducer。 ```js // src/store/index.js import { configureStore } from '@reduxjs/toolkit'; import counterReducer from './counterSlice'; const store = configureStore({ reducer: { counter: counterReducer } }); export default store; ``` 將 Redux store 注入到 React 應用中。 ```js // src/index.js import React from 'react'; import ReactDOM from 'react-dom/client'; import { Provider } from 'react-redux'; import store from './store'; import App from './App'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <Provider store={store}> <App /> </Provider> ); ``` 使用 useSelector 讀取狀態,useDispatch 發送 action。 ```js // src/App.js import React from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { increment, decrement, incrementByAmount } from './store/counterSlice'; const App = () => { const count = useSelector((state) => state.counter.count); const dispatch = useDispatch(); return ( <div style={{ textAlign: 'center' }}> <h1>Redux Toolkit 範例</h1> <p>計數器: {count}</p> <button onClick={() => dispatch(increment())}>增加</button> <button onClick={() => dispatch(decrement())}>減少</button> <button onClick={() => dispatch(incrementByAmount(5))}>增加 5</button> </div> ); }; export default App; ``` ### 異步操作範例 假如我們需要獲取一個 API 數據,createAsyncThunk 提供了簡化的方式: ```js // src/store/userSlice.js import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; // 定義異步邏輯 export const fetchUser = createAsyncThunk('user/fetchUser', async (userId) => { const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`); return response.json(); }); const userSlice = createSlice({ name: 'user', initialState: { data: null, loading: false, error: null }, reducers: {}, extraReducers: (builder) => { builder .addCase(fetchUser.pending, (state) => { state.loading = true; }) .addCase(fetchUser.fulfilled, (state, action) => { state.loading = false; state.data = action.payload; }) .addCase(fetchUser.rejected, (state, action) => { state.loading = false; state.error = action.error.message; }); } }); export default userSlice.reducer; ``` ## 題目 請複製 (右下角 fork)這個[範例](https://codepen.io/yen-kg/pen/yLmWeBb?editors=1011),利用 Redux 的方式替按鈕加上 +1 跟 -1 的功能。 ## 回報流程 將答案寫在 CodePen 並複製 CodePen 連結貼至底下回報就算完成了喔! 解答位置請參考下圖(需打開程式碼的部分觀看) ![](https://i.imgur.com/vftL5i0.png) <!-- 解答: ``` const { createStore } = Redux; const initialState = { count: 0 }; function counterReducer(state = initialState, action) { switch (action.type) { case 'INCREMENT': return { ...state, count: state.count + 1 }; case 'DECREMENT': return { ...state, count: state.count - 1 }; default: return state; } } const store = createStore(counterReducer); const App = () => { const [count, setCount] = React.useState(store.getState().count); React.useEffect(() => { const unsubscribe = store.subscribe(() => { setCount(store.getState().count); }); return () => unsubscribe(); }, []); const increment = () => store.dispatch({ type: 'INCREMENT' }); const decrement = () => store.dispatch({ type: 'DECREMENT' }); return ( <div> <h1>React + Redux 範例</h1> <p>計數器: {count}</p> <button onClick={increment}>增加</button> <button onClick={decrement}>減少</button> </div> ); }; const root = ReactDOM.createRoot(document.getElementById('root')); root.render(<App />); ``` --> 回報區 --- | # | Discord | CodePen / 答案 | |:---:|:-------------- |:----------------------------------------------------------------------------- | | 1 | Aaron 謝宗佑 | [Codepen](https://codepen.io/aaron-hsieh/pen/azbzXwX) | | 2 | 4chan | [Codepen](https://codepen.io/ijuolaqc-the-looper/pen/gbObqZm) | |3|Hailey|[CodePen](https://codepen.io/sxbokfja-the-flexboxer/pen/pvovGBO?editors=1111)| |4|邵|[Codepen](https://codepen.io/ukscrlno-the-typescripter/pen/GgRgeqM?editors=1011)| | 5 | 毛巾 |[Codepen](https://codepen.io/bqdcjboa-the-solid/pen/WbNbmqq)| | 6 | Noy(Toad) | [Codepen](https://codepen.io/MochiCodingPen/pen/YPzzVVW?editors=1011) | | 7 | Sonia | [Codepen](https://codepen.io/YUJOU/pen/dPyoPWK) | | 8 | Johnson | [Codepen](https://codepen.io/crpbugqy-the-typescripter/pen/zxYGXPZ) | | 9 | 嚼勁先生 | [Codepen](https://codepen.io/James520284/pen/pvojyww) | | 10 | 泊岸 | [Codepen](https://codepen.io/qoq77416416/pen/QwWjdOx?editors=1011) | | 11 | Kaya | [Codepen](https://codepen.io/kayaribi/pen/zxYvNLO) | | 12 | 姜承 | [Codepen](https://codepen.io/Troy0718/pen/ZYEMgYZ?editors=0011) | | 13 | jinliu214 | [Codepen](https://codepen.io/jinliu214/pen/YPXPMBL?editors=0010) | | 14 | chris | [Codepen](https://codepen.io/chris-chen-the-selector/pen/ogXOwEb?editors=1011) | | 15 | shin_kai | [Codepen](https://codepen.io/KAI-SHIN-the-animator/pen/LEGWdrv) | --- - 快速複製格式: ```markdown! | 0 | user | [Codepen]() | ```