這是react超推薦學習的管理套件,在很多重複頁面都會用到的狀態上,非常好管理,可以大大減少維護的成本,以及提高程式效率,只是剛開始學習會花一些時間理解他的整體架構,需要一點耐心了解
※ps整篇文章依照布魯斯的 TypeScript + React 全攻略|快速上手仿 Instagram UI的課程學習,範例也是依照課程的,所以文章敘述都是單獨部分的觀念,快速架設Redux參考下面文章
內容目錄
Redux運作基本概念
類似郵差(Dispatch)帶著包裹(Action)送給收信、處理者(reducer)
reducer | 是一個switch case的function, 處理action帶來的邏輯,更新state |
Action | 是描述動作,帶了什麼資料,是物件。EX:取錢、並取10元 |
Dispatch | 派發action給reducer的人 |
Store | 包含Reducer跟State,通知UI要重新渲染 |

安裝Redux
來到官網,可以看到有三個工具

Redux | 核心的函式庫、工具 |
React-Redux | 透過提供的API(Hook)去將Redux連結我們APP(react組件),是React跟Redux的橋樑 |
Redux-Toolkit | 是讓整個寫法更簡潔,在沒有這套工具的時候一種狀態會分為reducer、Action、Dispatch三個部份三個檔案,所以這是一套檔案管理工具 |
然後點擊Get Started,右邊有Installation,安裝Redux Toolkit,那這裡面已經包含Redux Core,所以Redux Core就不用再安裝
npm install @reduxjs/toolkit

再來安裝react-redux跟redux-devtools,這個devtools是一個可以從chrome的插件去管理redux狀態的一個工具
npm install @types/react-redux
npm install --save-dev @redux-devtools/core

google搜尋Redux DevTools,就可以到chrome的線上應用程式找到這個插件

安裝後就可以從開發者工具裡面,找到Redux,這樣開發的時候就可以查看每個狀態

專案練習
接下來我要透過布魯斯的 TypeScript + React 全攻略|快速上手仿 Instagram UI這堂課提供的版型,練習用redex建立一個TodoList
Slice.ts的建立
- 先到官網的教學文件,快速建立中,將Slice建立起來
- 將官網的例子複製
- 反白的部分需要依照自己需求建立
- 將reducers裡面的邏輯刪除
- 那在建立createSlice的時候,有三個參數
- 參數1:name >取名子
- 參數2:initialState > 建立初始化的state,將反白的那段型態跟內容定義好
- 參數3:reducers > 處理Dispatch傳來的東西
- 建立的function有兩個參數
- 參數1:上面設定的state
- Dispatch傳來的內容payload
- 最後export,把剛剛設定的Slice.actions設定出來,從中取出設好的reducers方法(這些功能會在UI [組件]那裡調用)
- 還有export,預設為Slice.reducer,會在store那邊import進去用

建立Store.ts
- 繼續官網的教學文件,將Store建立起來
- 複製官網的import例子,利用configureStore建立store
- configureStore的參數為一個物件,必須放入reducer
- 再將我們剛剛這設置好的Slice檔案裡的reducer給import進來給reducer
- 再將store給export出去

UI使用Store
將store引入到UI (組件)裡面使用,就先回到源頭index.tsx裡面
- 把react-redux中的Provider給import進來
- 使用Provider把APP組件包住
- 剛設定好的store給import進來
- 設定Provider的屬性store為import來的store
有點類似context的用法,那react-redux這就是用來溝通react跟redux的橋樑,所以要找兩者相通的API就要從這裡找
APP被帶有store屬性的Provider包覆,所以APP所有的子組件都可以調用store設定的狀態
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import { Provider } from "react-redux";
import store from "./store";
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById("root")
);
這邊設定好之後,打開開發者工具選擇redex就可以看到state裡面,有你設定的store裡的state
PS:我練習專案畫面為TypeScript + React 全攻略|快速上手仿 Instagram UI的課程版型

