[React] React with Redux
這篇文章主要來認識 redux,首先我們要知道redux是一種設計來儲存 JavaScript 的狀態的工具。在 React.js 的寫作中,我之前較常使用的是 contextAPI,也是用來儲存資料的狀態,而他們倆者之間主要有兩個差別:
- accessibility of the data(資料的取用): context 可以僅包裹內層的元件,像是 Categories context 只有關於 shop 的元件們需要使用,就只要包裹住他們就好;redux store 會將整個 app 包裹住。
- data flow: In Redux, there's single store and single dispatch action, every reducers will receive action, but only the needed reducers will fire.
npm 套件
安裝所需的 npm 套件,這邊使用基本的 redux 居多,所以沒有用到@reduxjs/toolkit,未來 可能會再透過其他文章學習與紀錄
npm install redux react-redux redux-logger
boilerplate 模板
在使用 redux 之前,我們要先建立一些模板,像是資料站(store),跟資料儲存操作的地方(reducer)
create a new store folder and store.js
import { compose, createStore, applyMiddleware } from "redux";
import logger from "redux-logger";
// root-reducer 下面才要建立,會放入不同類型的reducer
create root-reducer.js
import { combineReducers } from "redux";
export const rootReducer = combineReducers({});
reducer 資料物件
create user/user.reducer.js,這是我們真正的 reducer,可以直接從user.context複製 state 過來,再做一點點變化。
原本的user.context.jsx
export const USER_ACTION_TYPES = {
SET_CURRENT_USER: "SET_CURRENT_USER",
}; // 這邊可以 提出放在`user.type.js`
const INITIAL_STATE = {
currentUser: null,
};
const userReducer = (state, action) => {
const { type, payload } = action;
switch (type) {
case USER_ACTION_TYPES.SET_CURRENT_USER:
return {
...state,
currentUser: payload,
};
default:
throw new Error(`Unhandled type ${type} in userReducer`);
}
};
改變後的user.reducer.js,重要的是every action passes to every single reducer,代表說如果沒有相對應的 type,每個 reducer 都要回傳前一個 state。
export const USER_ACTION_TYPES = {
SET_CURRENT_USER: "SET_CURRENT_USER",
};
const INITIAL_STATE = {
currentUser: null,
};
export const userReducer = (state = INITIAL_STATE, action) => {
const { type, payload } = action; // 因為我們沒有`useReducer` hook,所以我們要將state預設為INITIAL_STATE。
switch (type) {
case USER_ACTION_TYPES.SET_CURRENT_USER:
return {
...state,
currentUser: payload,
};
default:
return state; // 因為在redux中,所有的reducer都會接收到action,當action不是自己負責的時候,要回傳redux store的state。
}
};
接著將剛剛製作好的userReducer放進root-reducer.js
import { combineReducers } from "redux";
import { userReducer } from "./user/user.reducer";
export const rootReducer = combineReducers({
user: userReducer,
});
store 資料站
將 reducer 的狀態放入store.js之中,加入logger的 midderware,會在 redux 狀態改變時,在 console 印出來資料的狀態
import { rootReducer } from "./root-reducer";
const middleWares = [logger]; // middleware like little library helpers that run before action hits the reducer.
const composedEnhancers = compose(applyMiddleware(...middleWares));
export const store = createStore(rootReducer, undefined, composedEnhancers); // createStore(reducer, [preloadedState], [enhancer])
add store into our application through providers
import { Provider } from "react-redux";
import { store } from "./store/store";
render(<Provider store={store}>...components</Provider>);
dispatch action 處理資料
接著,我們要處理 action 跟 dispatch,如何接收到正確的 type、資料處理、回傳改變後的狀態,也就是之前的UserProvider做的事情。
我們決定要將這個處理的邏輯放在App.js,因為 user 對一個 app 來說是很重要的,我們需要在 app 的最頂層就知道 user 登入與否。
import { useEffect } from "react";
import { useDispatch } from "react-redux";
import {
onAuthStateChangedListener,
createUserDocumentFromAuth,
} from "./utils/firebase/firebase.utils";
import { setCurrentUser } from "./store/user/user.action"; // 我們將setCurrentUser分開到不同的檔案,讓每個元件都可以使用到
const App = () => {
const dispatch = useDispatch(); // dispatch action object
useEffect(() => {
const unsubscribe = onAuthStateChangedListener((user) => {
if (user) {
createUserDocumentFromAuth(user);
}
dispatch(setCurrentUser(user));
});
return unsubscribe;
}, []);
return (
...Routes and UI
)
}
我們將更新使用者的 action 放在user.action.js,createAction 會回傳一個物件,包含 type 跟 payload,這邊的 payload 會是 user(parameter)
import { USER_ACTION_TYPES } from "./user.type";
import { createAction } from "../../utils/reducer/reducer.utils"; // createAction = (type, payload) => ({ type, payload });
export const setCurrentUser = (user) => {
return createAction(USER_ACTION_TYPES.SET_CURRENT_USER, user);
};
selector 取出資料
當我們要將 state 從 redux store 拿出來時,要使用useSelector,我們看到navigation.component.jsx
import { useSelector } from "react-redux";
const currentUser = useSelector((state) => state.user.currentUser); // get state and return the nested value
我們可以透過user.selector.js來分割出來這個選取 state 的邏輯
export const selectCurrentUser = (state) => state.user.currentUser;
然後我們看回navigation.component.jsx
import { selectCurrentUser } from "../../store/user/user.selecor";
const currentUser = useSelector(selectCurrentUser);
如此一來,就可以順利取得 user 的狀態了。
結論
這篇文章介紹了基本的 redux 操作,而實際上,redux 的資料管理會更加複雜。在大型的 JavaScript 應用程式,大多都會使用 redux,redux 雖然一開始的設置有些麻煩,依照我上的網路課程,每個類別都會有四種 javascript 檔案,
- reducer.js 資料物件
- selector 資料的操作、取出
- action.js 資料的操作類型
- type.js 使得資料操作類型較不容易出錯的物件(optional)
https://redux.js.org/tutorials/fundamentals/part-3-state-actions-reducers
這樣的操作慣例,雖然需要先建立一定的模板,但是實際在擴大應用程式時,比較不容易出錯,因此,才會成為主流的資料狀態的控制工具。