React Router V7 ไม่ใช่แค่ Router สำหรับ React แล้วนะ
React Router V7 NextJS Alternative
ตอนนี้ React Router ไม่ได้เป็นแค่ Library ที่เอาไว้ใช้ทำ Routing ใน React อีกต่อไป
React Router เป็น Framework แล้ว สามารถทำ features ที่ Server side ทำได้แล้ว เช่น
- Server Side Rendering (SSR) ได้
- Static Site Generation (SSG) ได้
- Code Splitting ได้
- File based routing ได้
แต่ก็ยังสามารถใช้เป็น Router แบบเดิมได้เหมือนเดิม
และนี่คือ Version ใหม่ของ Remix นะครับ คือควบรวมกันเลย
ใครใช้ Remix v2 แล้วอยาก upgrade ก็มี script auto migrate มาให้ด้วย
ผมก็คิดว่า Remix น่าจะไม่ได้ถูกพัฒนาต่อแล้ว แต่ก็ไม่มีการยืนยันอะไรจากทีมงาน remix run นะครับ
Unified
react router v7 นี้ รวมทุกอย่างไว้ที่ library เดียวเลย
หากใครเคยใช้ react router มาก่อน น่าจะเคยใช้คู่กับ react-router-dom
แต่ตอนนี้ไม่ต้องแล้ว 😎
ทุกอย่างอยู่ใน react-router
library เดียว
แต่ถ้าใครใช้ react router version 6 อยู่ก่อนแล้ว แล้วอยากจะ migrate มาใช้ version7
เรายังสามารถใช้ react-router-dom
ได้อยู่เหมือนเดิมนะ
แต่ก็แนะนำให้ใช้ตัว react-router
ตัว unified แหละ
Framework
ใช้ react router v7 ตอนนี้ ส่วนตัวผมแนะนำให้ใช้แบบ framework นะครับ
มาดูรายละเอียดกัน แต่ไม่ลึกมากนะ
SSR, SPA and Pre-rendering
การเปลี่ยนแปลงรอบนี้ ส่วนที่เห็นได้ชัดมากๆน่าจะเป็นส่วน framework นี้แหละ ทำให้ React router สามารถทำ fullstack website ได้ง่ายขึ้นมากๆ ไม่ต้องพึ่ง framework อื่นๆแล้ว แต่ก็ไม่สามารถทำ REST API แบบ NextJs หรือ Sveltekit ได้นะ
การ set จาก SPA เป็น SSR ก็ทำได้ง่ายๆในไฟล์ react-router.config.ts
react-router.config.ts
หรือถ้าจะทำ Static Pre-rendering ก็ config แบบนี้
react-router.config.ts
Routing
การ config routing จะทำได้ 2 แบบ
- Route Module เป็นการ config ผ่าน function
layout()
,index()
และroute()
ซึ่งก็จะเป็นท่าที่คนใช้ react router คุ้นเคยอยู่แล้ว แค่เปลี่ยนจะ Tag<Route>
มาเป็น function แทน
ตัวอย่าง
routes.ts
- File based routing (fs-route) อันนี้ก็จะเป็นเหมือน fullstack framework อื่นๆแหละ คือแยก pages ต่างๆตามชื่อไฟล์เลย แต่วิธีการเขียนจะต่างจาก fullstack framework เจ้าอื่นๆเลย แทนที่จะใช้ folder ในการแบ่ง paths ต่างๆตาม URL ที่อยากให้เกิดขึ้น react router ดันไปใช้ dot-delimiters แทนคนที่มาจาก framework อื่นๆอาจจะงงได้
การใช้ file based routing จะต้องติดตั้ง package เพิ่มนะ
แล้วในไฟล์ routes.ts
ก็จะแก้ให้เป็นแบบนี้
routes.ts
แล้วก็สร้าง page ด้วยการสร้างไฟล์ ประมาณนี้
Ready to Deploy
ถ้าเราใช้ตัว framework ที่เป็นการ init project ผ่าน cli เบื้องหลังมันก็ไปเอา template ที่ทีม remix-run ทำไว้ให้
ซึ่งในนั้นเขาเขียน Dockerfile มาให้เราเลย มีครบสำหรับการรันบน Nodejs(npm), Nodejs(pnpm), Bun
Typescript type
typescript ก็ supported แบบเต็มตัว อันนี้ก็ตั้งแต่ version ก่อนๆแล้วนะ แต่ใน framework mode นี้ type ที่เอามาใช้จะเกิดจากการที่ dev server เป็นคน generate มาให้เรา และเราต้อง import ให้ถูกตัวด้วยนะ ถ้าใครเคยใช้ sveltekit ก็ใช้วิธีการเดียวกันนี่แหละ
มาดูตัวอย่างกัน เราจะใช้ file-based routing นะ ใช้ dot-delimiters มาแทน folder
ในที่นี้อยากได้ url /products
ก็เลย สร้างไฟล์ routes/products._index.tsx
routes/products._index.tsx
จะเห็นว่าเราต้อง import Route
มาจาก ./+types/products._index
ซึ่งไฟล์นี้เราไม่ได้สร้าง และมันก็ไม่ได้อยู่ใน folder เดียวกันกับที่เราเขียนนี้ด้วย
ซึ่ง file ./+types/products._index
นี้เป็นไฟล์ที่ react router สร้างมาให้อัตโนมัติ โดยไฟล์ที่สร้างขึ้นมานี้จะอยู่ใน folder .react-router
นะ
อีกตัวอย่าง
อยากได้ url /users
ก็จะสร้างไฟล์ users._index.tsx
routes/users._index.tsx
data loading
การดึง data มาแสดง ก็จะมี 2 รูปแบบ
- โหลด data ที่ฝั่ง client เผื่อว่ามี data ที่อยากโหลดที่ browser เรียกว่า client data loading
- โหลด data ที่ฝั่ง server เท่านั้น เรียกว่า server data loading
มาดูกันทีละตัว
1. client data loading
เราจะต้อง export function ที่ชื่อว่า clientLoader()
ซึ่ง function parameter จะมี type ที่มาจาก Route.ClientLoaderArgs
/routes/products.tsx
จาก Code ตัวอย่างด้านบน
component ของ Page นี้จะต้องใช้ export default
นะ
ส่วน clientLoader()
ให้ export
ปกติ
ใน parameter Route.ClientLoaderArgs
ไม่ได้มีแค่ params
นะ มี request
object ให้เราดึง headers ได้ด้วย
เมื่อหน้า page products ถูกโหลดที่ browser browser ก็จะทำการ fetch ไปที่ /api/products/${params.pid} เพื่อขอ data ให้เรา
แล้วจากนั้น ตัว compoment ของ page ก็จะรับ data ผ่าน props นะ ซึ่งก็จะมีเอา type มาจาก Route.ComponentProps
มี property ที่ชื่อว่า loaderData
2. Server data loading
มาดูตัวอย่าง code กันก่อนเลย
ส่วนของ page component ก็ให้ใช้ export default
เหมือนเดิม
แต่ส่วน function ที่จะดึง data จาก server เท่านั้นเนี่ย ต้องตั้งชื่อว่า loader()
และต้องใช้ export
แบบธรรมดา
ส่วน function parameter ก็ใช้ type ที่มาจาก Route.LoaderArgs
เพื่อรับ parameter ที่จะใช้ในการดึง data
ตรงนี้ก็เหมือนกัน มี params
มี req
ให้ใช้เหมือนใน clientLoader()
เลย
ส่วนใน page component ก็จะรับ data ที่ได้ผ่าน Props loaderData
เหมือนกัน
More
เราสามารถใช้ clientLoader()
กับ loader()
พร้อมกันได้นะ
การทำงานก็จะทำแยกกัน ทำหน้าที่ของใครของมัน
เพื่อนๆไปอ่านเพิ่มเติมได้ที่ use both loader
ยังมี Static pre-rendering อีกนะ แต่ไม่ได้มีอะไรซับซ้อน ก็เลยไม่ได้เอามาเล่าให้นะ
เพื่อนๆไปอ่านเพิ่มเติมได้ตรงนี้ static pre-rendering
Action
Action เป็นส่วนที่ใช้กับ Form นะ เวลาเรา submit form มันก็จะมาเรียกใช้ action นี้นี่แหละ
ซึ่งเราสามารถกำหนดได้อีกว่า action ที่อยากให้ทำงานเนี่ย ถูกทำงานที่ไหน ที่ server หรือ ที่ client(browser)
ใน React Router V7 ก็เลยมี action()
ที่ทำงานบน server กับ clientAction()
ที่ทำงานบน browser
วิธีการใช้เขียนก็เหมือนกับการใช้ loader เลย
มาดูตัวอย่างกัน
Server Action
ตัวอย่างโค้ด
/routes/project.tsx
จากตัวอย่างด้านบน
เราแค่ export action(context: Route.ActionArgs)
การทำงานจากการ submit form ก็เขียนใน function นี้แหละ
ส่วน type ของ function parameter นี้คือ Route.ActionArgs
นะ มีพวก request
ให้ใช้เหมือนกัน
และในตัวอย่างนี้จะเห็นว่าถ้า form ทำงานเสร็จก็ return object ที่ต้องการออกไปได้เลย
ที่ page ก็จะเอา object นี้ไปใช้ต่อ จาก props ที่ชื่อว่า actionData
และถ้า ้hover ดูที่ actionData
ก็จะเห็น type ที่ return จาก action function เลย
แต่ถ้าเกิดว่า form มันเกิด error แล้วเราอยากจะส่ง error กลับไปให้ browser รู้ อยากจะใส่ status code ด้วย เราต้องใช้ตัวช่วยเพิ่มเข้ามา
เราจะใช้ function data()
ที่ import มาจาก react-router lib
แบบนี้
/routes/project.tsx
Client Action
ถ้าเราอยากให้ form submittion ทำงานที่ client เราก็แค่ใช้ function export function clientAction()
แทน function action()
เท่านั้นเองที่เหลือก็เหมือนกันทั้งหมด
ตัวอย่าง
/routes/project.tsx
Streaming with Suspense
ส่วนนี้เป็นส่วนที่ผมชอบมากที่สุดในการอัพเดตครั้งนี้เลย
มันคือการที่เราสามารถ response html กลับไปก่อนได้โดยที่หน้านั้นยังโหลดข้อมูลไม่เสร็จทั้งหมด ทำให้ users สามารถเห็นว่าหน้าเว็ปโหลดมาแล้ว แต่มี data บางอย่างที่กำลังตามาทีหลัง
ผมจะยกตัวอย่างอธิบายเพิ่มเติมแบบนี้ ดูภาพประกอบได้ครับ
ปกติแล้วหน้าเว็ปของเราก็จะมีส่วนที่ไม่ได้เปลี่ยนแปลงเลย เช่น nav-bar, logo, menu, footer ซึ่งส่วนนี้เรา render ไปก่อนได้เลยเพราะรู้แน่ๆอยู่แล้วว่าคืออะไร ไม่ต้องรอถาม server ส่วนนี้คือสีขาวในรูป
และก็จะมีบางส่วนที่ต้องไปถาม server แต่ก็รอไม่นาน เช่น account name, account picture ในตัวอย่างนี้คือสีฟ้า
ยังมีอีกคือเนื้อหา ในหน้าเว็ป ในแต่ละส่วนไม่จำเป็นต้องรอโหลดเป็นลำดับ สามารถโหลดข้อมูลเหล่านี้จาก server พร้อมๆกันได้เลย แล้วส่วนไหนได้ response ก่อนก็เอามาแสดงผลก่อน
ในรูปจะเป็นสีเขียว สีเหลืองและสีส้ม ทั้งหมดนี้คือเราโหลด data ที่ server ด้วยนะ
พอ server ได้ข้อมูลมาตัว framework ก็จะจัดการ stream ข้อมูลมาที่ browser และจัดการ render ในจุดที่ข้อมูลต้องไปแสดง ส่วนอื่นๆ ก็รอการ stream เพิ่มเติมต่อไปได้
มาดูตัวอย่างการเขียนกัน
เราจะใช้ react suspense มาช่วย
และใช้ component <Await>
ของ react-router
เราจะใช้ function loader()
เพื่อโหลด data ที่ server
ทั้งนี้เราอาจจะดูว่า data ส่วนไหนสำคัญต้องแสดงผลเลย ก็ให้ return object นั้นไปเลย
แต่ส่วนไหนที่รอแสดงผลที่หลังได้ ก็ให้ return Promise
ส่วน type เราก็จะเห็นผ่าน Route.ComponentProps
อยู่แล้ว
จากตัวอย่างด้านบน เรามี data ที่สำคัญจะต้อง render ทันที เราก็จะให้ render ที่ฝั่ง server ไปเลย คือ criticalData
และมีอีกส่วนหนึ่งคือ data ที่เราไม่ได้สำคัญ เดี๋ยวค่อย render ก็ได้ ก็จะให้ render ที่ฝั่ง client เมื่อ server ได้ data มาแล้ว แล้วค่อยๆ stream data มาที่ client ในตัวอย่างจะเป็น nonCriticalData
จะเห็นว่า ที่ criticalData
เราใช้ await
เราจะได้ data มาแน่ๆ
ส่วน nonCriticalData
เราไม่ได้ใช้ await ตรงนี้เราจะได้ Promise มาแทน
ด้านบนนั้นเป็นแค่ loader()
function
เรามาดู Page component กันต่อ จะเอา data ที่ได้จาก loader()
มาใช้ยังไง
ในส่วน criticalData
เราเอาไปใช้ได้เลยตรงๆ เพราะเป็น data จริงๆที่ได้มาจาก server
ส่วน nonCriticalData
เราจะเอาไปใช้ตรงๆไม่ได้เพราะมันเป็น Promise ตรงนี้แหละ เราจะเอา <React.Suspense>
มาใช้
จะเห็นว่าเราใส่ fallback
ลงไปเป็น attribute ของ <React.Suspense>
ด้วย ตรงนี้เราสามารถใส่ Component อะไรก็ได้ที่บอก user ว่า data ตรงนี้ยังมาไม่ครบนะ ส่วนใหญ่ก็จะเป็น loading component อะนะ
ถัดมา children ของ React.Suspense
component ก็จะใส่ <Await>
component ที่มาจาก react-router พร้อมกับใส่ data ที่เป็น Promise ของเราเข้าไปที่ attribute resolve
ถัดมา children ของ <Await>
component ก็จะเป็น callback function ที่ต้อง return React Component เอาง่ายๆมันก็เหมือน Promise.then()
ที่ต้อง return jsx อะนะ เมื่อ data พร้อมใช้งานเราจะส่งมันให้กับ Component ไหนเพื่อเอาไป render ก็ให้มัน return ตรงนี้แหละ