ข้ามไปยังเนื้อหาหลัก

Page layouts (เลย์เอาต์ของหน้า)

ไกด์นี้จะสำรวจเรื่อง Abstraction ของ Page layout — เมื่อหลายๆ หน้าใช้โครงสร้างรวมๆ เหมือนกัน และต่างกันแค่เนื้อหาหลัก

info

คำถามของคุณไม่อยู่ในไกด์นี้เหรอ? โพสต์คำถามของคุณโดยการให้ Feedback ในบทความนี้สิ (ปุ่มสีฟ้าทางขวา) แล้วเราจะพิจารณาขยายไกด์นี้ให้!

Simple layout (เลย์เอาต์แบบง่าย)

เลย์เอาต์ที่ง่ายที่สุดก็คือหน้านี้ที่คุณกำลังดูอยู่ไง มันมี Header พร้อม Navigation, Sidebars สองข้าง, และ Footer พร้อม External links ไม่มี Business logic ซับซ้อน และส่วนที่ Dynamic มีแค่ Sidebars กับ Switchers ทางขวาของ Header เท่านั้น เลย์เอาต์แบบนี้วางไว้ใน shared/ui หรือ app/layouts ได้ทั้งก้อนเลย โดยรับ Props เพื่อเติม Content ให้ Sidebars:

shared/ui/layout/Layout.tsx
import { Link, Outlet } from "react-router-dom";
import { useThemeSwitcher } from "./useThemeSwitcher";

export function Layout({ siblingPages, headings }) {
const [theme, toggleTheme] = useThemeSwitcher();

return (
<div>
<header>
<nav>
<ul>
<li> <Link to="/">Home</Link> </li>
<li> <Link to="/docs">Docs</Link> </li>
<li> <Link to="/blog">Blog</Link> </li>
</ul>
</nav>
<button onClick={toggleTheme}>{theme}</button>
</header>
<main>
<SiblingPageSidebar siblingPages={siblingPages} />
<Outlet /> {/* นี่คือที่ที่เนื้อหาหลักจะถูกวาง */}
<HeadingsSidebar headings={headings} />
</main>
<footer>
<ul>
<li>GitHub</li>
<li>Twitter</li>
</ul>
</footer>
</div>
);
}
shared/ui/layout/useThemeSwitcher.ts
export function useThemeSwitcher() {
const [theme, setTheme] = useState("light");

function toggleTheme() {
setTheme(theme === "light" ? "dark" : "light");
}

useEffect(() => {
document.body.classList.remove("light", "dark");
document.body.classList.add(theme);
}, [theme]);

return [theme, toggleTheme] as const;
}

โค้ดของ Sidebars ขอละไว้ให้นักอ่านไปทำเป็นการบ้านนะ 😉

การใช้ Widgets ใน Layout

บางครั้งคุณอาจอยากใส่ Business logic บางอย่างลงใน Layout โดยเฉพาะถ้าคุณใช้ Nested routes ซ้อนลึกๆ กับ Router เช่น React Router ทีนี้คุณจะเก็บ Layout ไว้ใน Shared หรือ Widgets ไม่ได้แล้ว เพราะ กฎการ Import ของ Layers:

Module ใน Slice หนึ่ง สามารถ Import Slice อื่นๆ ได้เฉพาะเมื่อ Slice เหล่านั้นอยู่ใน Layer ที่ต่ำกว่าอย่างเคร่งครัด

ก่อนจะคุยเรื่องวิธีแก้ เราต้องคุยกันก่อนว่ามันเป็นปัญหาจริงๆ รึเปล่า คุณ จำเป็นต้องใช้ Layout นั้นจริงๆ มั้ย และถ้าใช่ มัน จำเป็นมั้ย ที่จะต้องเป็น Widget? ถ้า Business logic ก้อนนั้นถูกใช้ซ้ำแค่ 2-3 หน้า และ Layout เป็นแค่ Wrapper เล็กๆ สำหรับ Widget นั้น ลองพิจารณา 1 ใน 2 ทางเลือกนี้ดู:

  1. เขียน Layout แบบ Inline ใน App layer ตรงที่คุณ Config routing เลย
    วิธีนี้เยี่ยมมากสำหรับ Router ที่รองรับ Nesting เพราะคุณสามารถจัดกลุ่ม Route และใส่ Layout ให้เฉพาะพวกมันได้

  2. ก็แค่ Copy-paste มันซะ
    ความอยากที่จะ Abstract โค้ดมักจะถูกให้ค่าเกินจริง โดยเฉพาะกับ Layouts ที่แทบไม่ค่อยเปลี่ยน สักวันหนึ่งถ้าหน้าหนึ่งต้องเปลี่ยน คุณก็แค่แก้หน้านั้นโดยไม่ต้องกระทบหน้าอื่นให้วุ่นวาย ถ้ากลัวใครจะลืมอัปเดตหน้าอื่น ก็เขียน Comment ทิ้งไว้บอกความสัมพันธ์ระหว่างหน้าพวกนั้นได้

ถ้าข้างบนใช้ไม่ได้สักวิธี มี 2 solutions ที่จะใส่ Widget ลงใน Layout:

  1. ใช้ Render props หรือ Slots
    Framework ส่วนใหญ่ยอมให้คุณส่ง UI เข้ามาจากภายนอกได้ ใน React เรียกว่า render props, ใน Vue เรียกว่า slots
  2. ย้าย Layout ไปที่ App layer
    คุณยังสามารถเก็บ Layout ของคุณไว้ที่ App layer ได้ด้วย เช่นใน app/layouts แล้วคุณอยากจะ Compose widget ไหนเข้าไปก็ได้ตามใจชอบ

อ่านเพิ่มเติม

  • มีตัวอย่างการสร้าง Layout พร้อม Authentication ด้วย React และ Remix (เทียบเท่า React Router) ใน tutorial