調用Store
剛已經建立了兩者的關聯,那要調用裡面的state(todoList)的話,就要使用API,那redux提供的Hook因為是TypeScript的關係,要先設定一些型態的部分,所以先設定hook.ts的檔案
建立hook.ts
這個檔案完全是為了設定型態而建立,不然useSelector是可以直接取用
- 繼續官網的教學文件,將hook.ts建立起來
- 先將react-redux中的useSelector跟TypedUseSelectorHook給import進來
- useSelector 取得當下store的state(todoList)的內容
- TypedUseSelectorHook是用來設定型態用的API
- 建立一個useAppSelector 給予值就是useSelector,型態就要從Store取得
- 到Store檔案中
- 設定RootState型態,並使用ReturnType把Store設定為型態取出來
- 並將RootState給export出來
- 回到hook.ts檔案中把Store的RootState這個型態import進來


在UI使用State
剛剛設定好hooks之後,接下來只要在Provider包覆之下的組件都可以使用這個API,那就先在APP上面使用看看
先將hook的useAppSelector給import進來
直接使用這個API取的狀態在參數,就可以回傳store裡面的reducer
import { useAppSelector } from "./hook";
function App() {
const todoList = useAppSelector((state) => {
return state.todoReducer;
});
console.log(todoList);
return ( <></>);
}

建立Dispatch
接下來要讓UI觸發 Dispatch發一些action給reducer去更新狀態,所以回到hook檔案把剛剛沒建立的Dispatch這個API給建立好
- 繼續官網的教學文件,將hook.ts的Dispatch建立起來
- useDispatch給import進來
- 一樣賦予型態,那型態也是從Store去取得
- 回到store.ts
- 建立AppDispatch型態,從store裡面取得dispatch的型態,最後export
- 回到hook.ts把AppDispatch從store給import進來
- 建立一個useAppDispatch,從useDispatch設定型態給予


讓UI發起Dispatch
建立好dispatch之後,就要讓UI有發出Dispatch的功用
※官方範例slices的action應該是increment, decrement, incrementByAmount這些,只是我這裡用TypeScript + React 全攻略|快速上手仿 Instagram UI這堂課練習
- 先把Dispatch的hook跟之前在slices定義好的action給import進來
- 宣告dispatch使用useAppDispatch這個hook
- 直接在UI的click上使用dispatch()
- dispatch()參數為之前slices設定好的action方法
import { useAppSelector ,useAppDispatch } from "./hook";
import { addTodo, addTimeStamp } from "./slices/todo";
function App() {
const todoReducer = useAppSelector((state) => {
return state.todoReducer;
});
const todoList = todoReducer.todoList;
const dispatch = useAppDispatch();
return (
<button
onClick={() => {
dispatch(addTimeStamp());
}}
}></button>
);
}
中間層Middleware
從Dispatch到渲染之間,所做的邏輯處理
先找到官網的Understanding Redux裡面的History and Design有找到Middleware
先建立一個檔案middleware.ts
把官網的範例貼上來
※因為是TypeScript的關係,所以把Middlewarek的型態加入進去
import { Middleware } from "redux";
export const logger: Middleware = (store) => (next) => (action) => {
console.log("dispatching", action);
let result = next(action);
console.log("next state", store.getState());
return result;
};

然後在store.ts將middleware給引入
- 把middleware給import進來之後
- 並在reducer下方新增一個middleware:
- 這裡有兩個方法一個直接放陣列後把值丟進去
- 另一個是把funtion寫出來,參數會抓到middleware陣列,並且把我們自訂義的middleware加進去
import { configureStore } from "@reduxjs/toolkit";
import todoReducer from "./slices/todo";
import { logger } from "./middleware";
const store = configureStore({
reducer: {
todoReducer,
},
// middleware:[logger]
middleware: (getMiddleware) => {
return getMiddleware().concat(logger);
},
});
export default store;
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
接下來要解決我們設定的middleware類型報錯問題
回到官網的Using Redux的code Quality裡面的Usage With TypeScript,這個是專門找一些寫TypeScript會遇到的雷區問題
那可以看到右邊有個Type Checking Middleware這個問題區
可以看到,官網這邊是說
- 把Redux 的Middleware跟store的RootState分別import進來之後
- 把自訂義的Middleware function加上型態就可以解決
※但實際上還是有報錯
主要是我們RootState的部分是用Store本身去推斷型態,所以往下看還有其他解法
import { Middleware } from "redux";
import { RootState } from "./store";
export const logger: Middleware<
{}, // Most middleware do not modify the dispatch return value
RootState
> = (store) => (next) => (action) => {
console.log("dispatching", action);
let result = next(action);
console.log("next state", store.getState());
return result;
};

