diff --git a/resources/js/Components/Nav.jsx b/resources/js/Components/Nav.jsx index ff7f52e2..6504eb7d 100644 --- a/resources/js/Components/Nav.jsx +++ b/resources/js/Components/Nav.jsx @@ -174,6 +174,11 @@ const Nav = ({ className }) => { Load when visible +
  • + + Infinite scroll + +
  • Remembering state diff --git a/resources/js/Pages/infinite-scroll.jsx b/resources/js/Pages/infinite-scroll.jsx new file mode 100644 index 00000000..e970ee8b --- /dev/null +++ b/resources/js/Pages/infinite-scroll.jsx @@ -0,0 +1,1190 @@ +import { A, Code, CodeBlock, H1, H2, H3, Li, P, TabbedCode, Ul } from '@/Components' +import dedent from 'dedent-js' + +export const meta = { + title: 'Infinite Scroll', + links: [ + { url: '#top', name: 'Introduction' }, + { url: '#server-side', name: 'Server-side' }, + { url: '#client-side', name: 'Client-side' }, + { url: '#loading-buffer', name: 'Loading buffer' }, + { url: '#url-synchronization', name: 'URL synchronization' }, + { url: '#loading-direction', name: 'Loading direction' }, + { url: '#reverse-mode', name: 'Reverse mode' }, + { url: '#manual-mode', name: 'Manual mode' }, + { url: '#slots', name: 'Slots' }, + { url: '#custom-element', name: 'Custom element' }, + { url: '#element-targeting', name: 'Element targeting' }, + { url: '#scroll-containers', name: 'Scroll containers' }, + { url: '#programmatic-access', name: 'Programmatic access' }, + { url: '#inertia-scroll-method', name: 'Inertia::scroll() method' }, + ], +} + +export default function () { + return ( + <> +

    Infinite scroll

    +

    + Inertia's infinite scroll feature loads additional pages of content as users scroll, replacing traditional + pagination controls. This is great for applications like chat interfaces, social feeds, photo grids, and product + listings. +

    + +

    Server-side

    +

    + To configure your paginated data for infinite scrolling, you should use the Inertia::scroll() method + when returning your response. This method automatically configures the proper merge behavior and normalizes + pagination metadata for the frontend component. +

    + Inertia::scroll(fn () => User::paginate()) + ]); + }); + `} + /> +

    + The Inertia::scroll() method works with Laravel's paginate(),{' '} + simplePaginate(), and cursorPaginate() methods, as well as pagination data wrapped in{' '} + Eloquent API resources. For more details, check out + the Inertia::scroll() method documentation. +

    + +

    Client-side

    +

    + On the client side, Inertia provides the {''} component to automatically load + additional pages of content. The component accepts a data prop that specifies the key of the prop + containing your paginated data. The {''} component should wrap the content that + depends on the paginated data. +

    + +
    + {{ user.name }} +
    + + `, + }, + { + name: 'React', + language: 'jsx', + code: dedent` + import { InfiniteScroll } from '@inertiajs/react' + + export default function Users({ users }) { + return ( + + {users.data.map(user => ( +
    + {user.name} +
    + ))} +
    + ) + } + `, + }, + { + name: 'Svelte', + language: 'jsx', + code: dedent` + + + + {#each users.data as user (user.id)} +
    {user.name}
    + {/each} +
    + `, + }, + ]} + /> +

    + The component uses{' '} + + intersection observers + {' '} + to detect when users scroll near the end of the content and automatically triggers requests to load the next + page. New data is merged with existing content rather than replacing it. +

    +

    Loading buffer

    +

    + You can control how early content begins loading by setting a buffer distance. The buffer specifies how many + pixels before the end of the content loading should begin. +

    + + + + `, + }, + { + name: 'React', + language: 'jsx', + code: dedent` + + {/* ... */} + + `, + }, + { + name: 'Svelte', + language: 'jsx', + code: dedent` + + + + `, + }, + ]} + /> +

    + In the example above, content will start loading 500 pixels before reaching the end of the current content. A + larger buffer loads content earlier but potentially loads content that users may never see. +

    +

    URL synchronization

    +

    + The infinite scroll component updates the browser URL's query string (?page=...) as users scroll + through content. The URL reflects which page has the most visible items on screen, updating in both directions + as users scroll up or down. This allows users to bookmark or share links to specific pages. You can disable this + behavior to maintain the original page URL. +

    + + + + `, + }, + { + name: 'React', + language: 'jsx', + code: dedent` + + {/* ... */} + + `, + }, + { + name: 'Svelte', + language: 'jsx', + code: dedent` + + + + `, + }, + ]} + /> +

    + This is useful when infinite scroll is used for secondary content that shouldn't affect the main page URL, such + as comments on a blog post or related products on a product page. +

    +

    Loading direction

    +

    + The infinite scroll component loads content in both directions when you scroll near the start or end. You can + control this behavior using the only-next and only-previous props. +

    + + + + + + + + + + + + + + + `, + }, + { + name: 'React', + language: 'jsx', + code: dedent` + {/* Only load the next page */} + + {/* ... */} + + + {/* Only load the previous page */} + + {/* ... */} + + + {/* Load in both directions (default) */} + + {/* ... */} + + `, + }, + { + name: 'Svelte', + language: 'jsx', + code: dedent` + + + + + + + + + + + + + + + `, + }, + ]} + /> +

    + The default option is particularly useful when users start on a middle page and need to scroll in both + directions to access all content. +

    +

    Reverse mode

    +

    + For chat applications, timelines, or interfaces where content is sorted descendingly (newest items at the bottom), you + can enable reverse mode. This configures the component to load older content when scrolling upward. +

    + + + + `, + }, + { + name: 'React', + language: 'jsx', + code: dedent` + + {/* ... */} + + `, + }, + { + name: 'Svelte', + language: 'jsx', + code: dedent` + + + + `, + }, + ]} + /> +

    + In reverse mode, the component flips the loading directions so that scrolling up loads the next page (older + content) and scrolling down loads the previous page (newer content). The component handles the loading + positioning, but you are responsible for reversing your content to display in the correct order. +

    +

    + Reverse mode also enables automatic scrolling to the bottom on initial load, which you can disable with{' '} + :auto-scroll="false". +

    + + + + `, + }, + { + name: 'React', + language: 'jsx', + code: dedent` + + {/* ... */} + + `, + }, + { + name: 'Svelte', + language: 'jsx', + code: dedent` + + + + `, + }, + ]} + /> +

    Manual mode

    +

    + Manual mode disables automatic loading when scrolling and allows you to control when content loads through the{' '} + next and previous slots. For more details about available slot properties and + customization options, see the Slots documentation. +

    + + + + + + + + `, + }, + { + name: 'React', + language: 'jsx', + code: dedent` + import { InfiniteScroll } from '@inertiajs/react' + + export default ({ users }) => ( + ( + hasMore && ( + + ) + )} + next={({ loading, fetch, hasMore }) => ( + hasMore && ( + + ) + )} + > + {users.data.map(user => ( +
    {user.name}
    + ))} +
    + ) + `, + }, + { + name: 'Svelte', + language: 'jsx', + code: dedent` + + + +
    + {#if exposedPrevious.hasMore} + + {/if} +
    + + {#each users.data as user (user.id)} +
    {user.name}
    + {/each} + +
    + {#if exposedNext.hasMore} + + {/if} +
    +
    + `, + }, + ]} + /> +

    + You can also configure the component to automatically switch to manual mode after a certain number of pages + using the manualAfter prop. +

    + + + + `, + }, + { + name: 'React', + language: 'jsx', + code: dedent` + + {/* ... */} + + `, + }, + { + name: 'Svelte', + language: 'jsx', + code: dedent` + + + + `, + }, + ]} + /> +

    Slots

    +

    + The infinite scroll component provides several slots to customize the loading experience. These slots allow you + to display custom loading indicators and create manual load controls. Each slot receives properties that provide + loading state information and functions to trigger content loading. +

    +

    Default slot

    +

    The main content area where you render your data items. This slot receives loading state information.

    + + + + `, + }, + { + name: 'React', + language: 'jsx', + code: dedent` + + {({ loading, loadingPrevious, loadingNext }) => ( +
    {/* Your content with access to loading states */}
    + )} +
    + `, + }, + { + name: 'Svelte', + language: 'jsx', + code: dedent` + + + + `, + }, + ]} + /> + +

    Loading slot

    +

    + The loading slot is used as a fallback when loading content and no custom before or{' '} + after slots are provided. This creates a default loading indicator. +

    + + + + + `, + }, + { + name: 'React', + language: 'jsx', + code: dedent` + "Loading more users..."}> + {/* Your content */} + + `, + }, + { + name: 'Svelte', + language: 'jsx', + code: dedent` + + +
    + Loading more users... +
    +
    + `, + }, + ]} + /> + +

    Previous and next slots

    +

    + The previous and next slots are rendered above and below the main content, typically + used for manual load controls. These slots receive several properties including loading states, fetch functions, + and mode indicators. +

    + + + + + + + + `} + /> +

    + The loading, previous, and next slots receive the following properties: +

    +
      +
    • + loading - Whether the slot is currently loading content +
    • +
    • + loadingPrevious - Whether previous content is loading +
    • +
    • + loadingNext - Whether next content is loading +
    • +
    • + fetch - Function to trigger loading for the slot +
    • +
    • + hasMore - Whether more content is available for the slot +
    • +
    • + hasPrevious - Whether more previous content is available +
    • +
    • + hasNext - Whether more next content is available +
    • +
    • + manualMode - Whether manual mode is active +
    • +
    • + autoMode - Whether automatic loading is active +
    • +
    +

    Custom element

    +

    + The InfiniteScroll component renders as a {'

    '} element. You may + customize this to use any HTML element using the as prop. +

    + +
  • + {{ product.name }} +
  • + + `, + }, + { + name: 'React', + language: 'jsx', + code: dedent` + + {products.data.map(product => ( +
  • + {product.name} +
  • + ))} +
    + `, + }, + { + name: 'Svelte', + language: 'jsx', + code: dedent` + + {#each products.data as product (product.id)} +
  • {product.name}
  • + {/each} +
    + `, + }, + ]} + /> + +

    Element targeting

    +

    + The infinite scroll component automatically tracks content and assigns page numbers to elements for{' '} + URL synchronization. When your data items are not direct children of the + component's root element, you need to specify which element contains the actual data items using the{' '} + itemsElement prop. +

    + + + + + + + + + + +
    Name
    {{ user.name }}
    + + `, + }, + { + name: 'React', + language: 'jsx', + code: dedent` + + + + + + + {users.data.map(user => ( + + + + ))} + +
    Name
    {user.name}
    +
    + `, + }, + { + name: 'Svelte', + language: 'jsx', + code: dedent` + + + + + + + {#each users.data as user (user.id)} + + + + {/each} + +
    Name
    {user.name}
    +
    + `, + }, + ]} + /> +

    + In this example, the component monitors the #table-body element and automatically + tags each {''}{' '} with a page number as new content loads. This enables + proper URL updates based on which page's content is most visible in the viewport. +

    +

    + You can also specify custom trigger elements for loading more content using CSS selectors. This prevents the + default trigger elements from being rendered and uses intersection observers on your custom elements instead. +

    + + + + + + + + + + + + + +
    Name
    {{ user.name }}
    Footer
    + + `, + }, + { + name: 'React', + language: 'jsx', + code: dedent` + + + + + + + {users.data.map(user => ( + + + + ))} + + + + +
    Name
    {user.name}
    Footer
    +
    + `, + }, + { + name: 'Svelte', + language: 'jsx', + code: dedent` + + + + + + + {#each users.data as user (user.id)} + + + + {/each} + + + + +
    Name
    {user.name}
    Footer
    +
    + `, + }, + ]} + /> +

    + Alternatively, you can use template refs instead of CSS selectors. This avoids adding HTML attributes and + provides direct element references. +

    + + import { ref } from 'vue' + const tableHeader = ref() + const tableFooter = ref() + const tableBody = ref() + + + + `, + }, + { + name: 'React', + language: 'jsx', + code: dedent` + import { useRef } from 'react' + + export default ({ users }) => { + const tableHeader = useRef() + const tableFooter = useRef() + const tableBody = useRef() + + return ( + tableBody.current} + startElement={() => tableHeader.current} + endElement={() => tableFooter.current} + > + + + + + + {users.data.map(user => ( + + + + ))} + + + + +
    Name
    {user.name}
    Footer
    +
    + ) + } + `, + }, + { + name: 'Svelte', + language: 'jsx', + code: dedent` + + + tableBody} + start-element={() => tableHeader} + end-element={() => tableFooter} + > + + + + + + {#each users.data as user (user.id)} + + + + {/each} + + + + +
    Name
    {user.name}
    Footer
    +
    + `, + }, + ]} + /> +

    Scroll containers

    +

    + The infinite scroll component works within any scrollable container, not just the main document. The component + automatically adapts to use the custom scroll container for trigger detection and calculations instead of the + main document scroll. +

    + + +
    + {{ user.name }} +
    +
    + + `, + }, + { + name: 'React', + language: 'jsx', + code: dedent` +
    + + {users.data.map(user => ( +
    + {user.name} +
    + ))} +
    +
    + `, + }, + { + name: 'Svelte', + language: 'jsx', + code: dedent` +
    + + {#each users.data as user (user.id)} +
    {user.name}
    + {/each} +
    +
    + `, + }, + ]} + /> +

    Programmatic access

    +

    When you need to trigger loading actions programmatically, you may use a template ref.

    + + import { ref } from 'vue' + const infiniteScrollRef = ref(null) + + const fetchNext = () => { + infiniteScrollRef.value?.fetchNext() + } + + + + `, + }, + { + name: 'React', + language: 'jsx', + code: dedent` + import { InfiniteScroll } from '@inertiajs/react' + import { useRef } from 'react' + + export default ({ users }) => { + const infiniteScrollRef = useRef(null) + + const fetchNext = () => { + infiniteScrollRef.current?.fetchNext() + } + + return ( + <> + + + + {users.data.map(user => ( +
    {user.name}
    + ))} +
    + + ) + } + `, + }, + { + name: 'Svelte', + language: 'jsx', + code: dedent` + + + + + + {#each users.data as user (user.id)} +
    {user.name}
    + {/each} +
    + `, + }, + ]} + /> +

    The component exposes the following methods:

    +
      +
    • + fetchNext() - Manually fetch the next page +
    • +
    • + fetchPrevious() - Manually fetch the previous page +
    • +
    • + hasNext() - Whether there is a next page +
    • +
    • + hasPrevious() - Whether there is a previous page +
    • +
    + +

    Inertia::scroll() method

    +

    + The Inertia::scroll() method provides server-side configuration for infinite scrolling. It + automatically configures the proper merge behavior so that new data is appended or prepended to existing content + instead of replacing it, and normalizes pagination metadata for the frontend component. +

    + +

    + If you don't use Laravel's paginator or use a different transformation layer, you may use the additional + arguments that scroll() accepts. +

    + +

    + The metadata parameter accepts an instance of ProvidesScrollMetadata or a callback that returns + such an instance. The callback receives the $data parameter. This is useful when integrating with + third-party pagination libraries like Fractal. +

    + +

    You may then use this custom metadata provider in your scroll function.

    + $this->transformData($data), + metadata: fn ($data) => new FractalScrollMetadata($data) + ); + `} + /> +

    To avoid repeating this setup in multiple controllers, you may define a macro.

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