Django + React + Redux 實戰練習 - 01

Built functional member management page with Django + React + Redux

Posted by Young on 2022-11-21
Estimated Reading Time 6 Minutes
Words 1.4k In Total
Viewed Times

建立 Constants

constants 資料夾中建立對應 userConstants.js,並寫好對應的 constants

1
2
3
4
5
.....................
export const USER_UPDATE_PROFILE_REQUEST = "USER_UPDATE_PROFILE_REQUEST";
export const USER_UPDATE_PROFILE_SUCCESS = "USER_UPDATE_PROFILE_SUCCESS";
export const USER_UPDATE_PROFILE_FAIL = "USER_UPDATE_PROFILE_FAIL";
export const USER_UPDATE_PROFILE_RESET = "USER_UPDATE_PROFILE_RESET";

撰寫 Reducer

reducers 資料夾中建立對應 userReducers.js,並撰寫對應的 reducer,並將剛剛寫好的 constants import 進來:

1
2
3
4
5
6
7
import {
...........
USER_UPDATE_PROFILE_REQUEST,
USER_UPDATE_PROFILE_SUCCESS,
USER_UPDATE_PROFILE_FAIL,
USER_UPDATE_PROFILE_RESET,
} from "../constants/userConstants.js";

接著撰寫對應的 reducer function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export const userUpdateProfileReducer = (state = {}, action) => {
switch (action.type) {
case USER_UPDATE_PROFILE_REQUEST:
return { loading: true };
case USER_UPDATE_PROFILE_SUCCESS:
return { loading: false, success: true, userInfo: action.payload };
case USER_UPDATE_PROFILE_FAIL:
return { loading: false, error: action.payload };
case USER_UPDATE_PROFILE_RESET:
return {};
default:
return state;
}
};
  • 在 case USER_UPDATE_PROFILE_SUCCESS 多一個 success 物件是為了到時候在 ProfilePage.js output 判斷是否成功更新 user profile,並顯示對應的訊息
  • USER_UPDATE_PROFILE_RESET 這邊是為了在 ProfilePage.js 中的 useEffect 中,當 user 更新成功後就清空 state 中 userUpdateProfile 的資料

在 store.js 中加入 reducer

一樣將剛剛寫好的 reducer import 進來,並將其加入 store 常數中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import {
............
userUpdateProfileReducer,
} from "./reducers/userReducers";
.....................
const store = configureStore({
reducer: {
// 儲存多個state的地方,到時會再各個component用useSelector指定要使用哪一個
...........................
userDetails: userDetailsReducer,
userUpdateProfile: userUpdateProfileReducer,
},
preloadedState: initialState,
middleware: middleware,
});

建立對應 Action

