5-5 實作資料載入中的狀態

本單元對應的專案分支為:show-loading-status

單元核心#

這個單元的主要目標包含:

  • 了解為什麼需要製作「資料載入中」的狀態
  • 了解實作「資料載入中」的方法
  • 了解如何從 setState() 中透過帶入函式的方式取得未變更前的資料狀態

現在當頁面一載入,或當使用者點擊了重新整理的按鈕時,就會透過中央氣象局的 API 重新拉取最新的觀測資料,並更新畫面內容,但如果使用者再點一次,因為畫面上沒有任何資料有變動,使用者有些時候會不太確定到底是自己沒點到,或是點到了但資料正在載入中。

什麼是資料載入中的狀態#

為了著重良好的使用者體驗,不要讓使用者不清楚發生了什麼事,許多網站都會實作「資料載入中」的畫面。特別是由於現今網站許多都是透過 AJAX 去向後端伺服器拉取資料回來呈現,拉取資料的過程中必然需要消耗一些時間,因此處理「載入中」的狀態算是現在每個網站都需要考慮到的部分。

以 Instagram 為例,一開始進入網頁的時候,會先看到一個「空畫面」:

imgur

接著會出現一個「空殼」,可以注意到右下角有一個載入中的圖示,也就是那一朵像花會轉動的東西,相信大家一定不陌生:

imgur

那麼回到我們的即時天氣 App 中可以怎麼樣做呢?

實作載入中的資料狀態 - isLoading#

定義「載入中」的資料狀態 - isLoading#

現在可以在 useState 的地方,多添加一個 isLoading 的狀態,當它是 true 時表示資料還在載入中,這時候就可以顯示對應載入的畫面。一般會把 isLoading 的預設值設為 true,表示一進來的時候就正在拉取資料:

// ./src/App.js
const [currentWeather, setCurrentWeather] = useState({
// ...
rainPossibility: 60,
isLoading: true, // 多一個名為 isLoading 的狀態
});
提醒

這裡也可以把 isLoading 拆成另一個 state,但這裡考量到載入完成指的就是天氣資料(currentWeather)是否已經載入完成,因此多數時候是 isLoadingcurrentWeather 是會一起變動的,所以才把 isLoading 的狀態放在 currentWeather 物件中。

資料取得後修改 isLoading 的狀態#

接著在原本更新 currentWeather 函式中,透過 setCurrentWeatherisLoading 的值改為 false,表示資料已經載入完畢:

// ./src/App.js
const fetchCurrentWeather = () => {
fetch(/* ... */)
.then((response) => response.json())
.then((data) => {
// ...
setCurrentWeather({
// ...
rainPossibility: 60,
isLoading: false, // 資料拉取完後,把 isLoading 設為 false
});
});
};

點擊重新整理時,再次將 isLoading 改為 true - 使用 prevState#

現在,當使用者初次進來網站時,一開始的 isLoading 會是 true,也就是資料正在載入中;待 fetchCurrentWeather 的資料都回來之後,isLoading 會變成 false

但還有一個情況是當使用者點擊右下角的重新整理時,也需要把 isLoading 的狀態改成 true,這時候我們可以在 fetchCurrentWeather 實際開始向 API 拉取資料前,先把 isLoading 的狀態設成 true

但要注意的是,像下圖這種寫法是會產生錯誤的:

const fetchCurrentWeather = () => {
// ❌ 這種寫法是錯的
setCurrentWeather({ isLoading: true });
fetch(/* ... */)
.then((response) => response.json())
.then((data) => {
//...
});
};

我們需要在呼叫 fetch 之前去將 isLoading 改成 true 這部分的邏輯是沒有錯的,但還記得在前面的單元中,筆者曾提到「每次 setSomething 時都是用新的資料覆蓋舊的」,所以這裡如果直接用:

setCurrentWeather({ isLoading: true });

那麼整個 currentWeather 的資料狀態都會被覆蓋掉,變成只剩下 { isLoading: true }