往下移就能看到,使用combineReducers把Reducer集結起來,並且重新給予RootState型態就好
- 先combineReducers從Redux給import進來
- 將reducer給集結起來
- 把reducer的值改為集結後的rootReducer
- 再把RootState改為取得rootReducer的型態
import { configureStore } from "@reduxjs/toolkit";
import todoReducer from "./slices/todo";
import { logger } from "./middleware";
import { combineReducers } from "redux";
const rootReducer = combineReducers({ todoReducer });
const store = configureStore({
reducer: rootReducer,
// middleware:[logger]
middleware: (getMiddleware) => {
return getMiddleware().concat(logger);
},
});
export default store;
export type RootState = ReturnType<typeof rootReducer>;
export type AppDispatch = typeof store.dispatch;

新增多個middleware
那就是在middleware檔案中,依照範例在新增一個function
- 並到store.ts將function給import進來
- 再多新增一個concat即可
export const loggerMyName: Middleware<
{}, // Most middleware do not modify the dispatch return value
RootState
> = (store) => (next) => (action) => {
console.log("dispatching , I am Hank");
let result = next(action);
console.log("dispatching之後 , I am Mary");
return result;
};
store.ts
import { configureStore } from "@reduxjs/toolkit";
import todoReducer from "./slices/todo";
import { logger, loggerMyName } from "./middleware";
import { combineReducers } from "redux";
const rootReducer = combineReducers({ todoReducer });
const store = configureStore({
reducer: rootReducer,
// middleware:[logger]
middleware: (getMiddleware) => {
return getMiddleware().concat(logger).concat(loggerMyName);
},
});
export default store;
export type RootState = ReturnType<typeof rootReducer>;
export type AppDispatch = typeof store.dispatch;

RTA Query
使用Redux Toolkit裡面有個功能是獲取後端的API,以前大家會使用React thunk來獲取API,但如果使用在redux就要寫很多padding、success、error在reducers裡面,透過RTA這套工具可以替我們處理這些狀態,以及融入狀態管理中
先到Redux Toolkit的官網的RTK Query裡面有RTK Query Overview,看到重點是使用createApi跟fetchBaseQuery來獲取需要的資料
- 先建立一個檔案server資料夾裡面放todoAPI.ts
- 將createApi跟fetchBaseQuery給import進來
- 將官網的範例貼上
- 使用https://jsonplaceholder.typicode.com/ 來練習
- 最後將自訂義的hook給export出去
reducerPath | 就是幫我們寫好的reducer內容,並且包含padding、success、error這些功能,只需要callAPI就好,可以自己取名子 |
baseQuery | 這個就是你要抓取的後端資料網址 |
endpoints | 針對這個路徑,抓取底下更多內容。 使用builder的query跟baseUrl去搭配,就可以用最後輸出的hook去call資料 < Pokemon , string > Pokemon回傳資料的類型,這個自己定義 string 是到時候要輸入的網址後面的內容 |
Hook的部分 | 會依照endpoints裡面的設定,取名為useXXXQuery |
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
// import { Pokemon } from "./types";
export const todoApi = createApi({
reducerPath: "todoApi",
baseQuery: fetchBaseQuery({
baseUrl: "https://jsonplaceholder.typicode.com/",
}),
endpoints: (builder) => ({
// getPokemonByName: builder.query<Pokemon, string>({
// query: (name) => `pokemon/${name}`,
todoList: builder.query<any, string>({
query: (id) => `todos/${id}`,
}),
}),
});
export const { useTodoListQuery } = todoApi;

