0%

[week 22] React:用 SPA 架構實作一個部落格(四)- 優化篇

在完成部落格的基本功能之後,再來要進行優化的部分,也就是解決畫面閃爍的問題。

部落格 DEMO

一、處理登入狀態的畫面閃爍

[week 22] React:用 SPA 架構實作一個部落格(二)- 身分驗證 這篇筆記中,有提到登入狀態時,重整頁面會出現畫面閃爍的問題,之所以會有這個現象,是因為畫面進行了兩次 render:

  • 預設為登出狀態(第一次 render)
  • 當我們發 API 確認有登入之後,才會顯示登入狀態(第二次 render)

為了解決這個問題,就是在「確認是否登入之前,不要顯示和登入登出狀態有關的東西」。

App.js:設定 isLoadingGetMe 狀態

  • App.js

在 App.js 執行開始,就先設定一個 isLoadingGetMe,預設值為 true,也就是不顯示登入登出:

const [isLoadingGetMe, setLoadingGetMe] = useState(true);

一旦接收到 getMe() 回傳的 response 時,或是發現沒有 token 時,就會改成 false,顯示登入登出:

import { getMe } from "../../WebAPI";
import { getAuthToken } from "../../utils";

useEffect(() => {
  // 以 getAuthToken 從 localStorage 讀取 token
  if (getAuthToken()) {
    // 有 token 才 call API
    getMe().then((response) => {
      if (response.ok) {
        setUser(response.data);
        setLoadingGetMe(false);
      }
    });
  } else {
    setLoadingGetMe(false);
  }
}, []);

並透過 Provider 將參數設為全域變數:

import { AuthContext, LoadingContext } from "../../contexts";

// ...

<AuthContext.Provider value={{ user, setUser }}>
  <Root>
     <LoadingContext.Provider
        value={{ isLoading, setIsLoading, isLoadingGetMe }}
      >
 
    //  ...   

    </LoadingContext.Provider>
  </Root>
</AuthContext.Provider>

context.js:建立 context

在 src/context.js 建立 context,初始值設為 null:

import { createContext } from "react";

// 初始值為 null
export const AuthContext = createContext(null);
export const LoadingContext = createContext(null);

Header.js:根據 isLoadingGetMe 顯示登入狀態

首先從 context.js 引入參數,以及引入需要的 hooks:

import React, { useContext } from "react";
import { Link, NavLink, useHistory, useLocation } from "react-router-dom";

import { AuthContext, LoadingContext } from "../../contexts";
import { setAuthToken } from "../../utils";

接著就可以根據 isLoadingGetMe 以及 user 的布林值,決定如何顯示登入狀態:

export default function Header() {
  const { isLoadingGetMe } = useContext(LoadingContext);
  const { user, setUser } = useContext(AuthContext);
  const location = useLocation();

  // 登出功能
  const history = useHistory();
  const handleLogout = () => {
    setAuthToken("");
    setUser(null);
    if (location.pathname !== "/") {
      history.push("/");
    }
  };

  return (
    <HeaderContainer>
      <Brand>
        <Link to="/" replace>
          React 部落格
        </Link>
      </Brand>
      <NavbarList>
        <StyledLink exact to="/about" replace activeClassName="active">
          關於我
        </StyledLink>
        <StyledLink to="/post-list/" replace activeClassName="active">
          文章列表
        </StyledLink>
        {isLoadingGetMe ? (
          <LoadingGetMe>資料讀取中...</LoadingGetMe>
        ) : (
          <>
            {!user && (
              <StyledLink to="/register" replace activeClassName="active">
                註冊
              </StyledLink>
            )}
            {!user && (
              <StyledLink to="/login" replace activeClassName="active">
                登入
              </StyledLink>
            )}
            {user && (
              <StyledLink to="/new-post" replace activeClassName="active">
                發布文章
              </StyledLink>
            )}
            {user && (
              <StyledLink to="" replace onClick={handleLogout}>
                登出
              </StyledLink>
            )}
          </>
        )}
      </NavbarList>
    </HeaderContainer>
  );
}

