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 連結貼至底下回報就算完成了喔!
解答位置請參考下圖(需打開程式碼的部分觀看)

<!-- 解答:
```
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]() |
```