React Router V6 - Fetch data with Loader
上一篇介紹了 React Router V6 的基本架構,包含導頁、動態路由與巢狀路由,本文則會介紹 V6 全新的重要功能 Loader。
New Feature - Loader
「進入畫面後,需要 Call API 取得初始資料!」這個需求應該不陌生吧?許多頁面都有這個動作,例如:一進到商品頁,先取得商品資料。
這麼做並沒錯,但是這在使用者體驗 (UX) 上比較差,因爲當 User 進入頁面時,畫面或許還沒渲染完成。針對這一點,React Router V6 提供了 loader
這項功能,讓資料可以透過路由系統先行處理,在渲染前預先 Loading Data。
使用 Loader 與 useLoaderData 獲取資料
要透過路由系統取得資料,首先我們要在路由中定義 loader
這個參數,其值需要是一個函式,並且預期會回傳資料,讓我們能夠在元件中取用這份資料。
範例:我們使用 loader
與 Async/Await 去呼叫 API,雖然這會回傳一個 Promise,但是 React Router 會確保 API 資料已經回傳,讓我們能夠在元件中取得 resData.events
的資料。
1const router = createBrowserRouter([ 2 { 3 path: '/', 4 element: <RootLayout />, 5 children: [ 6 { 7 path: 'events', 8 element: <EventRootLayout />, 9 children: [ 10 { 11 index: true, 12 element: <Events />, 13 loader: async () => { 14 const response = await fetch('http://localhost:8080/events'); 15 if (!response.ok) { 16 // Handle Error... 17 } else { 18 const resData = await response.json(); 19 return resData.events; 20 } 21 }, 22 }, 23 ], 24 }, 25 ], 26 }, 27]); 28
回傳成功後,我們就可以在元件裡面透過 useLoaderData
取得資料了。
範例中,我們透過 useLoaderData
取得 Events 的相關資料。
1const EventsPage = () => { 2 const events = useLoaderData(); 3 4 return <EventsList events={events} />; 5}; 6 7export default EventsPage; 8
不過,如果我們在 App.js
這種定義路由的檔案中撰寫 loader
的函式,這個檔案會變得很大一包,所以建議的做法還是將 Loader 寫在 Page Component 裡面再 export
到路由檔案去做定義喔。
例如:我們將 Loader 寫在 Page-Level 元件中。
1export default EventsPage; 2 3export const loader = async () => { 4 const response = await fetch('http://localhost:8080/events'); 5 if (!response.ok) { 6 // Handle Error... 7 } else { 8 const resData = await response.json(); 9 return resData.events; 10 } 11}; 12
定義好 Loader 後,將元件裡面定義的 loader
通過 import
引入進來,並且可以使用 alias 定義不同頁面元件所使用的 Loader 名稱,常見的命名方式為 Events
頁面就叫做 eventsLoader
。
1import Events, { loader as eventsLoader } from './pages/Events'; 2 3const router = createBrowserRouter([ 4 { 5 path: '/', 6 element: <RootLayout />, 7 children: [ 8 , 9 { 10 path: 'events', 11 element: <EventRootLayout />, 12 children: [ 13 { 14 index: true, 15 element: <Events />, 16 loader: eventsLoader, 17 }, 18 ], 19 }, 20 ], 21 }, 22]); 23
最後一樣就可以在元件裡面透過 useLoaderData
取用資料啦!
Behind The Scenes: When Are loader() Functions Executed
如果你的 API 回傳時間較長,或是刻意讓 API 延遲回傳,就可以發現 Router 的跳轉,其實是等到 loader
取得資料後才執行。
這個機制的優點是能夠確保你已經取得資料,接著才去渲染畫面。
但缺點是,使用者在切換路由時可能會出現延遲,搞不好還會因此以為網頁壞掉了!?
關於這個問題,我們可以透過 useNavigation
的 state
去判斷該路由的狀態,藉由這個狀態動態地加上 Loading 樣式。
注意:
useNavigation
是用來取得路由狀態等資訊,而上一篇提到的useNavigate
則是用來執行程式化導頁等動作。
1const RootLayout = () => { 2 const navigation = useNavigation(); 3 4 return ( 5 <> 6 <MainNavigation /> 7 <main> 8 {navigation.state === 'loading' && <p>Loading...</p>} 9 <Outlet /> 10 </main> 11 </> 12 ); 13}; 14
另外,Loader 是在 Browser 環境中執行,而非在 Server 環境中執行。
但是,雖然 Loader 是在 Browser 中執行,但是 Loader 裡面還是「不能」使用像 useState
與 useParams
等 React Hooks 喔。
Throw Responses and Catch Errors with useRouteError
在上一篇 Setup Routes 的文章裡,我們使用 errorElement
來指定當路由導向發生錯誤時,應該渲染的頁面或元件,而這個 Error 頁面除了用在處理錯誤的路由,同樣也能用來處理錯誤的 API 回應。
範例:我們在父路由(也就是最外層)設置錯誤頁面,預計在這一層處理 API 的錯誤回應。
1const router = createBrowserRouter([ 2 { 3 path: '/', 4 element: <RootLayout />, 5 errorElement: <Error />, // catch any errors 6 children: [ 7 { index: true, element: <Home /> }, 8 { 9 path: 'events', 10 element: <EventRootLayout />, 11 children: [ 12 { 13 index: true, 14 element: <Events />, 15 loader: eventsLoader, 16 }, 17 ], 18 }, 19 ], 20 }, 21]); 22
根據需求,你也可以在父子路由個別設置
errorElement
,如果子路由沒有設置,那麼子路由出現的 Error 就會 Bubble Up 到父路由。
接下來我們用 throw new Response()
的方式拋出錯誤,這是一個近期比較推薦的做法,因為這樣可以讓前端依照不同的 status
顯示不同的資訊給使用者。
1const EventsPage = () => { 2 const data = useLoaderData(); 3 const events = data.events; 4 5 return <EventsList events={events} />; 6}; 7 8export default EventsPage; 9 10export const loader = async () => { 11 const response = await fetch('http://localhost:8080/events'); 12 if (!response.ok) { 13 throw new Response(JSON.stringify({ message: 'Could not fetch events' }), { 14 status: 500, 15 }); 16 } else { 17 return response; 18 } 19}; 20
定義好錯誤訊息後,我們透過 React Router V6 提供的 useRouteError
去取得錯誤訊息。
當我們是 Throw Responses 的時候,useRouteError
能透過 JSON.parse(error.data)
去取得回傳資料,以及透過 error.status
取得不同的狀態。
1// Error.js 2 3const Error = () => { 4 const error = useRouteError(); 5 6 let title = 'Oops!'; 7 let message = 'Sorry, an unexpected error has occurred.'; 8 9 // API Error 10 if (error.status === 500) { 11 message = JSON.parse(error.data).message; 12 } 13 14 // Path Error 15 if (error.status === 404) { 16 title = 'Not Found!'; 17 message = 'Could not find resource or page.'; 18 } 19 20 return ( 21 <PageContent title={title}> 22 <p>{message}</p> 23 </PageContent> 24 ); 25}; 26
如果你是回傳一般物件,像是
return { message: "error" }
,那麼這個error
就會是那個物件本身了。
The Utility Function: json()
看到上面這個做法(回傳 Responses)是不是覺得有點麻煩呢?雖然遵循這個方式可以定義不同的錯誤狀態,以給予更好的使用者體驗,但是這也讓 Code 變得複雜許多。
或許是因為 React Router V6 團隊也覺得很繁瑣?所以他們準備了一個 Utility Function 叫做 json()
讓我們使用!剛剛上面那一大串 new Response 可以改成以下寫法。
1// Events.js 2 3export const loader = async () => { 4 const response = await fetch('http://localhost:8080/events22'); 5 if (!response.ok) { 6 // throw new Response(JSON.stringify({ message: "Could not fetch events" }), { 7 // status: 500, 8 // }); 9 throw json({ message: 'Could not fetch events' }, { status: 500 }); 10 } else { 11 return response; 12 } 13}; 14
與此同時,你用 useRouteError
取得 error.data
後也不需要再做 JSON.parse()
了。
1// Error.js 2 3if (error.status === 500) { 4 // message = JSON.parse(error.data).message; 5 message = error.data.message; 6} 7
可喜可賀 🍻
使用 loader() 的參數
loader
自帶兩個參數 request
與 params
:
request
為一個 Request 的 Standard Web Object,可以存取像是 URL 等資訊params
為 Route Parameters,也就是動態路由冒號後面的 Segments
例如:位於 /events/:eventId
頁面時,loader()
可以透過 params.eventId
取得動態路由的片段,進而取得活動的詳細資料。
1// EventDetail.js 2 3export const loader = async ({ request, params }) => { 4 const id = params.eventId; 5 const response = await fetch(`http://localhost:8080/events/${id}`); 6 if (!response.ok) { 7 throw json( 8 { message: 'Could not fetch details for the selected event' }, 9 { 10 status: 500, 11 }, 12 ); 13 } else { 14 return response; 15 } 16}; 17
通過 useRouteLoaderData() 在子路由之間分享 Loader
如果當前路由需要的 Loader 已經在其他路由定義過,我們不需要再重複撰寫,可以直接經由 useRouteLoaderData()
Hook 取得這份 Loader。
useRouteLoaderData()
能夠讓同一個路由樹的子路由共享 Loader,換句話說,想要分享同一份 Loader 時必須是子路由。
為了達到這一點,我們可以新增一層父路由,但是不給予 element
只定義 loader
,並且賦予一個 id
,這樣裡面的子路由就能透過 useRouteLoaderData(id)
取得共享的資料囉。
範例:當 EditEvent 頁面也需要使用 EventDetail 頁面的 Loader 時,首先我們要重新配置路由,將 EventDetail 的 Loader 提出來作為父路由,再把兩個頁面都放在這個父路由底下。
1const router = createBrowserRouter([ 2 { 3 path: '/', 4 children: [ 5 { 6 path: 'events', 7 element: <EventRootLayout />, 8 children: [ 9 { 10 path: ':eventId', 11 id: 'event-detail', // 記得加上 ID 12 loader: eventDetailLoader, // 共用的 Loader 13 children: [ 14 { 15 index: true, 16 element: <EventDetail />, 17 }, 18 { path: 'edit', element: <EditEvent /> }, 19 ], 20 }, 21 ], 22 }, 23 ], 24 }, 25]); 26
設定好 RouteLoader 後,進入 EventDetail 頁面將原本的 useLoaderData
改為使用 useRouteLoaderData
。
同樣的,EditEvent 頁面也要使用 useRouteLoaderData("event-detail")
來取得 RouteLoader 的資料。
1// EventDetail.js 2const EventDetail = () => { 3 const data = useRouteLoaderData('event-detail'); 4 5 return <EventItem event={data.event} />; 6}; 7 8// EditEvent.js 9const EditEvent = () => { 10 const data = useRouteLoaderData('event-detail'); 11 12 return ( 13 <> 14 <h1>EditEvent</h1> 15 <EventForm event={data.event} /> 16 </> 17 ); 18}; 19
注意:
useRouteLoaderData
必須接收 Routes ID 這一個參數才能運作喔。
回顧
看完這篇文章,我們認識了 React Router V6 新增的 Loader 功能,瞭解如何使用 Loader 幫助我們建立更完整的路由系統。
下一篇文章會介紹另一個全新功能 Action 的用法,它可以幫助我們更全面地處理表單送出與驗證等功能。