> ## Documentation Index
> Fetch the complete documentation index at: https://inertiajs-vi.tuantq.online/llms.txt
> Use this file to discover all available pages before exploring further.

# Layouts

export const VueSpecific = ({children}) => {
  const [code, setCode] = useState(() => {
    if (typeof window === "undefined") {
      return "Vue";
    }
    return localStorage.getItem("code")?.replace(/"/g, "") || "Vue";
  });
  useEffect(() => {
    const handler = event => {
      if (event.detail?.key === "code") {
        setCode(event.detail.value?.replace(/"/g, ""));
      }
    };
    window.addEventListener("localStorageUpdate", handler);
    return () => window.removeEventListener("localStorageUpdate", handler);
  }, []);
  if (code !== "Vue") {
    return null;
  }
  return children;
};

export const SvelteSpecific = ({children}) => {
  const [code, setCode] = useState(() => {
    if (typeof window === "undefined") {
      return null;
    }
    return localStorage.getItem("code")?.replace(/"/g, "") || null;
  });
  useEffect(() => {
    const handler = event => {
      if (event.detail?.key === "code") {
        setCode(event.detail.value?.replace(/"/g, ""));
      }
    };
    window.addEventListener("localStorageUpdate", handler);
    return () => window.removeEventListener("localStorageUpdate", handler);
  }, []);
  if (!code?.includes("Svelte")) {
    return null;
  }
  return children;
};

export const ReactSpecific = ({children}) => {
  const [code, setCode] = useState(() => {
    if (typeof window === "undefined") {
      return null;
    }
    return localStorage.getItem("code")?.replace(/"/g, "") || null;
  });
  useEffect(() => {
    const handler = event => {
      if (event.detail?.key === "code") {
        setCode(event.detail.value?.replace(/"/g, ""));
      }
    };
    window.addEventListener("localStorageUpdate", handler);
    return () => window.removeEventListener("localStorageUpdate", handler);
  }, []);
  if (code !== "React") {
    return null;
  }
  return children;
};

export const ClientSpecific = ({children}) => {
  const [nada, setNada] = useState();
  return children;
};

Hầu hết ứng dụng chia sẻ các phần tử UI chung giữa nhiều trang như thanh điều hướng chính, sidebar hoặc footer. Layout component cho phép bạn định nghĩa UI dùng chung một lần rồi tự động bao bọc các trang bằng nó.

## Tạo layout

Layout là component tiêu chuẩn nhận nội dung con. Không có gì đặc thù Inertia trong bản thân layout.

<CodeGroup>
  ```vue Vue icon="vuejs" theme={null}
  <script setup>
  import { Link } from "@inertiajs/vue3"
  </script>

  <template>
      <main>
          <header>
              <Link href="/">Home</Link>
              <Link href="/about">About</Link>
              <Link href="/contact">Contact</Link>
          </header>
          <article>
              <slot />
          </article>
      </main>
  </template>
  ```

  ```jsx React icon="react" theme={null}
  import { Link } from "@inertiajs/react"

  export default function Layout({ children }) {
      return (
          <main>
              <header>
                  <Link href="/">Home</Link>
                  <Link href="/about">About</Link>
                  <Link href="/contact">Contact</Link>
              </header>
              <article>{children}</article>
          </main>
      )
  }
  ```

  ```svelte Svelte icon="s" theme={null}
  <script>
      import { inertia } from '@inertiajs/svelte'

      let { children } = $props()
  </script>

  <main>
      <header>
          <a use:inertia href="/">Home</a>
          <a use:inertia href="/about">About</a>
          <a use:inertia href="/contact">Contact</a>
      </header>
      <article>
          {@render children()}
      </article>
  </main>
  ```
</CodeGroup>

Bạn có thể dùng layout bằng cách trực tiếp bao nội dung trang bằng nó. Tuy nhiên cách này buộc instance layout bị hủy và tạo lại giữa các visit.

<CodeGroup>
  ```vue Vue icon="vuejs" theme={null}
  <script setup>
  import Layout from './Layout'

  defineProps({ user: Object })
  </script>

  <template>
      <Layout>
          <h1>Welcome</h1>
          <p>Hello {{ user.name }}, welcome to your first Inertia app!</p>
      </Layout>
  </template>
  ```

  ```jsx React icon="react" theme={null}
  import Layout from './Layout'

  export default function Welcome({ user }) {
      return (
          <Layout>
              <h1>Welcome</h1>
              <p>Hello {user.name}, welcome to your first Inertia app!</p>
          </Layout>
      )
  }
  ```

  ```svelte Svelte icon="s" theme={null}
  <script>
      import Layout from './Layout.svelte'

      let { user } = $props()
  </script>

  <Layout>
      <h1>Welcome</h1>
      <p>Hello {user.name}, welcome to your first Inertia app!</p>
  </Layout>
  ```
</CodeGroup>

## Layout duy trì xuyên suốt

Bao page bằng layout dưới dạng child component vẫn hoạt động, nhưng đồng nghĩa layout bị hủy và tạo lại ở mọi visit. Điều này ngăn duy trì layout state qua các lần điều hướng, chẳng hạn audio player cần tiếp tục phát hoặc sidebar cần giữ vị trí cuộn.

Persistent layout giải quyết vấn đề này bằng cách cho Inertia biết layout nào được dùng cho page. Inertia quản lý instance layout riêng và giữ nó sống giữa các visit.

<CodeGroup>
  ```vue Vue icon="vuejs" theme={null}
  <script>
  import Layout from './Layout'

  export default {
      layout: Layout,
  }
  </script>

  <script setup>
  defineProps({ user: Object })
  </script>

  <template>
      <h1>Welcome</h1>
      <p>Hello {{ user.name }}, welcome to your first Inertia app!</p>
  </template>
  ```

  ```jsx React icon="react" theme={null}
  import Layout from './Layout'

  const Welcome = ({ user }) => {
      return (
          <>
              <h1>Welcome</h1>
              <p>Hello {user.name}, welcome to your first Inertia app!</p>
          </>
      )
  }

  Welcome.layout = (page) => <Layout>{page}</Layout>

  export default Welcome
  ```

  ```svelte Svelte icon="s" theme={null}
  <script module>
      export { default as layout } from './Layout.svelte'
  </script>

  <script>
      let { user } = $props()
  </script>

  <h1>Welcome</h1>
  <p>Hello {user.name}, welcome to your first Inertia app!</p>
  ```
</CodeGroup>

<VueSpecific>
  Người dùng Vue 3.3+ cũng có thể dùng [defineOptions](https://vuejs.org/api/sfc-script-setup.html#defineoptions) để định nghĩa layout bên trong `<script setup>`:

  ```vue theme={null}
  <script setup>
  import Layout from './Layout'
  defineOptions({ layout: Layout })
  </script>
  ```
</VueSpecific>

<ReactSpecific>
  Arrow-function component nên được bọc trong một mảng. Nếu không có mảng, Inertia không thể phân biệt chúng với render function tại runtime:

  ```jsx theme={null}
  const ArrowLayout = ({ children }) => <main>{children}</main>

  Welcome.layout = [ArrowLayout]
  ```
</ReactSpecific>

### Nested layouts

Bạn có thể tạo bố cục layout phức tạp hơn bằng nested layout. Truyền một mảng layout component để bao page qua nhiều lớp.

<CodeGroup>
  ```vue Vue icon="vuejs" theme={null}
  <script>
  import SiteLayout from './SiteLayout'
  import NestedLayout from './NestedLayout'

  export default {
      layout: [SiteLayout, NestedLayout],
  }
  </script>

  <script setup>
  defineProps({ user: Object })
  </script>

  <template>
      <h1>Welcome</h1>
      <p>Hello {{ user.name }}, welcome to your first Inertia app!</p>
  </template>
  ```

  ```jsx React icon="react" theme={null}
  import SiteLayout from './SiteLayout'
  import NestedLayout from './NestedLayout'

  const Welcome = ({ user }) => {
      return (
          <>
              <h1>Welcome</h1>
              <p>Hello {user.name}, welcome to your first Inertia app!</p>
          </>
      )
  }

  Welcome.layout = [SiteLayout, NestedLayout]

  export default Welcome
  ```

  ```svelte Svelte icon="s" theme={null}
  <script module>
      import SiteLayout from './SiteLayout.svelte'
      import NestedLayout from './NestedLayout.svelte'

      export const layout = [SiteLayout, NestedLayout]
  </script>

  <script>
      let { user } = $props()
  </script>

  <h1>Welcome</h1>
  <p>Hello {user.name}, welcome to your first Inertia app!</p>
  ```
</CodeGroup>

## Layout mặc định

Tùy chọn `layout` trong `createInertiaApp` cho phép định nghĩa layout mặc định cho mọi page, tránh phải khai báo ở từng page. Layout theo từng page luôn ưu tiên hơn layout mặc định.

```js theme={null}
import Layout from './Layout'

createInertiaApp({
    layout: () => Layout,
    // ...
})
```

Bạn cũng có thể trả layout có điều kiện dựa trên tên page. Ví dụ có thể muốn loại các public page khỏi layout mặc định.

```js theme={null}
import Layout from './Layout'

createInertiaApp({
    layout: (name) => {
        if (name.startsWith('Public/')) {
            return null
        }

        return Layout
    },
    // ...
})
```

Full page object cũng có sẵn dưới dạng đối số thứ hai, cho phép truy cập URL, props và metadata khác của page.

Callback `layout` hỗ trợ mọi định dạng layout, bao gồm mảng cho [nested layouts](#nested-layouts), object có tên cho [named layouts](#targeting-named-layouts), và tuple cho [static props](#static-props).

### Sử dụng Resolve callback

Bạn cũng có thể đặt layout mặc định bên trong callback `resolve` bằng cách mutate page component đã resolve. Callback nhận tên component và full page object, hữu ích khi cần áp dụng layout có điều kiện dựa trên page data.

<CodeGroup>
  ```js Vue icon="vuejs" theme={null}
  import Layout from './Layout'

  createInertiaApp({
      resolve: (name) => {
          const pages = import.meta.glob('./Pages/**/*.vue', { eager: true })
          let page = pages[`./Pages/${name}.vue`]
          page.default.layout = page.default.layout || Layout
          return page
      },
      // ...
  })
  ```

  ```jsx React icon="react" theme={null}
  import Layout from './Layout'

  createInertiaApp({
      resolve: (name) => {
          const pages = import.meta.glob('./Pages/**/*.jsx', { eager: true })
          let page = pages[`./Pages/${name}.jsx`]
          page.default.layout = page.default.layout || ((page) => <Layout>{page}</Layout>)
          return page
      },
      // ...
  })
  ```

  ```js Svelte icon="s" theme={null}
  import Layout from './Layout'

  createInertiaApp({
      resolve: (name) => {
          const pages = import.meta.glob('./Pages/**/*.svelte', { eager: true })
          let page = pages[`./Pages/${name}.svelte`]
          return { default: page.default, layout: page.layout || Layout }
      },
      // ...
  })
  ```
</CodeGroup>

## Layout props

Persistent layout thường cần dữ liệu động từ page hiện tại, chẳng hạn page title, navigation item đang active hoặc trạng thái bật/tắt sidebar. Layout prop cung cấp cách định nghĩa giá trị mặc định trong layout và override từ bất kỳ page nào.

### Định nghĩa giá trị mặc định

Layout prop được định nghĩa như component prop thông thường với giá trị mặc định.

<CodeGroup>
  ```vue Vue icon="vuejs" theme={null}
  <script setup>
  const props = withDefaults(defineProps<{
      title?: string
      showSidebar?: boolean
  }>(), {
      title: 'My App',
      showSidebar: true,
  })
  </script>

  <template>
      <header>{{ title }}</header>
      <aside v-if="showSidebar">Sidebar</aside>
      <main>
          <slot />
      </main>
  </template>
  ```

  ```jsx React icon="react" theme={null}
  export default function Layout({ title = 'My App', showSidebar = true, children }) {
      return (
          <>
              <header>{title}</header>
              {showSidebar && <aside>Sidebar</aside>}
              <main>{children}</main>
          </>
      )
  }
  ```

  ```svelte Svelte icon="s" theme={null}
  <script>
  let { title = 'My App', showSidebar = true, children } = $props()
  </script>

  <header>{title}</header>
  {#if showSidebar}
      <aside>Sidebar</aside>
  {/if}
  <main>
      {@render children()}
  </main>
  ```
</CodeGroup>

### Static props

Bạn có thể truyền static prop trực tiếp trong định nghĩa persistent layout bằng tuple. Các prop này được đặt một lần khi layout được định nghĩa và không thay đổi giữa các lần điều hướng page.

<CodeGroup>
  ```vue Vue icon="vuejs" theme={null}
  <script setup>
  import Layout from './Layout'

  defineProps({ user: Object })
  defineOptions({
      layout: [Layout, { title: 'Dashboard' }],
  })
  </script>

  <template>
      <h1>Dashboard</h1>
  </template>
  ```

  ```jsx React icon="react" theme={null}
  import Layout from './Layout'

  const Dashboard = ({ user }) => {
      return <h1>Dashboard</h1>
  }

  Dashboard.layout = [Layout, { title: 'Dashboard' }]

  export default Dashboard
  ```

  ```svelte Svelte icon="s" theme={null}
  <script module>
      import Layout from './Layout.svelte'

      export const layout = [Layout, { title: 'Dashboard' }]
  </script>

  <script>
      let { user } = $props()
  </script>

  <h1>Dashboard</h1>
  ```
</CodeGroup>

### Callback props

Đôi khi layout prop cần được suy ra từ prop của page hiện tại. Callback function nhận page props và trả về layout definition với static prop đã tính toán.

<CodeGroup>
  ```vue Vue icon="vuejs" theme={null}
  <script setup>
  import Layout from './Layout'

  defineOptions({
      layout: (props) => [Layout, { title: 'Profile: ' + props.auth.user.name }],
  })
  </script>

  <template>
      <h1>Profile</h1>
  </template>
  ```

  ```jsx React icon="react" theme={null}
  import Layout from './Layout'

  const Profile = () => {
      return <h1>Profile</h1>
  }

  Profile.layout = (props) => [Layout, { title: 'Profile: ' + props.auth.user.name }]

  export default Profile
  ```

  ```svelte Svelte icon="s" theme={null}
  <script module>
      import Layout from './Layout.svelte'

      export const layout = (props) => [Layout, { title: 'Profile: ' + props.auth.user.name }]
  </script>

  <h1>Profile</h1>
  ```
</CodeGroup>

Callback nhận props của page và có thể trả bất kỳ định dạng layout hợp lệ nào: một component, tuple kèm static props, mảng cho nested layouts hoặc named layout object. Người dùng TypeScript có thể dùng type [`LayoutCallback`](/v3/advanced/typescript#layout-callbacks) để đảm bảo type safety.

#### Chỉ trả props

Khi [default layout](#default-layouts) được cấu hình trong `createInertiaApp`, callback có thể trả plain props object thay vì full layout definition. Inertia tự động dùng default layout và merge các prop được trả vào layout đó.

<CodeGroup>
  ```vue Vue icon="vuejs" theme={null}
  <script setup>
  defineOptions({
      layout: (props) => ({ title: 'Profile: ' + props.auth.user.name, showSidebar: false }),
  })
  </script>

  <template>
      <h1>Profile</h1>
  </template>
  ```

  ```jsx React icon="react" theme={null}
  const Profile = () => {
      return <h1>Profile</h1>
  }

  Profile.layout = (props) => ({ title: 'Profile: ' + props.auth.user.name, showSidebar: false })

  export default Profile
  ```

  ```svelte Svelte icon="s" theme={null}
  <script module>
      export const layout = (props) => ({
          title: 'Profile: ' + props.auth.user.name,
          showSidebar: false,
      })
  </script>

  <h1>Profile</h1>
  ```
</CodeGroup>

Bạn cũng có thể dùng static object khi prop không phụ thuộc page data.

```js theme={null}
Dashboard.layout = { title: 'Dashboard', showSidebar: true }
```

### Dynamic props

Bạn cũng có thể cập nhật layout prop động từ bất kỳ page component nào bằng hàm `setLayoutProps`. Người dùng TypeScript có thể [khai báo type cho các prop này](/v3/advanced/typescript#layout-props) trên toàn cục.

<CodeGroup>
  ```vue Vue icon="vuejs" theme={null}
  <script setup>
  import { setLayoutProps } from '@inertiajs/vue3'

  setLayoutProps({
      title: 'Dashboard',
      showSidebar: false,
  })
  </script>

  <template>
      <h1>Dashboard</h1>
  </template>
  ```

  ```jsx React icon="react" theme={null}
  import { setLayoutProps } from '@inertiajs/react'

  export default function Dashboard() {
      setLayoutProps({
          title: 'Dashboard',
          showSidebar: false,
      })

      return <h1>Dashboard</h1>
  }
  ```

  ```svelte Svelte icon="s" theme={null}
  <script>
  import { setLayoutProps } from '@inertiajs/svelte'

  setLayoutProps({
      title: 'Dashboard',
      showSidebar: false,
  })
  </script>

  <h1>Dashboard</h1>
  ```
</CodeGroup>

### Nhắm đến named layouts

[Nested layouts](#nested-layouts) cũng có thể được định nghĩa dưới dạng named object thay vì mảng, cho phép nhắm tới layout cụ thể bằng prop.

<CodeGroup>
  ```vue Vue icon="vuejs" theme={null}
  <script>
  import AppLayout from './AppLayout'
  import ContentLayout from './ContentLayout'

  export default {
      layout: {
          app: AppLayout,
          content: ContentLayout,
      },
  }
  </script>
  ```

  ```jsx React icon="react" theme={null}
  import AppLayout from './AppLayout'
  import ContentLayout from './ContentLayout'

  Dashboard.layout = {
      app: AppLayout,
      content: ContentLayout,
  }
  ```

  ```svelte Svelte icon="s" theme={null}
  <script module>
      import AppLayout from './AppLayout.svelte'
      import ContentLayout from './ContentLayout.svelte'

      export const layout = {
          app: AppLayout,
          content: ContentLayout,
      }
  </script>
  ```
</CodeGroup>

Bạn có thể nhắm tới named layout cụ thể bằng cách truyền tên layout làm đối số đầu tiên của `setLayoutProps`.

<CodeGroup>
  ```js Vue icon="vuejs" theme={null}
  import { setLayoutProps } from '@inertiajs/vue3'

  setLayoutProps('sidebar', {
      collapsed: true,
  })
  ```

  ```js React icon="react" theme={null}
  import { setLayoutProps } from '@inertiajs/react'

  setLayoutProps('sidebar', {
      collapsed: true,
  })
  ```

  ```js Svelte icon="s" theme={null}
  import { setLayoutProps } from '@inertiajs/svelte'

  setLayoutProps('sidebar', {
      collapsed: true,
  })
  ```
</CodeGroup>

[Nested layouts](#nested-layouts) và named layout cũng có thể bao gồm static prop bằng cú pháp tuple.

```js theme={null}
// Nested layouts with static props
Dashboard.layout = [
    [AppLayout, { title: 'Dashboard' }],
    [ContentLayout, { padding: 'sm' }],
]

// Named layouts with static props
Dashboard.layout = {
    app: [AppLayout, { theme: 'dark' }],
    content: [ContentLayout, { padding: 'sm' }],
}
```

### Thứ tự ưu tiên khi merge

Layout prop được resolve từ nhiều nguồn theo thứ tự ưu tiên sau, từ cao xuống thấp:

1. **Dynamic props** - được đặt qua `setLayoutProps()`
2. **Static props** - được định nghĩa trong persistent layout definition (bao gồm [callback props](#callback-props))
3. **Defaults** - được khai báo là giá trị mặc định trên prop của layout component

### Tự động reset khi điều hướng

Dynamic layout prop tự động reset khi điều hướng tới page mới (trừ khi bật `preserveState`). Điều này đảm bảo mỗi page bắt đầu với trạng thái sạch và chỉ áp dụng các layout prop được page đó thiết lập rõ ràng.

### Reset props

Bạn cũng có thể reset thủ công toàn bộ dynamic layout prop bằng `resetLayoutProps`.

<CodeGroup>
  ```js Vue icon="vuejs" theme={null}
  import { resetLayoutProps } from '@inertiajs/vue3'

  resetLayoutProps()
  ```

  ```js React icon="react" theme={null}
  import { resetLayoutProps } from '@inertiajs/react'

  resetLayoutProps()
  ```

  ```js Svelte icon="s" theme={null}
  import { resetLayoutProps } from '@inertiajs/svelte'

  resetLayoutProps()
  ```
</CodeGroup>

***

## Tài liệu chính thức

Bài dịch này được đối chiếu từ [tài liệu Inertia.js v3 chính thức](https://inertiajs.com/docs/v3/the-basics/layouts). Nếu có khác biệt do phiên bản hoặc cập nhật mới, hãy ưu tiên tài liệu chính thức làm nguồn tham chiếu.