actions 資料夾中建立對應 userActions.js,並撰寫對應的 action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
export const updateUserProfile = (user) => async (dispatch, getState) => {
// getState 大多都是在需要訪問 store 的裡的數據以做一些操作時使用
try {
dispatch({
type: USER_UPDATE_PROFILE_REQUEST,
});

const {
userLogin: { userInfo },
} = getState(); // 確保 User 是登入的狀態

const config = {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${userInfo.token}`, // 需要有 token 才能取得資料,因為後端有設 permission_classes
},
};
const { data } = await axios.put(
`/api/users/profile/update/`,
user, // 這邊的 user 是從 ProfilePage.js 傳過來的
config
);

dispatch({
type: USER_UPDATE_PROFILE_SUCCESS,
payload: data, // payload 是後端回傳的資料
});
// 當更新成功後,要讓使用者也用新的資料登入,所以要更新 localStorage
dispatch({
type: USER_LOGIN_SUCCESS,
payload: data,
});

localStorage.setItem("userInfo", JSON.stringify(data)); // 將登入資訊存到 localStorage
} catch (error) {
dispatch({
type: USER_UPDATE_PROFILE_FAIL,
payload:
error.response && error.response.data.message
? error.response.data.message
: error.message,
});
}
};

更新完後應該要讓用戶在自動登入,所以要才要再 dispatch USER_LOGIN_SUCCESS 以及更新 localStorage 中的 userInfo

在 ProfilePage.js 中使用

ProfilePage.js 中,將剛剛寫好的 action import 進來,並用 useDispatch 取得 dispatchuseSelector 取得對應的 state

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { useState, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import { getUserDetails, updateUserProfile } from "../actions/userActions";
..........................
const dispatch = useDispatch();

const userDetails = useSelector((state) => state.userDetails);
const { loading, error, user } = userDetails;

const userLogin = useSelector((state) => state.userLogin);
const { userInfo } = userLogin;

const userUpdateProfile = useSelector((state) => state.userUpdateProfile);
const { success } = userUpdateProfile; // 從userUpdateProfile取出success,為了得知這個 action 是否成功

然後用 useEffect 去判斷是否有登入,沒有登入就跳轉到登入頁面,有登入就去取得 user 的資料,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
useEffect(() => {
if (!userInfo) {
// 如果沒有登入就跳轉到登入頁面
navigate("/login");
} else {
if (!user || !user.first_name || success) {
dispatch({ type: USER_UPDATE_PROFILE_RESET }); // 若更新成功就重置 userUpdateProfile 的狀態
dispatch(getUserDetails("profile"));
} else {
setFirst_name(user.first_name);
setEmail(user.email);
}
}
}, [dispatch, navigate, userInfo, user, success]);

在 ProfilePage.js 中加入更新 user profile 的表單

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<Form onSubmit={submitHandler}>
<Form.Group controlId="first_name">
<Form.Label>ID</Form.Label>
<Form.Control
required
type="name"
placeholder="請輸入您的ID"
value={first_name}
onChange={(e) => setFirst_name(e.target.value)}
></Form.Control>
</Form.Group>
...................................
<Button type="submit" variant="primary">
更新
</Button>
</Form>

submitHandler 事件處理函數

setMessage 是用來設定顯示在表單最上方的訊息,若密碼不一樣就會顯示錯誤訊息,若沒問題就會 dispatch updateUserProfile 這個 action:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const submitHandler = (e) => {
e.preventDefault();

if (password !== confirmPassword) {
setMessage({ text: "密碼不一樣啦!!", variant: "danger" });
} else {
dispatch(
updateUserProfile({
id: user.id,
first_name: first_name,
email: email,
password: password,
})
);
setMessage({ text: "更新成功!", variant: "success" });
}
};

表單檢查沒問題後,就把更新後的資料回傳給 updateUserProfile 這個 action 的 user 參數。

成功畫面

最後應可以直接看到成功更新後的資料顯示在原本的欄位上,不需要重整畫面,且在 Redux DevTools 中也能看到 USER_UPDATE_PROFILE_SUCCESS 的 action 有被觸發,而 USER_DETAILS_REQUEST 也有被觸發,因為在更新成功後,會再去取得 user 的資料,並更新到 userDetails 的 state 中。

update_success

在 localStorage 中也能看到更新後的 userInfo

localstorage_change

常見問題

更新後成功觸發 dispatch,卻出現 401 (Unauthorized) 錯誤

這個往往是因為請求缺乏必要的授權憑證,大部分都是因為 token 沒有帶上的問題。

檢查步驟:

  1. 對應的 action 中,檢查是否有帶上 token
  2. 檢查後端 Django 處理更新會員資料的 views.py 看看,是否有設定 permission_classes
  3. 檢查後端 Django 的 serializers.py 中,是否在將 UserSerializerWithToken 函示中,存取正確的token,應該要 return access_token,而不是 refresh_token
1
2
3
def get_token(self,obj):
token = RefreshToken.for_user(obj) # 當使用者註冊或重設密碼,就會產生新的token
return str(token.access_token) # 因為token是物件,所以要轉成字串

要記得要用調用對的 serializer,不然會出現 token 不存在的錯誤。例如這裡要用 UserSerializerWithToken,而不是 UserSerializer

1
2
3
4
5
6
7
8
@api_view(['PUT']) # 更新自己的 profile
@permission_classes([IsAuthenticated]) # 只有登入的人才能 access
def updateUserProfile(request):
user = request.user
serializer = UserSerializerWithToken(user,many=False) # 記得這邊要用 UserSerializerWithToken 而不是 UserSerializer,因為要產生新的token
.....................
user.save()
return Response(serializer.data)

若您覺得這篇文章對您有幫助,歡迎分享出去讓更多人看到⊂◉‿◉つ~


留言版