好在,透過 useState 產生的 setSomething 這個方法中,參數不只可以帶入物件,還可以帶入函式,透過這個函式就可以取得「更新前的資料狀態」,慣例上我們會把前一次的資料狀態取名為 prevState,接著透過物件的解構賦值把原本的資料狀態(prevState)放入物件中,再把要更新的資料狀態放進去:

// 在 setState 中如果是帶入函式的話,可以取得前一次的資料狀態
setState((prevState) => {
return { ...prevState, ...updatedValues };
});

因此,這裡的 setCurrentWeather 中,只需要帶入函式就可以取得原本的資料狀態,再透過物件的解構賦值把原有資料帶進去,更新 isLoading 的狀態改成 true 就可以了,程式碼會修改成下面這樣:

const fetchCurrentWeather = () => {
setCurrentWeather((prevState) => ({
...prevState,
isLoading: true,
}));
fetch(/* ... */)
.then((response) => response.json())
.then((data) => {
//...
});
};

到這一步後,一開始畫面載入或使用者點選「更新按鈕」時 isLoading 會是 true,資料載入完畢後 isLoading 會變成 false。如果你不確定是不是有正確修改的話,可以在 return JSX 的地方,使用 console.log(currentWeather.isLoading) 看一下:

Imgur

從瀏覽器的開發者工具中可以看到:

  • 一開始網頁載入時,畫面一共會轉譯三次,isLoading 的狀態分別是 true (預設值)-> true (拉資料前)-> false(拉完資料後)
  • 當使用者點選更新按鈕後,畫面會轉譯兩次,isLoading 的狀態分別是 true(拉資料前) -> false(拉完資料後)

Imgur

撰寫載入中要顯示的樣式#

現在透過 isLoading 的狀態,我們已經可以清楚知道什麼時候是正在拉取資料,什麼時候已經取得資料,因此就可以來撰寫載入中要顯示的樣式了。

這裡載入中的提示很簡單,當資料在載入中的時候,右下角的「更新按鈕」就改成顯示「載入中」的圖示,並搭配旋轉的動畫效果。

根據載入狀態切換顯示圖示#

現在就可以根據 isLoading 的狀態來切換要顯示的是「更新圖示」或「載入中圖示」,切換圖示的方式就和前一個單元使用到的條件轉譯一樣:

  1. ./src/images 資料夾中載入 loading 圖示,並取名為 LoadingIcon
// ./src/App.js
import { ReactComponent as LoadingIcon } from './images/loading.svg';
  1. 使用三元判斷式來做到條件轉譯,當 isLoadingtrue 時顯示 LoadingIcon 否則顯示 RefreshIcon,也就是:
// ./src/App.js
<Refresh onClick={fetchCurrentWeather}>
最後觀測時間:
{/*... */} {currentWeather.isLoading ? <LoadingIcon /> : <RefreshIcon />}
</Refresh>

現在,只要資料在載入中,右下角的圖示就會變成 LoadingIcon,載入完畢後就會變回原本的 RefreshIcon。如果你的網路速度很快的話,可能會發現你幾乎看不到 Loading 圖示,這時候你可以如下圖所示,在 Network 面板把自己的網速暫時調慢(Slow 3G)後,再重新整理試試看:

Imgur

提示,除了把網速調慢來檢視 LoadingIcon 的效果,讀者也可以試著使用 React DevTools 來把 isLoading 的狀態改成 true 試試看!

增加圖示旋轉的效果#

只是這種效果設計師是不會滿意的,因為一點都沒有「載入中」的感覺,這裡我們得要加上一點旋轉的效果來讓它看起來更像是在「載入中」。

我們只需要透過 CSS 的 animation 就可以讓圖示旋轉,回到使用 styled components 撰寫 Refresh CSS 樣式的地方,只需要在這裡面加上 animation 的效果就可以了:

1.使用 @keyframes 定義旋轉的動畫效果,並取名為 rotate,接著在裡面使用 CSS 的 transform rotate 來產生旋轉的動畫

const Refresh = styled.div`
/* ... */
/* STEP 1:定義旋轉的動畫效果,並取名為 rotate */
@keyframes rotate {
from {
transform: rotate(360deg);
}
to {
transform: rotate(0deg);
}
}
`;
  1. 針對 svg 圖示透過 animation 屬性套用 rotate 動畫效果