將建立好的API跟store做連結,回到store.ts檔案
先把剛剛建立的檔案所設定的API給import進來
- 放進reducer裡面
- 有建立combineReducers就放進combineReducers裡面
- 拿出剛剛設定reducerPath,對應的值就是他們原本設定好的reducer
- 再來是把middleware給設定好
import { configureStore } from "@reduxjs/toolkit";
import todoReducer from "./slices/todo";
import { logger, loggerMyName } from "./middleware";
import { combineReducers } from "redux";
import { todoApi } from "./sever/todoAPI";
const rootReducer = combineReducers({
todoReducer,
[todoApi.reducerPath]: todoApi.reducer,
});
const store = configureStore({
reducer: rootReducer,
// middleware:[logger]
middleware: (getMiddleware) => {
return (
getMiddleware()
// .concat(logger)
// .concat(loggerMyName)
.concat(todoApi.middleware)
);
},
});
export default store;
export type RootState = ReturnType<typeof rootReducer>;
export type AppDispatch = typeof store.dispatch;

接下來使用設定好的hook,回到APP.tsx
- 把剛剛設定的useTodoListQuery給import進來
- 宣告使用hook來獲取三個原本設定好的方法,分別是資料,發生錯誤,是否載入中
- 最後log出來看看內容是什麼
log的結果就是載入中顯示,true,則載入完畢看到是false,並且獲取到data
import {useTodoListQuery} from "./sever/todoAPI";
function App() {
const { data, error, isLoading } = useTodoListQuery("1");
console.log("data", data);
console.log("error", error);
console.log("isLoading", isLoading);
}


針對非TypeScript 建立Redux
2023/3/21更新
由於上述都是針對課程,是由TypeScript建立React,我想記錄一下用JS建立的Redux,以下範例直接參照官網設置 Quick Start,前面npm安裝的部分就不再贅述
※使用的是React 17版
建立store
- 建立Store檔案
- 使用configureStore來建立stroe,reducer的部分,counterReducer這個檔案等等會建立先引入
檔案位置:app/store.js
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'
export default configureStore({
reducer: {
counter: counterReducer
}
})
建立counterSlice
- 建立Slice檔案,這是要建立剛剛store的reducer
- createSlice來建立,三個參數,分別為name、各狀態、及reducers(修改狀態的方法)
檔案位置:features/counter/counterSlice.js
import { createSlice } from '@reduxjs/toolkit'
export const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0
},
reducers: {
increment: state => {
state.value += 1
},
decrement: state => {
state.value -= 1
},
incrementByAmount: (state, action) => {
state.value += action.payload
}
}
})
// Action creators are generated for each case reducer function
export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
串接置UI
index.js
引入到index.js,並且使用Provider,將store串接起來
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import store from './app/store'
import { Provider } from 'react-redux'
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
Counter.js
- 使用useSelector取得剛設置的狀態,useDispatch傳遞UI指令
- 引入剛剛設置的reducer方法decrement、increment,透過useDispatch來傳遞
檔案位置:features/counter/counterSlice.js
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { decrement, increment } from './counterSlice'
export default function Counter() {
const count = useSelector(state => state.counter.value)
const dispatch = useDispatch()
return (
<div>
<div>
<button
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
Increment
</button>
<span>{count}</span>
<button
aria-label="Decrement value"
onClick={() => dispatch(decrement())}
>
Decrement
</button>
</div>
</div>
)
}
App.js
- 這邊就沒幹嘛,把剛剛設置的Counter組件引入
import logo from './logo.svg';
import './App.css';
import Counter from "./features/counter/Counter"
function App() {
return (
<div className="App">
<Counter></Counter>
</div>
);
}
export default App;
結論
今天持續學習布魯斯的 TypeScript + React 全攻略|快速上手仿 Instagram UI,在學習Redux我是花比較多時間的,由於他分的步驟相對較多,必須很清楚知道每個步驟都在幹嘛,他又分別拆分為三個套件,在理解整體架構之後,才會完全了解三個套件分別在什麼時間點被使用,只能說透過課程學習減少了我自己學習的踩雷時間,這個紀錄依照學習課程記錄下來,單單從文章內很難架設完整(依照課程而非官網程式碼),但可以知道每個程式碼的觀念,而不是單純抄下官網的範例而已,要知道完整架設流程,可以參考下面這篇,是寫給我自己以後快速架設理解用的文章
