來學 React 吧之九_實作部落格


Posted by Christy on 2021-12-15

本文為 [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 版本

Upgrade to React Router 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 把東西拿出來就完成了」

useParams

可以參考 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("/");
}









Related Posts

{210409} Hello, World!

{210409} Hello, World!

用後端好朋友們 express & sequelize 實作部落格

用後端好朋友們 express & sequelize 實作部落格

React with SPA Blog

React with SPA Blog


Comments