const Refresh = styled.div`
//...
svg {
//...
/* STEP 2:使用 rotate 動畫效果在 svg 圖示上 */
animation: rotate infinite 1.5s linear;
}
/* STEP 1:定義旋轉的動畫效果,並取名為 rotate */
@keyframes rotate {
/* ... */
}
`;

現在你應該會看到不論資料是否在載入中,畫面上的 <Refresh /> 圖示都會一直旋轉,因為我們並沒有告訴它說什麼時候要開始或停止套用旋轉的動畫。

只有在載入資料時才旋轉 - 把資料透過 props 傳入 Styled Component 內#

還記得曾經在前面的單元中提過,透過 CSS-in-JS 的這種寫法,除了可以在 CSS 中使用 JavaScript 外,還可以把資料透過 props 傳到 Styled Component 內,讓 CSS 可以根據這個資料來調整套用的樣式。

因此這裡可以這樣做:

  1. 是否正在載入中的 isLoading 資料狀態透過 props 帶入 <Refresh> 這個 styled components 中
<Refresh onClick={fetchCurrentWeather} isLoading={currentWeather.isLoading}>
最後觀測時間: {/* ... */}
</Refresh>
  1. Refresh 中的 animation-duration 屬性把傳入的 props 取出,直接透過物件的解構賦值取出 isLoading,並以此判斷是否要執行動畫,當 animation-duration0s 的話表示則不旋轉:
const Refresh = styled.div`
// ...
svg {
// ...
/* STEP 2:使用 rotate 動畫效果在 svg 圖示上 */
animation: rotate infinite 1.5s linear;
/* STEP 3:isLoading 的時候才套用旋轉的效果 */
animation-duration: ${({ isLoading }) => (isLoading ? '1.5s' : '0s')};
}
/* STEP 1:定義旋轉的動畫效果,並取名為 rotate */
@keyframes rotate {
/* ... */
}
`;

現在不論是第一次載入頁面,或是當你按下重新整理的按鈕,只有當資料真正時處於載入中的狀態時(isLoading = true),這個圖示才會套用旋轉的效果。

使用解構賦值讓版面更乾淨#

現在在 <App /> 元件的 JSX 中,使用了非常多 currentWeather.observationTimecurrentWeather.locationNamecurrentWeather.temperature...,這樣寫起來非常繁瑣,因為一直要寫 currentWeather.ooo

因此在 React 中對於物件類型的資料,經常會使用物件的解構賦值方法,先把要使用到的資料取出來,像是這樣:

const App = () => {
// ...
const {
observationTime,
locationName,
description,
windSpeed,
temperature,
rainPossibility,
isLoading,
} = currentWeather;
return {
/* ... */
};
};

如此,在 JSX 中就可以直接使用這些變數,而不需要在前面多加上 currentWeather.ooo,修改後的程式碼會變得更加精簡:

Imgur

換你了!完成資料載入中的狀態#

現在換你來完成資料載入中的狀態。可以參考以下流程:

  • currentWeather 中添加 isLoading 的資料,並預設為 true
  • 當從 API 取得資料後將 isLoading 的值改為 false
  • 若使用者點擊重新整理的按鈕,則要先讓 isLoading 變成 true,這裡要留意如何從 setState() 中以帶入函式的方式取得未變更前的資料狀態
  • 載入 Loading 圖示
  • 透過 CSS 增加旋轉的動畫效果,讓 Loading 圖示可以旋轉
  • 將 isLoading 的資料狀態以 props 傳入 <Refresh> 中,並根據此狀態決定要不要讓圖示套用旋轉動畫
  • 使用物件的解構賦值,將所需的資料從 currentWeather 中取出,並帶入 JSX 內

本單元相關之網頁連結、完整程式碼與程式碼變更部分可於 show-loading-status 分支檢視:https://github.com/pjchender/learn-react-from-hook-realtime-weather-app/tree/show-loading-status

Imgur