> ## 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.

# Infinite Scroll

<Warning>Bạn đang xem tài liệu Inertia.js v2. Inertia.js v3 đã được phát hành và hiện là phiên bản mặc định. Hãy xem [hướng dẫn nâng cấp](/v3/getting-started/upgrade-guide) để bắt đầu.</Warning>

Tính năng infinite scroll của Inertia tải thêm các trang nội dung khi người dùng cuộn, thay thế control phân trang truyền thống. Tính năng này phù hợp cho giao diện chat, social feed, lưới ảnh và danh sách sản phẩm.

## Phía máy chủ

Để cấu hình dữ liệu phân trang cho infinite scroll, bạn nên dùng method `Inertia::scroll()` khi trả response. Method này tự động cấu hình merge behavior phù hợp và chuẩn hóa metadata phân trang cho component frontend.

```php theme={null}
Route::get('/users', function () {
    return Inertia::render('Users/Index', [
        'users' => Inertia::scroll(fn () => User::paginate())
    ]);
});
```

Method `Inertia::scroll()` hoạt động với các method `paginate()`, `simplePaginate()` và `cursorPaginate()` của Laravel, cũng như dữ liệu phân trang được bọc trong [Eloquent API resource](https://laravel.com/docs/eloquent-resources). Xem tài liệu [method Inertia::scroll()](#inertia-scroll-method) để biết thêm.

## Phía client

Ở phía client, Inertia cung cấp component `<InfiniteScroll>` để tự động tải thêm các trang nội dung. Component nhận prop `data` chỉ định key của prop chứa dữ liệu phân trang. Component `<InfiniteScroll>` cần bọc nội dung phụ thuộc vào dữ liệu này.

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

  defineProps(['users'])
  </script>

  <template>
      <InfiniteScroll data="users">
          <div v-for="user in users.data" :key="user.id">
              {{ user.name }}
          </div>
      </InfiniteScroll>
  </template>
  ```

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

  export default function Users({ users }) {
      return (
          <InfiniteScroll data="users">
              {users.data.map(user => (
                  <div key={user.id}>
                      {user.name}
                  </div>
              ))}
          </InfiniteScroll>
      )
  }
  ```

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

  <InfiniteScroll data="users">
      {#each users.data as user (user.id)}
          <div>{user.name}</div>
      {/each}
  </InfiniteScroll>
  ```
</CodeGroup>

Component sử dụng [Intersection Observer](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) để phát hiện khi người dùng cuộn gần cuối nội dung và tự động gửi request tải trang tiếp theo. Dữ liệu mới được merge với nội dung hiện có thay vì thay thế.

## Loading buffer

Bạn có thể kiểm soát việc tải nội dung bắt đầu sớm đến mức nào bằng khoảng buffer. Buffer chỉ định số pixel trước cuối nội dung mà quá trình tải cần bắt đầu.

<CodeGroup>
  ```vue Vue icon="vuejs" theme={null}
  <InfiniteScroll data="users" :buffer="500">
      <!-- ... -->
  </InfiniteScroll>
  ```

  ```jsx React icon="react" theme={null}
  <InfiniteScroll data="users" buffer={500}>
      {/* ... */}
  </InfiniteScroll>
  ```

  ```svelte Svelte icon="s" theme={null}
  <InfiniteScroll data="users" buffer={500}>
      <!-- ... -->
  </InfiniteScroll>
  ```
</CodeGroup>

Trong ví dụ trên, nội dung bắt đầu tải khi còn cách cuối phần hiện tại 500 pixel. Buffer lớn hơn tải nội dung sớm hơn nhưng có thể tải cả nội dung người dùng không bao giờ xem.

## Đồng bộ URL

Component infinite scroll cập nhật query string URL của trình duyệt (`?page=...`) khi người dùng cuộn qua nội dung. URL phản ánh trang có nhiều item hiển thị nhất trên màn hình và cập nhật theo cả hai hướng khi cuộn lên hoặc xuống. Nhờ đó người dùng có thể bookmark hoặc chia sẻ liên kết đến trang cụ thể. Bạn có thể tắt hành vi này để giữ nguyên URL ban đầu.

<CodeGroup>
  ```vue Vue icon="vuejs" theme={null}
  <InfiniteScroll data="users" preserve-url>
      <!-- ... -->
  </InfiniteScroll>
  ```

  ```jsx React icon="react" theme={null}
  <InfiniteScroll data="users" preserveUrl>
      {/* ... */}
  </InfiniteScroll>
  ```

  ```svelte Svelte icon="s" theme={null}
  <InfiniteScroll data="users" preserve-url>
      <!-- ... -->
  </InfiniteScroll>
  ```
</CodeGroup>

Điều này hữu ích khi infinite scroll được dùng cho nội dung phụ không nên ảnh hưởng URL chính của trang, chẳng hạn comment của bài blog hoặc sản phẩm liên quan.

## Reset

Khi filter hoặc tham số khác thay đổi, bạn có thể cần reset dữ liệu infinite scroll để bắt đầu lại từ đầu. Nếu không reset, kết quả mới sẽ merge với nội dung hiện tại thay vì thay thế.

Bạn có thể reset dữ liệu bằng visit option `reset`.

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

  const show = (role) => {
      router.visit(route('users'), {
          data: { filter: { role } },
          only: ['users'],
          reset: ['users'],
      })
  }
  </script>

  <template>
      <button @click="show('admin')">Show admins</button>
      <button @click="show('customer')">Show customers</button>

      <InfiniteScroll data="users">
          <div v-for="user in users.data" :key="user.id">
              {{ user.name }}
          </div>
      </InfiniteScroll>
  </template>
  ```

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

  export default function Users({ users }) {
      const show = (role) => {
          router.visit(route('users'), {
              data: { filter: { role } },
              only: ['users'],
              reset: ['users'],
          })
      }

      return (

              <button onClick={() => show('admin')}>Show admins</button>
              <button onClick={() => show('customer')}>Show customers</button>

              <InfiniteScroll data="users">
                  {users.data.map(user => (
                      <div key={user.id}>
                          {user.name}
                      </div>
                  ))}
              </InfiniteScroll>

      )
  }
  ```

  ```svelte Svelte icon="s" theme={null}
  <script>
      import { InfiniteScroll, router } from '@inertiajs/svelte'
      export let users

      const show = (role) => {
          router.visit(route('users'), {
              data: { filter: { role } },
              only: ['users'],
              reset: ['users'],
          })
      }
  </script>

  <button on:click={() => show('admin')}>Show admins</button>
  <button on:click={() => show('customer')}>Show customers</button>

  <InfiniteScroll data="users">
      {#each users.data as user (user.id)}
          <div>{user.name}</div>
      {/each}
  </InfiniteScroll>
  ```
</CodeGroup>

Để biết thêm về option reset, xem tài liệu [Reset props](/v2/data-props/merging-props#resetting-props).

## Hướng tải dữ liệu

Component infinite scroll tải nội dung theo cả hai hướng khi bạn cuộn gần đầu hoặc cuối. Bạn có thể kiểm soát bằng các prop `only-next` và `only-previous`.

<CodeGroup>
  ```vue Vue icon="vuejs" theme={null}
  <!-- Only load the next page -->
  <InfiniteScroll data="users" only-next>
      <!-- ... -->
  </InfiniteScroll>

  <!-- Only load the previous page -->
  <InfiniteScroll data="messages" only-previous>
      <!-- ... -->
  </InfiniteScroll>

  <!-- Load in both directions (default) -->
  <InfiniteScroll data="posts">
      <!-- ... -->
  </InfiniteScroll>
  ```

  ```jsx React icon="react" theme={null}
  {/* Only load the next page */}
  <InfiniteScroll data="users" onlyNext>
      {/* ... */}
  </InfiniteScroll>

  {/* Only load the previous page */}
  <InfiniteScroll data="messages" onlyPrevious>
      {/* ... */}
  </InfiniteScroll>

  {/* Load in both directions (default) */}
  <InfiniteScroll data="posts">
      {/* ... */}
  </InfiniteScroll>
  ```

  ```svelte Svelte icon="s" theme={null}
  <!-- Only load the next page -->
  <InfiniteScroll data="users" only-next>
      <!-- ... -->
  </InfiniteScroll>

  <!-- Only load the previous page -->
  <InfiniteScroll data="messages" only-previous>
      <!-- ... -->
  </InfiniteScroll>

  <!-- Load in both directions (default) -->
  <InfiniteScroll data="posts">
      <!-- ... -->
  </InfiniteScroll>
  ```
</CodeGroup>

Option mặc định đặc biệt hữu ích khi người dùng bắt đầu ở một trang giữa và cần cuộn theo cả hai hướng để truy cập toàn bộ nội dung.

## Reverse mode

Với ứng dụng chat, timeline hoặc giao diện có nội dung sắp xếp giảm dần (item mới nhất ở dưới cùng), bạn có thể bật reverse mode. Chế độ này cấu hình component tải nội dung cũ hơn khi cuộn lên.

<CodeGroup>
  ```vue Vue icon="vuejs" theme={null}
  <InfiniteScroll data="messages" reverse>
      <!-- ... -->
  </InfiniteScroll>
  ```

  ```jsx React icon="react" theme={null}
  <InfiniteScroll data="messages" reverse>
      {/* ... */}
  </InfiniteScroll>
  ```

  ```svelte Svelte icon="s" theme={null}
  <InfiniteScroll data="messages" reverse>
      <!-- ... -->
  </InfiniteScroll>
  ```
</CodeGroup>

Trong reverse mode, component đảo hướng tải: cuộn lên tải trang kế tiếp (nội dung cũ hơn), cuộn xuống tải trang trước (nội dung mới hơn). Component xử lý vị trí khi loading, nhưng bạn chịu trách nhiệm đảo thứ tự nội dung để hiển thị đúng.

Reverse mode cũng tự động cuộn xuống cuối ở lần tải đầu tiên; bạn có thể tắt bằng `:auto-scroll="false"`.

<CodeGroup>
  ```vue Vue icon="vuejs" theme={null}
  <InfiniteScroll data="messages" reverse :auto-scroll="false">
      <!-- ... -->
  </InfiniteScroll>
  ```

  ```jsx React icon="react" theme={null}
  <InfiniteScroll data="messages" reverse autoScroll={false}>
      {/* ... */}
  </InfiniteScroll>
  ```

  ```svelte Svelte icon="s" theme={null}
  <InfiniteScroll data="messages" reverse auto-scroll={false}>
      <!-- ... -->
  </InfiniteScroll>
  ```
</CodeGroup>

## Manual mode

Manual mode tắt tự động tải khi cuộn và cho phép bạn kiểm soát thời điểm tải nội dung qua slot `next` và `previous`. Để biết chi tiết property slot và option tùy chỉnh, xem [Slots](#slots).

<CodeGroup>
  ```vue Vue icon="vuejs" theme={null}
  <template>
      <InfiniteScroll data="users" manual>
          <template #previous="{ loading, fetch, hasMore }">
              <button v-if="hasMore" @click="fetch" :disabled="loading">
                  {{ loading ? 'Loading...' : 'Load previous' }}
              </button>
          </template>

          <!-- Your content -->

          <template #next="{ loading, fetch, hasMore }">
              <button v-if="hasMore" @click="fetch" :disabled="loading">
                  {{ loading ? 'Loading...' : 'Load more' }}
              </button>
          </template>
      </InfiniteScroll>
  </template>
  ```

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

  export default ({ users }) => (
      <InfiniteScroll
          data="users"
          manual
          previous={({ loading, fetch, hasMore }) => (
              hasMore && (
                  <button onClick={fetch} disabled={loading}>
                      {loading ? 'Loading...' : 'Load previous'}
                  </button>
              )
          )}
          next={({ loading, fetch, hasMore }) => (
              hasMore && (
                  <button onClick={fetch} disabled={loading}>
                      {loading ? 'Loading...' : 'Load more'}
                  </button>
              )
          )}
      >
          {users.data.map(user => (
              <div key={user.id}>{user.name}</div>
          ))}
      </InfiniteScroll>
  )
  ```

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

  <InfiniteScroll data="users" manual>
      <div slot="previous" let:hasMore let:fetch let:loading>
          {#if hasMore}
              <button on:click={fetch} disabled={loading}>
                  {loading ? 'Loading...' : 'Load previous'}
              </button>
          {/if}
      </div>

      {#each users.data as user (user.id)}
          <div>{user.name}</div>
      {/each}

      <div slot="next" let:hasMore let:fetch let:loading>
          {#if hasMore}
              <button on:click={fetch} disabled={loading}>
                  {loading ? 'Loading...' : 'Load more'}
              </button>
          {/if}
      </div>
  </InfiniteScroll>
  ```
</CodeGroup>

Bạn cũng có thể cấu hình component tự chuyển sang manual mode sau một số lượng trang nhất định bằng prop `manualAfter`.

<CodeGroup>
  ```vue Vue icon="vuejs" theme={null}
  <InfiniteScroll data="users" :manual-after="3">
      <!-- ... -->
  </InfiniteScroll>
  ```

  ```jsx React icon="react" theme={null}
  <InfiniteScroll data="users" manualAfter={3}>
      {/* ... */}
  </InfiniteScroll>
  ```

  ```svelte Svelte icon="s" theme={null}
  <InfiniteScroll data="users" manual-after={3}>
      <!-- ... -->
  </InfiniteScroll>
  ```
</CodeGroup>

## Slots

Component infinite scroll cung cấp nhiều slot để tùy chỉnh trải nghiệm loading. Các slot cho phép hiển thị loading indicator tùy chỉnh và tạo control tải thủ công. Mỗi slot nhận property cung cấp thông tin trạng thái loading và function kích hoạt tải nội dung.

### Default slot

Khu vực nội dung chính nơi bạn render item dữ liệu. Slot này nhận thông tin trạng thái loading.

<CodeGroup>
  ```vue Vue icon="vuejs" theme={null}
  <InfiniteScroll data="users" #default="{ loading, loadingPrevious, loadingNext }">
      <!-- Your content with access to loading states -->
  </InfiniteScroll>
  ```

  ```jsx React icon="react" theme={null}
  <InfiniteScroll data="users">
      {({ loading, loadingPrevious, loadingNext }) => (
          <div>{/* Your content with access to loading states */}</div>
      )}
  </InfiniteScroll>
  ```

  ```svelte Svelte icon="s" theme={null}
  <InfiniteScroll data="users" let:loading let:loadingPrevious let:loadingNext>
      <!-- Your content with access to loading states -->
  </InfiniteScroll>
  ```
</CodeGroup>

### Loading slot

Loading slot được dùng làm fallback khi đang tải nội dung và không có custom slot `before` hoặc `after`. Nó tạo loading indicator mặc định.

<CodeGroup>
  ```vue Vue icon="vuejs" theme={null}
  <template>
      <InfiniteScroll data="users">
          <!-- Your content -->

          <template #loading>
              Loading more users...
          </template>
      </InfiniteScroll>
  </template>
  ```

  ```jsx React icon="react" theme={null}
  <InfiniteScroll data="users" loading={() => "Loading more users..."}>
      {/* Your content */}
  </InfiniteScroll>
  ```

  ```svelte Svelte icon="s" theme={null}
  <InfiniteScroll data="users">
      <!-- Your content -->

      <div slot="loading">
          Loading more users...
      </div>
  </InfiniteScroll>
  ```
</CodeGroup>

### Slot Previous và Next

Slot `previous` và `next` được render phía trên và dưới nội dung chính, thường dùng làm control tải thủ công. Các slot nhận nhiều property gồm trạng thái loading, function fetch và chỉ báo mode.

```vue theme={null}
<template>
    <InfiniteScroll data="users" :manual-after="3">
        <template #previous="{ loading, fetch, hasMore, manualMode }">
            <button v-if="manualMode && hasMore" @click="fetch" :disabled="loading">
                {{ loading ? 'Loading...' : 'Load previous' }}
            </button>
        </template>

        <!-- Your content -->

        <template #next="{ loading, fetch, hasMore, manualMode }">
            <button v-if="manualMode && hasMore" @click="fetch" :disabled="loading">
                {{ loading ? 'Loading...' : 'Load more' }}
            </button>
        </template>
    </InfiniteScroll>
</template>
```

Các slot `loading`, `previous` và `next` nhận những property sau:

| Property          | Mô tả                                          |
| :---------------- | :--------------------------------------------- |
| `loading`         | Slot hiện có đang tải nội dung hay không       |
| `loadingPrevious` | Nội dung phía trước có đang được tải hay không |
| `loadingNext`     | Nội dung tiếp theo có đang được tải hay không  |
| `fetch`           | Function kích hoạt tải cho slot                |
| `hasMore`         | Slot còn nội dung để tải hay không             |
| `hasPrevious`     | Còn nội dung phía trước hay không              |
| `hasNext`         | Còn nội dung tiếp theo hay không               |
| `manualMode`      | Manual mode có đang active hay không           |
| `autoMode`        | Chế độ tự động tải có đang active hay không    |

## Custom element

Component `InfiniteScroll` render dưới dạng phần tử `<div>`. Bạn có thể tùy chỉnh thành bất kỳ element HTML nào bằng prop `as`.

<CodeGroup>
  ```vue Vue icon="vuejs" theme={null}
  <template>
      <InfiniteScroll data="products" as="ul">
          <li v-for="product in products.data" :key="product.id">
              {{ product.name }}
          </li>
      </InfiniteScroll>
  </template>
  ```

  ```jsx React icon="react" theme={null}
  <InfiniteScroll data="products" as="ul">
      {products.data.map(product => (
          <li key={product.id}>
              {product.name}
          </li>
      ))}
  </InfiniteScroll>
  ```

  ```svelte Svelte icon="s" theme={null}
  <InfiniteScroll data="products" as="ul">
      {#each products.data as product (product.id)}
          <li>{product.name}</li>
      {/each}
  </InfiniteScroll>
  ```
</CodeGroup>

## Target element

Component infinite scroll tự động theo dõi nội dung và gán số trang cho element để [đồng bộ URL](#url-synchronization). Khi item dữ liệu không phải child trực tiếp của root element của component, bạn cần chỉ định element thực sự chứa item bằng prop `itemsElement`.

<CodeGroup>
  ```vue Vue icon="vuejs" theme={null}
  <template>
      <InfiniteScroll data="users" items-element="#table-body">
          <table>
              <thead>
                  <tr><th>Name</th></tr>
              </thead>
              <tbody id="table-body">
                  <tr v-for="user in users.data" :key="user.id">
                      <td>{{ user.name }}</td>
                  </tr>
              </tbody>
          </table>
      </InfiniteScroll>
  </template>
  ```

  ```jsx React icon="react" theme={null}
  <InfiniteScroll data="users" itemsElement="#table-body">
      <table>
          <thead>
              <tr><th>Name</th></tr>
          </thead>
          <tbody id="table-body">
              {users.data.map(user => (
                  <tr key="{user.id}">
                      <td>{user.name}</td>
                  </tr>
              ))}
          </tbody>
      </table>
  </InfiniteScroll>
  ```

  ```svelte Svelte icon="s" theme={null}
  <InfiniteScroll data="users" items-element="#table-body">
      <table>
          <thead>
              <tr><th>Name</th></tr>
          </thead>
          <tbody id="table-body">
              {#each users.data as user (user.id)}
              <tr>
                  <td>{user.name}</td>
              </tr>
              {/each}
          </tbody>
      </table>
  </InfiniteScroll>
  ```
</CodeGroup>

Trong ví dụ này, component theo dõi element `#table-body` và tự động gắn số trang cho từng `<tr>` khi nội dung mới được tải. Điều này cho phép URL cập nhật chính xác dựa trên nội dung của trang nào đang hiển thị nhiều nhất trong viewport.

Bạn cũng có thể chỉ định custom trigger element để tải thêm nội dung bằng CSS selector. Khi đó các trigger element mặc định sẽ không được render và Intersection Observer sẽ theo dõi custom element thay thế.

<CodeGroup>
  ```vue Vue icon="vuejs" theme={null}
  <template>
      <InfiniteScroll
          data="users"
          items-element="#table-body"
          start-element="#table-header"
          end-element="#table-footer"
          >
          <table>
              <thead id="table-header">
                  <tr><th>Name</th></tr>
              </thead>
              <tbody id="table-body">
                  <tr v-for="user in users.data" :key="user.id">
                      <td>{{ user.name }}</td>
                  </tr>
              </tbody>
              <tfoot id="table-footer">
                  <tr><td>Footer</td></tr>
              </tfoot>
          </table>
      </InfiniteScroll>
  </template>
  ```

  ```jsx React icon="react" theme={null}
  <InfiniteScroll
      data="users"
      itemsElement="#table-body"
      startElement="#table-header"
      endElement="#table-footer"
  >
      <table>
          <thead id="table-header">
              <tr><th>Name</th></tr>
          </thead>
          <tbody id="table-body">
              {users.data.map(user => (
                  <tr key={user.id}>
                      <td>{user.name}</td>
                  </tr>
              ))}
          </tbody>
          <tfoot id="table-footer">
              <tr><td>Footer</td></tr>
          </tfoot>
      </table>
  </InfiniteScroll>
  ```

  ```svelte Svelte icon="s" theme={null}
  <InfiniteScroll
      data="users"
      items-element="#table-body"
      start-element="#table-header"
      end-element="#table-footer"
  >
      <table>
          <thead id="table-header">
              <tr><th>Name</th></tr>
          </thead>
          <tbody id="table-body">
              {#each users.data as user (user.id)}
                  <tr>
                      <td>{user.name}</td>
                  </tr>
              {/each}
          </tbody>
          <tfoot id="table-footer">
              <tr><td>Footer</td></tr>
          </tfoot>
      </table>
  </InfiniteScroll>
  ```
</CodeGroup>

Ngoài ra, bạn có thể dùng template ref thay cho CSS selector. Cách này tránh phải thêm HTML attribute và cung cấp tham chiếu element trực tiếp.

<CodeGroup>
  ```vue Vue icon="vuejs" theme={null}
  <script setup>
  import { ref } from 'vue'
  const tableHeader = ref()
  const tableFooter = ref()
  const tableBody = ref()
  </script>

  <template>
      <InfiniteScroll
          data="users"
          :items-element="() => tableBody"
          :start-element="() => tableHeader"
          :end-element="() => tableFooter"
      >
          <table>
              <thead ref="tableHeader">
                  <tr><th>Name</th></tr>
              </thead>
              <tbody ref="tableBody">
                  <tr v-for="user in users.data" :key="user.id">
                      <td>{{ user.name }}</td>
                  </tr>
              </tbody>
              <tfoot ref="tableFooter">
                  <tr><td>Footer</td></tr>
              </tfoot>
          </table>
      </InfiniteScroll>
  </template>
  ```

  ```jsx React icon="react" theme={null}
  import { useRef } from 'react'

  export default ({ users }) => {
      const tableHeader = useRef()
      const tableFooter = useRef()
      const tableBody = useRef()

      return (
          <InfiniteScroll
              data="users"
              itemsElement={tableBody}
              startElement={tableHeader}
              endElement={tableFooter}
  >
              <table>
                  <thead ref={tableHeader}>
                      <tr><th>Name</th></tr>
                  </thead>
                  <tbody ref={tableBody}>
                      {users.data.map(user => (
                          <tr key={user.id}>
                              <td>{user.name}</td>
                          </tr>
                      ))}
                  </tbody>
                  <tfoot ref={tableFooter}>
                      <tr><td>Footer</td></tr>
                  </tfoot>
              </table>
          </InfiniteScroll>
      )
  }
  ```

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

      let tableHeader
      let tableFooter
      let tableBody
  </script>

  <InfiniteScroll
      data="users"
      items-element={() => tableBody}
      start-element={() => tableHeader}
      end-element={() => tableFooter}
  >
      <table>
          <thead bind:this={tableHeader}>
              <tr><th>Name</th></tr>
          </thead>
          <tbody bind:this={tableBody}>
              {#each users.data as user (user.id)}
                  <tr>
                      <td>{user.name}</td>
                  </tr>
              {/each}
          </tbody>
          <tfoot bind:this={tableFooter}>
              <tr><td>Footer</td></tr>
          </tfoot>
      </table>
  </InfiniteScroll>
  ```
</CodeGroup>

## Scroll container

Component infinite scroll hoạt động trong bất kỳ container có thể cuộn nào, không chỉ document chính. Component tự động thích nghi để dùng custom scroll container cho việc phát hiện trigger và tính toán thay cho scroll của document chính.

<CodeGroup>
  ```vue Vue icon="vuejs" theme={null}
  <template>
      <div style="height: 400px; overflow-y: auto;">
          <InfiniteScroll data="users">
              <div v-for="user in users.data" :key="user.id">
                  {{ user.name }}
              </div>
          </InfiniteScroll>
      </div>
  </template>
  ```

  ```jsx React icon="react" theme={null}
  <div style={{ height: '400px', overflowY: 'auto' }}>
      <InfiniteScroll data="users">
          {users.data.map(user => (
              <div key={user.id}>
                  {user.name}
              </div>
          ))}
      </InfiniteScroll>
  </div>
  ```

  ```svelte Svelte icon="s" theme={null}
  <div style="height: 400px; overflow-y: auto;">
      <InfiniteScroll data="users">
          {#each users.data as user (user.id)}
              <div>{user.name}</div>
          {/each}
      </InfiniteScroll>
  </div>
  ```
</CodeGroup>

### Nhiều scroll container

Đôi khi bạn cần render nhiều component infinite scroll trên cùng một trang. Tuy nhiên, nếu cả hai đều dùng query parameter `page` mặc định để [đồng bộ URL](#url-synchronization), chúng sẽ xung đột. Để giải quyết, hãy yêu cầu mỗi paginator dùng `pageName` riêng.

```php theme={null}
Route::get('/dashboard', function () {
    return Inertia::render('Dashboard', [
        'users' => Inertia::scroll(
            fn() => User::paginate(pageName: 'users')
        ),
        'orders' => Inertia::scroll(
            fn() => Order::paginate(pageName: 'orders')
        ),
    ]);
});
```

Method `Inertia::scroll()` tự động phát hiện `pageName` từ từng paginator, cho phép các scroll container duy trì pagination state độc lập. Kết quả URL sẽ có dạng `?users=2&orders=3` thay vì xung đột ở parameter `?page=`.

Để biết thêm về page name của pagination, xem [tài liệu Laravel](https://laravel.com/docs/pagination#multiple-paginator-instances-per-page).

## Truy cập bằng code

Khi cần kích hoạt action tải dữ liệu bằng code, bạn có thể dùng template ref.

<CodeGroup>
  ```vue Vue icon="vuejs" theme={null}
  <script setup>
  import { ref } from 'vue'
  const infiniteScrollRef = ref(null)

  const fetchNext = () => {
      infiniteScrollRef.value?.fetchNext()
  }
  </script>

  <template>
      <button @click="fetchNext">Load More</button>

      <InfiniteScroll ref="infiniteScrollRef" data="users" manual>
          <!-- Your content -->
      </InfiniteScroll>
  </template>
  ```

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

  export default ({ users }) => {
      const infiniteScrollRef = useRef(null)

      const fetchNext = () => {
          infiniteScrollRef.current?.fetchNext()
      }

      return (

              <button onClick={fetchNext}>Load More</button>

              <InfiniteScroll ref={infiniteScrollRef} data="users" manual>
                  {users.data.map(user => (
                      <div key={user.id}>{user.name}</div>
                  ))}
              </InfiniteScroll>

      )
  }
  ```

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

      let infiniteScrollRef

      const fetchNext = () => {
          infiniteScrollRef?.fetchNext()
      }
  </script>

  <button on:click={fetchNext}>Load More</button>

  <InfiniteScroll bind:this={infiniteScrollRef} data="users" manual>
      {#each users.data as user (user.id)}
          <div>{user.name}</div>
      {/each}
  </InfiniteScroll>
  ```
</CodeGroup>

Component expose các method sau:

* `fetchNext()` - Chủ động lấy trang tiếp theo
* `fetchPrevious()` - Chủ động lấy trang trước
* `hasNext()` - Có trang tiếp theo hay không
* `hasPrevious()` - Có trang trước hay không

## Method Inertia::scroll()

Method `Inertia::scroll()` cung cấp cấu hình phía máy chủ cho infinite scrolling. Nó tự động cấu hình merge behavior phù hợp để dữ liệu mới được append hoặc prepend vào nội dung hiện có thay vì thay thế, đồng thời chuẩn hóa pagination metadata cho component frontend.

```php theme={null}
// Works with all Laravel pagination methods...
Inertia::scroll(User::paginate(20));
Inertia::scroll(User::simplePaginate(20));
Inertia::scroll(User::cursorPaginate(20));

// Works with API resources...
Inertia::scroll(UserResource::collection(User::paginate(20)));
```

Nếu không dùng paginator của Laravel hoặc dùng transformation layer khác, bạn có thể sử dụng các đối số bổ sung mà `scroll()` hỗ trợ.

```php theme={null}
// Customize the data wrapper key (defaults to 'data')...
Inertia::scroll($customPaginatedData, wrapper: 'items');

// Provide custom metadata resolution...
Inertia::scroll($data, metadata: $metadataProvider);
```

Tham số metadata nhận instance `ProvidesScrollMetadata` hoặc callback trả về instance đó. Callback nhận tham số `$data`. Điều này hữu ích khi tích hợp thư viện pagination bên thứ ba như Fractal.

```php theme={null}
use League\Fractal\Resource\Collection;

class FractalScrollMetadata implements ProvidesScrollMetadata
{
    public function __construct(protected Collection $resource) {}
    public function getPageName(): string {}
    public function getPreviousPage(): int|string|null {}
    public function getNextPage(): int|string|null {}
    public function getCurrentPage(): int|string|null {}
}
```

Sau đó bạn có thể dùng custom metadata provider này trong scroll function.

```php theme={null}
// Using an instance directly
Inertia::scroll($data, metadata: new FractalScrollMetadata($data));

// Using a callback
Inertia::scroll(
    fn() => $this->transformData($data),
    metadata: fn($data) => new FractalScrollMetadata($data)
);
```

Để tránh lặp setup này trong nhiều controller, bạn có thể định nghĩa macro.

```php theme={null}
// In your AppServiceProvider's boot method
Inertia::macro('fractalScroll', function (Collection $data) {
    return Inertia::scroll(
        $data,
        metadata: fn(Collection $data) => new FractalScrollMetadata($data)
    );
});

// Then use it in your controllers
return Inertia::render('Users/Index', [
    'users' => Inertia::fractalScroll($fractalCollection)
]);
```

***

## 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 v2 chính thức](https://inertiajs.com/docs/v2/data-props/infinite-scroll). 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.
