本文為 [FE302] 影片中 「React 實戰篇 - 部落格」的學習筆記,記錄實作一個 SPA 的部落格的過程,利用 react router 做出內含有「首頁顯示所有文章」、「點擊標題顯示單篇文章」、「登入」、「登出」等功能
零、react router 的前置作業
1. 安裝 $npm install react-router-dom
2. 引入 import { HashRouter as Router, Routes, Route } from "react-router-dom";
3. 記得只要是連結都要加上 to="/path"
4. 實作過程通常是:串 API -> 刻畫面 -> 連接兩者 -> debug
5. 以下內容包含但不限於實作過程中的檢討、錯誤訊息們及怎麼解決
6. 大型專案可參考部落格的架構
7. [FE302] React 實戰篇 - 部落格之部落格實戰總結 5'26",有提到登出登入的閃一下問題的優化方向
8. 在這個部落格實戰裡面,示範沒有包含表單驗證、loading 狀態,這些在比較完整的 app 裡面還是必要的,有時間可以補上去
一、做部落格之前,先來認識 react router
React Router 背後的原理:HTML5 裡面的 history API 管理頁面路徑
課程當時版本是 v5: React Router
現在官方更新到 v6: Upgrade to React Router v6
1. React-Router 怎麼使用?
a. 在相對應資料夾底下安裝 $npm install react-router-dom
b. Router 有兩種:BrowserRouter、HashRouter
BrowserRouter:
假設網址是 xxx/about,在瀏覽器發送請求時,會去找 about 裡面的 index.html 試著載入,但實際上在不換頁的情況下,不會有 about 這個資料夾,裡面自然就沒有東西可以回傳,會顯示錯誤,要解決這個問題,可以用 HashRouter
HashRouter:
假設網址是 xxx/#/about,不管怎麼換頁,對瀏覽器來說,都是在找 xxx 這個頁面,因為 # 代表著 xxx 頁面的某一個部分
前後端路由的差別,可以參考 淺談新手在學習 SPA 時的常見問題:以 Router 為例
2. 以下為實作 Router 出現的錯誤訊息:
確認 router 版本:npm info react-router-dom version
a. 錯誤訊息:Attempted import error: 'Switch' is not exported from 'react-router-dom'.
嘗試:$ npm add react-router-dom
-> 同樣錯誤訊息
嘗試:$ yarn add react-router-dom
-> 同樣錯誤訊息
嘗試:把所有 Switch 都換成 Routes,適用於 v6 版本
b. 錯誤訊息:TypeError: Cannot read properties of undefined (reading 'pathname')
這個沒有解決,但把 Switch 換成 Routes 就好了
c. 錯誤訊息:Error: \[HomePage\] is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>
不可以這樣寫
<Route exact path="/home">
<HomePage />
</Route>
要寫成這樣
<Routes>
<Route exact path="/" element={<HomePage />} />
<Route path="login/*" element={<LoginPage />} />
</Routes>
註:如果有關鍵字 Fragment,也有可能是 component 不能夠自己獨立存在(例如 <input />
),要被包起來,而包起來可以用
// 但實際測試,沒有 import 還是可以用
import { Fragment } from "react";
<Fragment>
<input />
</Fragment>
// 簡寫是
<>
<input />
</>
參考資料:Fragments
PS 這在 W23 老師有提到
把網址換成 http://localhost:3000/#/login
login page 就出的來了
二、先來切板外加整合 react router
Link 的幾種方式:
先 import { Link } from "react-router-dom";
1. 用 link 標籤包起來
<Nav>
<Link to="/newpost">New Post</Link>
</Nav>
2. 指定的方式
<Nav $active as={Link}>
Home
</Nav>
3. 用 styled component 的方式,記得 component 裡面要加 to
const Nav = styled(Link)`
display: flex;
justify-content: center;
align-items: center;
`;
<NavbarList>
<Nav to="#">Login</Nav>
</NavbarList>
4. 小結:
a. 到目前為止的結構:
SRC
components folder
App folder
App.js
index.js
NavBar folder
index.js
NavBar.js
Pages folder
HomePage folder
HomePage.js
index.js
LoginPage folder
index.js
LoginPage.js
index.js(this file is always outside of any folder but in SRC)
b. Routes 在 React Router v6 版本已經取代 Switch 了
先 import { HashRouter as Router, Routes, Route } from "react-router-dom";
c. 結構會像是這樣
<Router>
<NavBar />
<Routes>
<Route exact path="/" element={<HomePage />} />
<Route path="login/*" element={<LoginPage />} />
</Routes>
</Router>
d. 要用 link 的話,用 style + to 的方式
const Nav = styled(Link)``;
<Nav to="/" $active={location.pathname === "/"}>
Home
</Nav>
三、實作文章列表頁面
1. 串 API
老師習慣會開一個檔案專門處理 API,放在 SRC 資料夾底下
// WebAPI.js code
const BASE_URL = "https://student-json-api.lidemy.me";
export const getPosts = () => {
return fetch(`${BASE_URL}/posts?_sort=createdAt&_order=desc`).then((res) =>
res.json()
);
};
實作紀錄:
a. getPosts 裡面忘了寫 return
b. Post fn component 裡面也忘了寫 return
2. HomePage 刻畫面
首頁的畫面會是文章列表,左邊是標題,右邊是時間,標題會是連結
錯誤訊息:TypeError: posts.map is not a function
我不知道要怎麼解決這個錯誤,先把程式碼回復到沒有錯誤的時候,在一個一個元素慢慢加上去,忽然就好了,恩也許這也是一種方法,可能就是某個地方寫錯,但我抓不到錯吧。
再實作一次又出現這個錯誤,可是我沒有辦法解決這個問題…
我想起來為什麼了!以前也犯過一樣錯,就是沒有把 props 傳進這裡 <Post post={post} />
隔天我又遇到同樣問題,但是這次我什麼事都沒做忽然就好了,也許有時候就是會遇到這種奇怪的問題吧。
// HomePage code
import React, { useState, useEffect } from "react";
import styled from "styled-components";
import PropTypes from "prop-types";
import { getPosts } from "../../WebAPI";
import { Link } from "react-router-dom";
const Root = styled.div`
width: 80%;
margin: 0 auto;
`;
const PostContainer = styled.div`
border-bottom: 1px solid rgba(0, 12, 34, 0.2);
padding: 16px;
display: flex;
align-items: flex-end;
justify-content: space-between;
`;
const PostTitle = styled(Link)`
font-size: 24px;
color: #333;
text-decoration: none;
`;
const PostDate = styled.div`
color: rgba(0, 0, 0, 0.8);
`;
function Post({ post }) {
return (
<PostContainer>
<PostTitle to={`/posts/${post.id}`}>{post.title}</PostTitle>
<PostDate>{new Date(post.createdAt).toLocaleString()}</PostDate>
</PostContainer>
);
}
Post.propTypes = {
post: PropTypes.object,
};
export default function HomePage() {
const [posts, setPosts] = useState([]);
useEffect(() => {
getPosts().then((posts) => setPosts(posts));
}, []);
return (
<Root>
{posts.map((post) => (
<Post post={post} />
))}
</Root>
);
}
老師用了一個 amargin
css 屬性我沒看過,用了以後本來置中的排版就變到偏左邊了,好特別。
四、練習:實作單一文章頁面
1. 影片裡老師給了提示:
「新增一個 route 去接收路徑,根據這個路徑去 render postpage 的 component 在 call API 把東西拿出來就完成了」
可以參考 API 文件 怎麼拿到不同 id 的文章內容。
2. 沒有報錯可是文章就是無法顯示,後來發現是因為抓 API 的時候,網址列打錯了
錯誤的寫法是
export const getArticle = (id) => {
return fetch(`${BASE_URL}/posts?id=${id}`).then((res) => res.json());
};
正確的寫法,下次可以點擊網址,看看網址列會長什麼樣子,依樣畫葫蘆就對了
export const getArticle = (id) => {
return fetch(`${BASE_URL}/posts/${id}`).then((res) => res.json());
};
3. 要顯示單一文章頁面,該怎麼做呢?
a. WebAPI.js 裡面要有一個抓取單一文章的 fn
// WebAPI.js code
const BASE_URL = "https://student-json-api.lidemy.me";
// 抓取所有文章
export const getPosts = () => {
return fetch(`${BASE_URL}/posts?_sort=createdAt&_order=desc`).then((res) =>
res.json()
);
};
// 抓取單一文章
export const getArticle = (id) => {
return fetch(`${BASE_URL}/posts?id=${id}`).then((res) => res.json());
};
b. 在 App.js 裡設置 route 路徑,可以參考 useParams
// App.js 重點程式碼
import ArticlePage from "../../Pages/ArticlePage";
<Route path="/posts/:id" element={<ArticlePage />} />
c. 在 Pages 底下再開一個 ArticlePage 資料夾,裡面一樣放 ArticlePage.js & index.js
// ArticlePage.js code
import React, { useState, useEffect } from "react";
import styled from "styled-components";
import { getArticle } from "../../WebAPI";
import { useParams } from "react-router-dom";
const ArticleWrapper = styled.div`
padding: 16px;
width: 80%;
margin: 0 auto;
`;
const ArticleTitle = styled.div`
font-size: 20px;
padding: 20px;
`;
const ArticleBody = styled.div`
color: #666;
padding: 20px;
width: 90%;
text-align: justify;
line-height: 26px;
`;
export default function ArticlePage() {
const [article, setArticle] = useState([]);
const { id } = useParams();
useEffect(() => {
getArticle(id).then((article) => setArticle(article));
}, [id, article]);
return (
<ArticleWrapper>
<ArticleTitle>{article && article.title}</ArticleTitle>
<ArticleBody>{article && article.body}</ArticleBody>
</ArticleWrapper>
);
}
d. 也可以仿照 HomePage 那樣,寫一個 Article 的 component,要注意傳參數的問題喔
import React, { useState, useEffect } from "react";
import styled from "styled-components";
import { getArticle } from "../../WebAPI";
import { useParams } from "react-router-dom";
const ArticleWrapper = styled.div`
padding: 20px;
width: 60%;
margin: 0 auto;
`;
const ArticleTitle = styled.div`
font-size: 28px;
font-weight: bold;
padding: 20px;
`;
const ArticleContent = styled.div`
padding: 18px;
line-height: 32px;
text-align: justify;
`;
function Article({ article }) {
return (
<ArticleWrapper>
<ArticleTitle>{article.title}</ArticleTitle>
<ArticleContent>{article.body}</ArticleContent>
</ArticleWrapper>
);
}
export default function ArticlePage() {
let { id } = useParams();
const [article, setArticle] = useState([]);
useEffect(() => {
getArticle(id).then((article) => setArticle(article));
}, [id, article]);
return <Article article={article} />;
}
4. Error logs
error message: Error: Objects are not valid as a React child (found: [object ReadableStream]). If you meant to render a collection of children, use an array instead.
solution
抓 API 時寫錯了,錯誤的寫法,正確的寫法參考上面的紀錄
export const getArticle = (id) => {
return fetch(`${BASE_URL}/posts/${id}`).then();
};
五、登入功能講解加實作(上)
1. SPA 的 API 實作,我們這次用 JWT 帶資料給 Server
以前通常身份驗證都是用 cookie,登入就打 API 到 server,確認帳密無誤後會回傳 Set-Cookie 的 HTTP response header,裡面會有 token or session id;接著驗證身份時,再打一次 API 到 server,此時 browser 會自動帶一個 cookie 上去,如果 sessoin id 正確的話,server 就會回傳使用者資料
做 SPA 以後就不用 cookie 做驗證了,不過也是一樣透過帶 session id 的方式,自己把 token 存在 browser 的 localStorage 裡面,每次發 request 就自己帶上去
這次我們實作 SPA 登入、驗證身份的方式是使用 JWT
登入時 server 會給一個 JWT,接著驗證身份時就把 JWT 帶在 header 上面傳給 server,沒有問題的話,server 就會回傳使用者資訊
這次老師提供了兩支 Lidemy 學生專用 API Server,login & getMe,login 拿到 token 以後,再去打 getME
註:API 裡面不應該有任何密碼等敏感資訊喔
2. 實作登入功能 -- login & getMe API
// WebAPI.js → 寫 login & getMe API
// 要記得傳參數 username, password
export const login = (username, password) => {
return fetch(`${BASE_URL}/login`, {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
username,
password,
}),
}).then((res) => res.json());
};
export const getMe = () => {
// token 從 browser 的 localStorage 來的
const token = localStorage.getItem("token");
return fetch(`${BASE_URL}/me`, {
headers: {
authorization: `Bearer ${token}`,
},
}).then((res) => res.json());
};
3. 實作登入功能 -- 輸入帳密拿到 token
a. 刻登入畫面
b. 監聽 input 輸入內容
c. 呼叫 API:若有錯可以顯示錯誤訊息
d. 把 token 存在 localStorage Storage.getItem(),此時可以優化把 token 另開檔案存放 -> utils.js
e. 拿到 token 登入後,把使用者導回首頁
f. 把使用者資料放在最底層,並用 useContext
把資料傳下去
註:toString() JavaScript Number toString()、
// LoginPage.js code
import styled from "styled-components";
import { useState, useContext } from "react";
import { getMe, login } from "../../WebAPI";
import { useNavigate } from "react-router-dom";
import { setAuthToken } from "../../utils";
import { AuthContext } from "../../contexts";
const Login = styled.form`
border: 1px solid rgba(0, 0, 0, 0.8);
border-radius: 5px;
text-align: center;
margin: 100px auto;
width: 25%;
padding: 50px;
color: #666;
font-size: 24px;
`;
const Username = styled.div`
padding: 20px;
fon-size: 12px;
`;
const Password = styled.div`
padding: 20px;
fon-size: 12px;
`;
const Button = styled.button``;
const ErrorMessage = styled.div`
color: red;
margin-top: 30px;
`;
export default function LoginPage() {
const { setUser } = useContext(AuthContext);
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [errorMessage, setErrorMessage] = useState();
const navigate = useNavigate();
const handelSubmit = () => {
setErrorMessage(null);
login(username, password).then((data) => {
if (data.ok === 0) {
return setErrorMessage(data.message);
}
setAuthToken(data.token);
getMe().then((response) => {
if (response.ok !== 1) {
return setErrorMessage(response.toString());
}
setUser(response.data);
navigate("/");
});
});
};
return (
<Login onSubmit={handelSubmit}>
LOGIN
<Username>
username:
<input
value={username}
onChange={(e) => setUsername(e.target.value)}
type="text"
/>
</Username>
<Password>
password:
<input
value={password}
onChange={(e) => setPassword(e.target.value)}
type="password"
/>
</Password>
<Button>Login</Button>
{errorMessage && <ErrorMessage>{errorMessage}</ErrorMessage>}
</Login>
);
}
// utils.js code
const TOKEN_NAME = "token";
export const setAuthToken = (token) => {
return localStorage.setItem(TOKEN_NAME, token);
};
export const getAuthToken = () => {
return localStorage.getItem(TOKEN_NAME);
};
六、登入功能講解加實作(下)
實作登入功能 -- 使用者資料
a. 你好,這裡有使用者嗎?如果有就表示有登入,反之亦然,把使用者存在底層也就是 App.js 裡面,並用 context 傳下去,可以參考 來學 React 吧之五_prop drilling 與 context
// App.js code
import styled from "styled-components";
import { useState, useEffect } from "react";
import { AuthContext } from "../../contexts";
import { HashRouter as Router, Routes, Route } from "react-router-dom";
import NavBar from "../NavBar";
import HomePage from "../../Pages/HomePage";
import LoginPage from "../../Pages/LoginPage";
import ArticlePage from "../../Pages/ArticlePage";
import { getMe } from "../../WebAPI";
const Root = styled.div`
padding-top: 64px;
`;
export default function App() {
const [user, setUser] = useState(null);
useEffect(() => {
getMe().then((response) => {
if (response.ok) {
setUser(response.data);
}
});
}, []);
return (
<AuthContext.Provider value={{ user, setUser }}>
<Root>
<Router>
<NavBar />
<Routes>
<Route exact path="/" element={<HomePage />} />
<Route path="/posts/:id" element={<ArticlePage />} />
<Route path="/login" element={<LoginPage />} />
</Routes>
</Router>
</Root>
</AuthContext.Provider>
);
}
b. 把 context 另開檔案存起來 -> contexts.js
// contexts.js code
import { createContext } from "react";
export const AuthContext = createContext(null);
c. 打 getMe API,確認使用者
在呼叫登入 API 時,還要拿到使用者資料才算是成功登入,因此在登入頁面寫 getMe API,拿到使用者資料
// LoginPage.js code
import styled from "styled-components";
import { useState, useContext } from "react";
import { getMe, login } from "../../WebAPI";
import { useNavigate } from "react-router-dom";
import { setAuthToken } from "../../utils";
import { AuthContext } from "../../contexts";
const Login = styled.form`
border: 1px solid rgba(0, 0, 0, 0.8);
border-radius: 5px;
text-align: center;
margin: 100px auto;
width: 25%;
padding: 50px;
color: #666;
font-size: 24px;
`;
const Username = styled.div`
padding: 20px;
fon-size: 12px;
`;
const Password = styled.div`
padding: 20px;
fon-size: 12px;
`;
const Button = styled.button``;
const ErrorMessage = styled.div`
color: red;
margin-top: 30px;
`;
export default function LoginPage() {
const { setUser } = useContext(AuthContext);
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [errorMessage, setErrorMessage] = useState();
const navigate = useNavigate();
const handelSubmit = () => {
setErrorMessage(null);
login(username, password).then((data) => {
if (data.ok === 0) {
return setErrorMessage(data.message);
}
setAuthToken(data.token);
getMe().then((response) => {
if (response.ok !== 1) {
return setErrorMessage(response.toString());
}
setUser(response.data);
navigate("/");
});
});
};
return (
<Login onSubmit={handelSubmit}>
LOGIN
<Username>
username:
<input
value={username}
onChange={(e) => setUsername(e.target.value)}
type="text"
/>
</Username>
<Password>
password:
<input
value={password}
onChange={(e) => setPassword(e.target.value)}
type="password"
/>
</Password>
<Button>Login</Button>
{errorMessage && <ErrorMessage>{errorMessage}</ErrorMessage>}
</Login>
);
}
d. getMe 的錯誤處理,如果有錯就把 token 清空
e. 在 nav bar 也把使用者也拿出來:有登入就顯示發表文章與登出;沒登入就顯示登入
f. 登出要清空 token 並且清除使用者,接著導回首頁
// NavBar.js code
import styled from "styled-components";
import { useContext } from "react";
import { AuthContext } from "../../contexts";
import { setAuthToken } from "../../utils";
import { Link, useLocation, useNavigate } from "react-router-dom";
const HeaderContainer = styled.div`
height: 64px;
display: flex;
justify-content: space-between;
align-items: center;
position: fixed;
left: 0;
right: 0;
top: 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
padding: 0px 32px;
box-sizing: border-box;
`;
const Logo = styled.div`
font-size: 32px;
font-weight: bold;
`;
const NavbarList = styled.div`
display: flex;
align-items: center;
height: 64px;
`;
const Nav = styled(Link)`
display: flex;
justify-content: center;
align-items: center;
height: 100%;
box-sizing: border-box;
width: 100px;
cursor: pointer;
text-decoration: none;
color: black;
${(props) =>
props.$active &&
`
background: rgba(0, 0, 0, 0.2);
`}
`;
const LeftContainer = styled.div`
display: flex;
align-items: center;
${NavbarList} {
margin-left: 64px;
}
`;
export default function NavBar() {
const location = useLocation();
const { user, setUser } = useContext(AuthContext);
const navigate = useNavigate();
const handleLogout = () => {
setAuthToken(null);
setUser(null);
if (location.pathname !== "/") {
navigate("/");
}
};
return (
<HeaderContainer>
<LeftContainer>
<Logo>My Blog</Logo>
<NavbarList>
<Nav to="/" $active={location.pathname === "/"}>
Home
</Nav>
{user && (
<Nav to="/new-post" $active={location.pathname === "/new-post"}>
New Post
</Nav>
)}
</NavbarList>
</LeftContainer>
<NavbarList>
{!user && (
<Nav to="/login" $active={location.pathname === "/login"}>
Login
</Nav>
)}
{user && (
<Nav onClick={handleLogout} to="/">
Logout
</Nav>
)}
</NavbarList>
</HeaderContainer>
);
}
g. 登入後重新整理,應該還是登入的狀態,因此在初始化時也要 getMe 拿到使用者資料
// App.js code
import styled from "styled-components";
import { useState, useEffect } from "react";
import { AuthContext } from "../../contexts";
import { HashRouter as Router, Routes, Route } from "react-router-dom";
import NavBar from "../NavBar";
import HomePage from "../../Pages/HomePage";
import LoginPage from "../../Pages/LoginPage";
import ArticlePage from "../../Pages/ArticlePage";
import { getMe } from "../../WebAPI";
const Root = styled.div`
padding-top: 64px;
`;
export default function App() {
const [user, setUser] = useState(null);
useEffect(() => {
getMe().then((response) => {
if (response.ok) {
setUser(response.data);
}
});
}, []);
return (
<AuthContext.Provider value={{ user, setUser }}>
<Root>
<Router>
<NavBar />
<Routes>
<Route exact path="/" element={<HomePage />} />
<Route path="/posts/:id" element={<ArticlePage />} />
<Route path="/login" element={<LoginPage />} />
</Routes>
</Router>
</Root>
</AuthContext.Provider>
);
}
h. 如果有拿到 token 才去呼叫 getMe api,這段在 App.js 裡面
useEffect((TOKEN_NAME, token) => {
if (getAuthToken(TOKEN_NAME, token)) {
getMe().then((response) => {
if (response.ok) {
setUser(response.data);
}
});
}
}, []);
i. 在登入狀態,重新整理時會先看起來好像登出了(顯示登入按鈕),才又回到登入狀態:這個我還沒做
七、Error Logs:
1. handleSubmit 參數傳錯
2. 再次實作時,忘了登入成功以後,要找個地方把 token 存起來
const handleSubmit = () => {
setErrorMessage(null);
login(username, password).then((data) => {
// 如果不成功的話顯示後端的錯誤訊息
if (data.ok === 0) {
return setErrorMessage(data.message);
}
// 登入成功後,把 token 存起來
const token = data.token;
localStorage.setItem("token", token);
});
};
這時候可以另開一個在 SRC 底下的檔案叫 utils,把 token 存在裡面:
const TOKEN_NAME = "token";
export const setAuthToken = (token) => {
return localStorage.setItem(TOKEN_NAME, token);
};
export const getAuthToken = () => {
return localStorage.getItem(TOKEN_NAME);
};
3. 把使用者導回首頁,影片中用 useHistory
,但 react-router-dom v6 的用法是 useNavigate
,官方文件
import { useNavigate } from "react-router-dom";
const navigate = useNavigate();
navigate("/");
4. 錯誤訊息:Attempted import error: 'useHistory' is not exported from 'react-router-dom'.
solution: In react-router-dom v6 useHistory() is replaced by useNavigate()
5. 錯誤訊息:Unhandled Rejection (TypeError): navigate.push is not a function
正確用法:navigate("/");
6. 錯誤訊息:TypeError: Object(...) is not a function
解法:useNavigate
不是從 react 引入,是從 react-router-dom
import { useNavigate } from "react-router-dom";
7. 錯誤訊息:TypeError: Cannot read properties of undefined (reading 'pathname')
參考資料,研究了一下,發現怎麼樣都不懂
隱約覺得錯誤要不是 link 相關,要不就是 Context 相關。看了估狗前幾頁的文章,但還是找不出 bug,去看同學的作業,發現原來錯誤在 NavBar 裡面
好吧,進展就是至少我有猜對一半,方向是正確的,可惡花了兩個小時
// NavBar.js code
<NavbarList>
{!user && (
<Nav to="/login" $active={location.pathname === "/login"}>
Login
</Nav>
)}
{user && (
// 這裡沒有加上 to="/",所以有錯
<Nav to="/" onClick={handleLogout}>
Logout
</Nav>
)}
</NavbarList>
8. 雖然影片 「React 實戰篇 - 部落格」 的「登入功能講解加實作(下)」10'17" 說可以忽略沒有加上 to 的 warning,可是在我本地端的情況下,登入會失敗,顯示上面的錯誤訊息,所以還是加上去
9. 錯誤訊息:Unhandled Rejection (TypeError): setUser is not a function
10. 錯誤訊息:Attempted import error: 'AuthContext' is not exported from '../../contexts'.
通常是忘了 export
11. 錯誤訊息:TypeError: Cannot read properties of undefined (reading 'Provider')
12. 最後的結構
SRC
components folder
App folder
App.js
index.js
NavBar folder
index.js
NavBar.js
Pages folder
ArticlePage foler
ArticlePage.js
index.js
HomePage folder
HomePage.js
index.js
LoginPage folder
index.js
LoginPage.js
contexts.js
index.js
utils.js
WebAPI.js
八、實作檢討
1. 小心自動完成,有時會幫你加上你不想要的括號
新增一個 context 檔案,從裡面拿到 { user, setUser },要特別小心自動完成的功能,有時候會有意想不到的錯誤,例如:
正確的寫法 <AuthContext.Provider value={{ user, setUser }}>
自動完成幫你加上括號,就變成錯誤的寫法:
<AuthContext.Provider value={( user, setUser )}>
2. 實作記錄
a. 實作 45 分鐘我才做得出來第一次,還要偷看解答
b. 第二次花了 40 分鐘才做完,下次目標是 20 分鐘
c. 第三次一邊紀錄錯誤,一邊寫扣,32 分鐘完成
d. 接下來的實作大多都是 30 分鐘左右完成,一邊寫一邊跟自己解釋為什麼這裡要這樣寫,好像又更懂了,讚讚。
3. 檢討與錯誤訊息們
a. 錯誤訊息:Attempted import error: 'AuthContext' is not exported from '../../contexts'.
通常是忘了 export
錯誤訊息:TypeError: Cannot read properties of undefined (reading 'Provider')
b. 實作檢討:
第一次實作檢討
WebAPI.js -> 忘了用 AuthToken
LoginPage.js ->
login 函式裡面沒有 return
handleSubmit 裡面沒有先把錯誤訊息去掉 setErrorMessage(null);
getMe 如果失敗,沒有 setAuthToken(null);
- NavBar.js ->
登出後沒有檢查,如果不在首頁,才導到首頁
- App.js 裡面,邏輯寫反了,這樣寫的話,登入後重新整理就會要再次登入,可是明明就有拿到 token
useEffect(() => {
getMe().then((response) => {
if (response.ok !== 1) {
setAuthToken(null);
setUser(null);
}
});
});
要這樣寫才對
useEffect(() => {
getMe().then((response) => {
if (response.ok) {
setUser(response.data);
}
});
}, []);
第二次實作檢討
WebAPI.js -> login 忘了傳參數 username, password
LoginPage.js -> 要傳兩個參數
localStorage.setItem("token", token);
NavBar.js -> handleLout 沒有檢查「如果不在首頁才導回首頁」
if (location.pathname !== "/") {
navigate("/");
}