重點在於 isLoadingGetMe 的判斷邏輯:

  • 如果 isLoadingGetMe 為 true,就不會顯示裡面和登入狀態有關的東西
  • 當 isLoadingGetMe 為 faluse,才會再根據 user 是否為 true,決定要顯示「註冊、登入」還是「發布文章、登出」
{isLoadingGetMe ? (
  <LoadingGetMe>資料讀取中...</LoadingGetMe>
) : (
  <>
    {!user && (
      <StyledLink to="/register" replace activeClassName="active">
        註冊
      </StyledLink>
    )}
    {!user && (
      <StyledLink to="/login" replace activeClassName="active">
        登入
      </StyledLink>
    )}
    {user && (
      <StyledLink to="/new-post" replace activeClassName="active">
        發布文章
      </StyledLink>
    )}
    {user && (
      <StyledLink to="" replace onClick={handleLogout}>
        登出
      </StyledLink>
    )}
  </>
)}

二、處理呼叫 API 造成的畫面閃爍

當我們需要 call API 時,必須考慮到非同步的問題。舉例來說,當我們進入文章列表時,第一次 render 會先看到空的列表,第二次 render 才會出現文章。

為了解決這個問題,我們可以將第一次 render 改為 Loading 畫面,等到第二次 render 再顯示文章頁面。

那麼就開始吧!

App.js:設定 isLoading 狀態

首先,同樣在 APP 執行時就先設第一個 isLoading 狀態,預設值為 false,當我們有進行 call API 的動作時才會設為 true:

const [isLoading, setIsLoading] = useState(false);

同樣透過 Provider 將參數設為全域變數:

 <LoadingContext.Provider
    value={{ isLoading, setIsLoading, isLoadingGetMe }}>
    // ...
</LoadingContext.Provider>

PoseListPage.js:根據 isLoading 狀態顯示畫面

接著引入 context,還有 isLoading 時要顯示的 Loading component:

import React, { useState, useEffect, useRef, useContext } from "react";
import { LoadingContext, AuthContext } from "../../contexts";
import Loading from "../../components/Loading";

接著是根據 isLoading 狀態顯示畫面,在 call API 時會設為 true,直到接收 response 後會設為 false:

export default function HomePage() {
  const { isLoading, setIsLoading } = useContext(LoadingContext);
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    setIsLoading(true);
    getPosts()
      .then((posts) => setPosts(posts))
      .then(() => {
        setIsLoading(false);
      });
  }, [setIsLoading]);

  return (
    <Root>
      {isLoading ? (
        <Loading />
      ) : (
        <PostsListContainer>
          <PostsListTitle>最新文章</PostsListTitle>
          {posts && posts.map((post) => <PostList post={post} key={post.id} />)}
          <ReadMore>
            <Link to="/post-list">查看更多</Link>
          </ReadMore>
        </PostsListContainer>
      )}
    </Root>
  );
}

三、透過 react-spinner 設定 loading 畫面

使用方法可參考 davidhu2000 / react-spinners 介紹,以及樣式 DEMO

安裝套件

npm install react-spinners --save

官方使用範例

官方範例是用 class component 去寫的,但其實概念和 function component 沒有差太多,狀態就從 App.js 設定的 isLoading 去判斷即可:

import React from "react";
import { css } from "@emotion/core";
import ClipLoader from "react-spinners/ClipLoader";

// Can be a string as well. Need to ensure each key-value pair ends with ;
const override = css`
  display: block;
  margin: 0 auto;
  border-color: red;
`;

class AwesomeComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      loading: true
    };
  }

  render() {
    return (
      <div className="sweet-loading">
        <ClipLoader
          css={override}
          size={150}
          color={"#123abc"}
          loading={this.state.loading}
        />
      </div>
    );
  }
}
  • Loading.js

改寫 Loading component 如下:

import React from "react";
import styled from "styled-components";
// 要引用的樣式
import { PuffLoader } from "react-spinners";

// 全版的半透明背景
const LoadingWapper = styled.div`
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.6);
  display: flex;
  align-items: center;
  justify-content: center;
`;

export default function Loading() {
  return (
    <LoadingWapper>
      <PuffLoader size={60} color={"#4A90E2"} />
    </LoadingWapper>
  );
}

效果如下: