From 31b0df8b43b04125507122f3bcdc388ef541c2f8 Mon Sep 17 00:00:00 2001 From: Pascal Baljet Date: Tue, 10 Feb 2026 15:36:30 +0100 Subject: [PATCH 01/27] v3 --- docs.json | 167 +- v3/advanced/asset-versioning.mdx | 61 + v3/advanced/code-splitting.mdx | 91 + v3/advanced/error-handling.mdx | 172 ++ v3/advanced/events.mdx | 790 +++++++ v3/advanced/progress-indicators.mdx | 387 ++++ v3/advanced/scroll-management.mdx | 135 ++ v3/advanced/server-side-rendering.mdx | 418 ++++ v3/advanced/testing.mdx | 246 +++ v3/advanced/typescript.mdx | 380 ++++ v3/core-concepts/how-it-works.mdx | 21 + v3/core-concepts/the-protocol.mdx | 559 +++++ v3/core-concepts/who-is-it-for.mdx | 15 + v3/data-props/deferred-props.mdx | 189 ++ v3/data-props/flash-data.mdx | 269 +++ v3/data-props/infinite-scroll.mdx | 1065 +++++++++ v3/data-props/load-when-visible.mdx | 519 +++++ v3/data-props/merging-props.mdx | 148 ++ v3/data-props/once-props.mdx | 143 ++ v3/data-props/partial-reloads.mdx | 179 ++ v3/data-props/polling.mdx | 154 ++ v3/data-props/prefetching.mdx | 467 ++++ v3/data-props/remembering-state.mdx | 224 ++ v3/data-props/shared-data.mdx | 150 ++ v3/getting-started/demo-application.mdx | 29 + v3/getting-started/index.mdx | 17 + v3/getting-started/upgrade-guide.mdx | 104 + v3/installation/client-side-setup.mdx | 268 +++ v3/installation/community-adapters.mdx | 36 + v3/installation/server-side-setup.mdx | 113 + v3/security/authentication.mdx | 17 + v3/security/authorization.mdx | 31 + v3/security/csrf-protection.mdx | 97 + v3/security/history-encryption.mdx | 61 + v3/the-basics/file-uploads.mdx | 196 ++ v3/the-basics/forms.mdx | 2667 +++++++++++++++++++++++ v3/the-basics/links.mdx | 422 ++++ v3/the-basics/manual-visits.mdx | 1113 ++++++++++ v3/the-basics/pages.mdx | 490 +++++ v3/the-basics/redirects.mdx | 47 + v3/the-basics/responses.mdx | 239 ++ v3/the-basics/routing.mdx | 88 + v3/the-basics/title-and-meta.mdx | 285 +++ v3/the-basics/validation.mdx | 236 ++ v3/the-basics/view-transitions.mdx | 320 +++ 45 files changed, 13783 insertions(+), 42 deletions(-) create mode 100644 v3/advanced/asset-versioning.mdx create mode 100644 v3/advanced/code-splitting.mdx create mode 100644 v3/advanced/error-handling.mdx create mode 100644 v3/advanced/events.mdx create mode 100644 v3/advanced/progress-indicators.mdx create mode 100644 v3/advanced/scroll-management.mdx create mode 100644 v3/advanced/server-side-rendering.mdx create mode 100644 v3/advanced/testing.mdx create mode 100644 v3/advanced/typescript.mdx create mode 100644 v3/core-concepts/how-it-works.mdx create mode 100644 v3/core-concepts/the-protocol.mdx create mode 100644 v3/core-concepts/who-is-it-for.mdx create mode 100644 v3/data-props/deferred-props.mdx create mode 100644 v3/data-props/flash-data.mdx create mode 100644 v3/data-props/infinite-scroll.mdx create mode 100644 v3/data-props/load-when-visible.mdx create mode 100644 v3/data-props/merging-props.mdx create mode 100644 v3/data-props/once-props.mdx create mode 100644 v3/data-props/partial-reloads.mdx create mode 100644 v3/data-props/polling.mdx create mode 100644 v3/data-props/prefetching.mdx create mode 100644 v3/data-props/remembering-state.mdx create mode 100644 v3/data-props/shared-data.mdx create mode 100644 v3/getting-started/demo-application.mdx create mode 100644 v3/getting-started/index.mdx create mode 100644 v3/getting-started/upgrade-guide.mdx create mode 100644 v3/installation/client-side-setup.mdx create mode 100644 v3/installation/community-adapters.mdx create mode 100644 v3/installation/server-side-setup.mdx create mode 100644 v3/security/authentication.mdx create mode 100644 v3/security/authorization.mdx create mode 100644 v3/security/csrf-protection.mdx create mode 100644 v3/security/history-encryption.mdx create mode 100644 v3/the-basics/file-uploads.mdx create mode 100644 v3/the-basics/forms.mdx create mode 100644 v3/the-basics/links.mdx create mode 100644 v3/the-basics/manual-visits.mdx create mode 100644 v3/the-basics/pages.mdx create mode 100644 v3/the-basics/redirects.mdx create mode 100644 v3/the-basics/responses.mdx create mode 100644 v3/the-basics/routing.mdx create mode 100644 v3/the-basics/title-and-meta.mdx create mode 100644 v3/the-basics/validation.mdx create mode 100644 v3/the-basics/view-transitions.mdx diff --git a/docs.json b/docs.json index 17b5897..82b8e23 100644 --- a/docs.json +++ b/docs.json @@ -154,6 +154,89 @@ ] } ] + }, + { + "version": "3.x", + "groups": [ + { + "group": "Getting Started", + "pages": [ + "v3/getting-started/index", + "v3/getting-started/demo-application", + "v3/getting-started/upgrade-guide" + ] + }, + { + "group": "Installation", + "pages": [ + "v3/installation/server-side-setup", + "v3/installation/client-side-setup" + ] + }, + { + "group": "Core Concepts", + "pages": [ + "v3/core-concepts/who-is-it-for", + "v3/core-concepts/how-it-works", + "v3/core-concepts/the-protocol" + ] + }, + { + "group": "The Basics", + "pages": [ + "v3/the-basics/pages", + "v3/the-basics/responses", + "v3/the-basics/redirects", + "v3/the-basics/routing", + "v3/the-basics/title-and-meta", + "v3/the-basics/links", + "v3/the-basics/manual-visits", + "v3/the-basics/forms", + "v3/the-basics/file-uploads", + "v3/the-basics/validation", + "v3/the-basics/view-transitions" + ] + }, + { + "group": "Data & Props", + "pages": [ + "v3/data-props/shared-data", + "v3/data-props/flash-data", + "v3/data-props/partial-reloads", + "v3/data-props/deferred-props", + "v3/data-props/merging-props", + "v3/data-props/once-props", + "v3/data-props/polling", + "v3/data-props/prefetching", + "v3/data-props/load-when-visible", + "v3/data-props/infinite-scroll", + "v3/data-props/remembering-state" + ] + }, + { + "group": "Security", + "pages": [ + "v3/security/authentication", + "v3/security/authorization", + "v3/security/csrf-protection", + "v3/security/history-encryption" + ] + }, + { + "group": "Advanced", + "pages": [ + "v3/advanced/asset-versioning", + "v3/advanced/code-splitting", + "v3/advanced/error-handling", + "v3/advanced/events", + "v3/advanced/progress-indicators", + "v3/advanced/scroll-management", + "v3/advanced/server-side-rendering", + "v3/advanced/testing", + "v3/advanced/typescript" + ] + } + ] } ] }, @@ -183,167 +266,167 @@ "redirects": [ { "source": "/index", - "destination": "/v2/getting-started/index" + "destination": "/v3/getting-started/index" }, { "source": "/demo-application", - "destination": "/v2/getting-started/demo-application" + "destination": "/v3/getting-started/demo-application" }, { "source": "/upgrade-guide", - "destination": "/v2/getting-started/upgrade-guide" + "destination": "/v3/getting-started/upgrade-guide" }, { "source": "/server-side-setup", - "destination": "/v2/installation/server-side-setup" + "destination": "/v3/installation/server-side-setup" }, { "source": "/client-side-setup", - "destination": "/v2/installation/client-side-setup" + "destination": "/v3/installation/client-side-setup" }, { "source": "/who-is-it-for", - "destination": "/v2/core-concepts/who-is-it-for" + "destination": "/v3/core-concepts/who-is-it-for" }, { "source": "/how-it-works", - "destination": "/v2/core-concepts/how-it-works" + "destination": "/v3/core-concepts/how-it-works" }, { "source": "/the-protocol", - "destination": "/v2/core-concepts/the-protocol" + "destination": "/v3/core-concepts/the-protocol" }, { "source": "/pages", - "destination": "/v2/the-basics/pages" + "destination": "/v3/the-basics/pages" }, { "source": "/responses", - "destination": "/v2/the-basics/responses" + "destination": "/v3/the-basics/responses" }, { "source": "/redirects", - "destination": "/v2/the-basics/redirects" + "destination": "/v3/the-basics/redirects" }, { "source": "/routing", - "destination": "/v2/the-basics/routing" + "destination": "/v3/the-basics/routing" }, { "source": "/title-and-meta", - "destination": "/v2/the-basics/title-and-meta" + "destination": "/v3/the-basics/title-and-meta" }, { "source": "/links", - "destination": "/v2/the-basics/links" + "destination": "/v3/the-basics/links" }, { "source": "/manual-visits", - "destination": "/v2/the-basics/manual-visits" + "destination": "/v3/the-basics/manual-visits" }, { "source": "/forms", - "destination": "/v2/the-basics/forms" + "destination": "/v3/the-basics/forms" }, { "source": "/file-uploads", - "destination": "/v2/the-basics/file-uploads" + "destination": "/v3/the-basics/file-uploads" }, { "source": "/validation", - "destination": "/v2/the-basics/validation" + "destination": "/v3/the-basics/validation" }, { "source": "/view-transitions", - "destination": "/v2/the-basics/view-transitions" + "destination": "/v3/the-basics/view-transitions" }, { "source": "/shared-data", - "destination": "/v2/data-props/shared-data" + "destination": "/v3/data-props/shared-data" }, { "source": "/partial-reloads", - "destination": "/v2/data-props/partial-reloads" + "destination": "/v3/data-props/partial-reloads" }, { "source": "/deferred-props", - "destination": "/v2/data-props/deferred-props" + "destination": "/v3/data-props/deferred-props" }, { "source": "/merging-props", - "destination": "/v2/data-props/merging-props" + "destination": "/v3/data-props/merging-props" }, { "source": "/polling", - "destination": "/v2/data-props/polling" + "destination": "/v3/data-props/polling" }, { "source": "/prefetching", - "destination": "/v2/data-props/prefetching" + "destination": "/v3/data-props/prefetching" }, { "source": "/load-when-visible", - "destination": "/v2/data-props/load-when-visible" + "destination": "/v3/data-props/load-when-visible" }, { "source": "/infinite-scroll", - "destination": "/v2/data-props/infinite-scroll" + "destination": "/v3/data-props/infinite-scroll" }, { "source": "/remembering-state", - "destination": "/v2/data-props/remembering-state" + "destination": "/v3/data-props/remembering-state" }, { "source": "/authentication", - "destination": "/v2/security/authentication" + "destination": "/v3/security/authentication" }, { "source": "/authorization", - "destination": "/v2/security/authorization" + "destination": "/v3/security/authorization" }, { "source": "/csrf-protection", - "destination": "/v2/security/csrf-protection" + "destination": "/v3/security/csrf-protection" }, { "source": "/history-encryption", - "destination": "/v2/security/history-encryption" + "destination": "/v3/security/history-encryption" }, { "source": "/asset-versioning", - "destination": "/v2/advanced/asset-versioning" + "destination": "/v3/advanced/asset-versioning" }, { "source": "/code-splitting", - "destination": "/v2/advanced/code-splitting" + "destination": "/v3/advanced/code-splitting" }, { "source": "/error-handling", - "destination": "/v2/advanced/error-handling" + "destination": "/v3/advanced/error-handling" }, { "source": "/events", - "destination": "/v2/advanced/events" + "destination": "/v3/advanced/events" }, { "source": "/progress-indicators", - "destination": "/v2/advanced/progress-indicators" + "destination": "/v3/advanced/progress-indicators" }, { "source": "/scroll-management", - "destination": "/v2/advanced/scroll-management" + "destination": "/v3/advanced/scroll-management" }, { "source": "/server-side-rendering", - "destination": "/v2/advanced/server-side-rendering" + "destination": "/v3/advanced/server-side-rendering" }, { "source": "/testing", - "destination": "/v2/advanced/testing" + "destination": "/v3/advanced/testing" }, { "source": "/typescript", - "destination": "/v2/advanced/typescript" + "destination": "/v3/advanced/typescript" } ] -} +} \ No newline at end of file diff --git a/v3/advanced/asset-versioning.mdx b/v3/advanced/asset-versioning.mdx new file mode 100644 index 0000000..685a50b --- /dev/null +++ b/v3/advanced/asset-versioning.mdx @@ -0,0 +1,61 @@ +--- +title: Asset Versioning +--- + +One common challenge when building single-page apps is refreshing site assets when they've been changed. Thankfully, Inertia makes this easy by optionally tracking the current version of your site assets. When an asset changes, Inertia will automatically make a full page visit instead of a XHR visit on the next request. + +## Configuration + +To enable automatic asset refreshing, you need to tell Inertia the current version of your assets. This can be any arbitrary string (letters, numbers, or a file hash), as long as it changes when your assets have been updated. + +Typically, your application's asset version can be specified within the `version` method of the Inertia `HandleInertiaRequests` middleware. + +```php +class HandleInertiaRequests extends Middleware +{ + public function version(Request $request) + { + return parent::version($request); + } +} +``` + +Alternatively, the asset version can be provided manually using the `Inertia::version()` method. + +```php +use Inertia\Inertia; + +Inertia::version($version); +Inertia::version(fn () => $version); // Lazily... +``` + +## Cache Busting + +Asset refreshing in Inertia works on the assumption that a hard page visit will trigger your assets to reload. However, Inertia doesn't actually do anything to force this. Typically this is done with some form of cache busting. For example, appending a version query parameter to the end of your asset URLs. + +With Laravel's Vite integration, asset versioning is done automatically. If you're using Laravel Mix, you can do this automatically by enabling [versioning](https://laravel.com/docs/mix#versioning-and-cache-busting) in your `webpack.mix.js` file. + +## Manual Refreshing + +If you want to take asset refreshing into your control, you can return a fixed value from the `version` method in the `HandleInertiaRequests` middleware. This disables Inertia's automatic asset versioning. + +For example, if you want to notify users when a new version of your frontend is available, you can still expose the actual asset version to the frontend by including it as [shared data](/v3/data-props/shared-data). + +```php +class HandleInertiaRequests extends Middleware +{ + public function version(Request $request) + { + return null; + } + + public function share(Request $request) + { + return array_merge(parent::share($request), [ + 'version' => parent::version($request), + ]); + } +} +``` + +On the frontend, you can watch the `version` property and show a notification when a new version is detected. diff --git a/v3/advanced/code-splitting.mdx b/v3/advanced/code-splitting.mdx new file mode 100644 index 0000000..0c2592a --- /dev/null +++ b/v3/advanced/code-splitting.mdx @@ -0,0 +1,91 @@ +--- +title: Code Splitting +--- + +Code splitting breaks apart the various pages of your application into smaller bundles, which are then loaded on demand when visiting new pages. This can significantly reduce the size of the initial JavaScript bundle loaded by the browser, improving the time to first render. + +While code splitting is helpful for very large projects, it does require extra requests when visiting new pages. Generally speaking, if you're able to use a single bundle, your app is going to feel snappier. + +To enable code splitting, you will need to tweak the `resolve` callback in your `createInertiaApp()` configuration, and how you do this is different depending on which bundler you're using. + +## Using Vite + +Vite enables code splitting (or lazy-loading as they call it) by default when using their `import.meta.glob()` function, so simply omit the `{ eager: true }` option, or set it to `false`, to disable eager loading. + + + +```js Vue icon="vuejs" +resolve: name => { + const pages = import.meta.glob('./Pages/**/*.vue', { eager: true }) // [!code --:2] + return pages[`./Pages/${name}.vue`] + const pages = import.meta.glob('./Pages/**/*.vue') // [!code ++:2] + return pages[`./Pages/${name}.vue`]() +}, +``` + +```js React icon="react" +resolve: name => { + const pages = import.meta.glob('./Pages/**/*.jsx', { eager: true }) // [!code --:2] + return pages[`./Pages/${name}.jsx`] + const pages = import.meta.glob('./Pages/**/*.jsx') // [!code ++:2] + return pages[`./Pages/${name}.jsx`]() +}, +``` + +```js Svelte icon="s" +resolve: name => { + const pages = import.meta.glob('./Pages/**/*.svelte', { eager: true }) // [!code --:2] + return pages[`./Pages/${name}.svelte`] + const pages = import.meta.glob('./Pages/**/*.svelte') // [!code ++:2] + return pages[`./Pages/${name}.svelte`]() +}, +``` + + + +## Using Webpack + +To use code splitting with Webpack, you will first need to enable [dynamic imports](https://github.com/tc39/proposal-dynamic-import) via a Babel plugin. Let's install it now. + +```bash +npm install @babel/plugin-syntax-dynamic-import +``` + +Next, create a `.babelrc` file in your project with the following configuration: + +```json +{ + "plugins": ["@babel/plugin-syntax-dynamic-import"] +} +``` + +If you're using Laravel Mix, the dynamic imports Babel plugin is already installed and configured, and you can skip these steps. We recommend using Laravel Mix 6 or above, as there are known issues with older versions. + +Finally, update the `resolve` callback in your app's initialization code to use `import`instead of `require`. + + + +```js Vue icon="vuejs" +resolve: name => require(`./Pages/${name}`), // [!code --] +resolve: name => import(`./Pages/${name}`), // [!code ++] +``` + +```js React icon="react" +resolve: name => require(`./Pages/${name}`), // [!code --] +resolve: name => import(`./Pages/${name}`), // [!code ++] +``` + +```js Svelte icon="s" +resolve: name => require(`./Pages/${name}.svelte`), // [!code --] +resolve: name => import(`./Pages/${name}.svelte`), // [!code ++] +``` + + + +You should also consider using cache busting to force browsers to load the latest version of your assets. To accomplish this, add the following configuration to your webpack configuration file. + +```js +output: { + chunkFilename: 'js/[name].js?id=[chunkhash]', +} +``` diff --git a/v3/advanced/error-handling.mdx b/v3/advanced/error-handling.mdx new file mode 100644 index 0000000..421fb77 --- /dev/null +++ b/v3/advanced/error-handling.mdx @@ -0,0 +1,172 @@ +--- +title: Error Handling +--- + +## Development + +One of the advantages to working with a robust server-side framework is the built-in exception handling you get for free. For example, Laravel ships with a beautiful error reporting tool which displays a nicely formatted stack trace in local development. + +The challenge is, if you're making an XHR request (which Inertia does) and you hit a server-side error, you're typically left digging through the network tab in your browser's devtools to diagnose the problem. + +Inertia solves this issue by showing all non-Inertia responses in a modal. This means you get the same beautiful error-reporting you're accustomed to, even though you've made that request over XHR. + +## Dialog Element + +By default, Inertia displays error modals using a custom `
` overlay. However, you can opt-in to using the native HTML `` element instead, which provides built-in modal functionality including backdrop handling. + +To enable this, configure the `future.useDialogForErrorModal` option in your [application defaults](/v3/installation/client-side-setup#configuring-defaults). + +```js +createInertiaApp({ + // resolve, setup, etc. + defaults: { + future: { + useDialogForErrorModal: true, + }, + }, +}); +``` + +## Production + +In production you will want to return a proper Inertia error response instead of relying on the modal-driven error reporting that is present during development. To accomplish this, you'll need to update your framework's default exception handler to return a custom error page. + +When building Laravel applications, you can accomplish this by using the `respond` exception method in your application's `bootstrap/app.php` file. + +```php +use Illuminate\Http\Request; +use Symfony\Component\HttpFoundation\Response; +use Inertia\Inertia; + +->withExceptions(function (Exceptions $exceptions) { + $exceptions->respond(function (Response $response, Throwable $exception, Request $request) { + if (! app()->environment(['local', 'testing']) && in_array($response->getStatusCode(), [500, 503, 404, 403])) { + return Inertia::render('ErrorPage', ['status' => $response->getStatusCode()]) + ->toResponse($request) + ->setStatusCode($response->getStatusCode()); + } + + if ($response->getStatusCode() === 419) { + return back()->with([ + 'message' => 'The page expired, please try again.', + ]); + } + + return $response; + }); +}) +``` + +You may have noticed we're returning an `ErrorPage` page component in the example above. You'll need to actually create this component, which will serve as the generic error page for your application. Here's an example error component you can use as a starting point. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +export default function ErrorPage({ status }) { + const title = { + 503: "503: Service Unavailable", + 500: "500: Server Error", + 404: "404: Page Not Found", + 403: "403: Forbidden", + }[status]; + + const description = { + 503: "Sorry, we are doing some maintenance. Please check back soon.", + 500: "Whoops, something went wrong on our servers.", + 404: "Sorry, the page you are looking for could not be found.", + 403: "Sorry, you are forbidden from accessing this page.", + }[status]; + + return ( +
+

{title}

+
{description}
+
+ ); +} +``` + +```svelte Svelte 4 icon="s" + + +
+

{title}

+
{description}
+
+``` + +```svelte Svelte 5 icon="s" + + +
+

{title[status]}

+
{description[status]}
+
+``` + +
diff --git a/v3/advanced/events.mdx b/v3/advanced/events.mdx new file mode 100644 index 0000000..cf93353 --- /dev/null +++ b/v3/advanced/events.mdx @@ -0,0 +1,790 @@ +--- +title: Events +--- + +Inertia provides an event system that allows you to "hook into" the various lifecycle events of the library. + +## Registering Listeners + +To register an event listener, use the `router.on()` method. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.on("start", (event) => { + console.log(`Starting a visit to ${event.detail.visit.url}`); +}); +``` + +```jsx React icon="react" +import { router } from "@inertiajs/react"; + +router.on("start", (event) => { + console.log(`Starting a visit to ${event.detail.visit.url}`); +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.on("start", (event) => { + console.log(`Starting a visit to ${event.detail.visit.url}`); +}); +``` + + + +Under the hood, Inertia uses native browser events, so you can also interact with Inertia events using the typical event methods you may already be familiar with - just be sure to prepend `inertia:` to the event name. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +document.addEventListener("inertia:start", (event) => { + console.log(`Starting a visit to ${event.detail.visit.url}`); +}); +``` + +```jsx React icon="react" +import { router } from "@inertiajs/react"; + +document.addEventListener("inertia:start", (event) => { + console.log(`Starting a visit to ${event.detail.visit.url}`); +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +document.addEventListener("inertia:start", (event) => { + console.log(`Starting a visit to ${event.detail.visit.url}`); +}); +``` + + + +## Removing Listeners + +When you register an event listener, Inertia automatically returns a callback that can be invoked to remove the event listener. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +let removeStartEventListener = router.on("start", (event) => { + console.log(`Starting a visit to ${event.detail.visit.url}`); +}); + +// Remove the listener... +removeStartEventListener(); +``` + +```jsx React icon="react" +import { router } from "@inertiajs/react"; + +let removeStartEventListener = router.on("start", (event) => { + console.log(`Starting a visit to ${event.detail.visit.url}`); +}); + +// Remove the listener... +removeStartEventListener(); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +let removeStartEventListener = router.on("start", (event) => { + console.log(`Starting a visit to ${event.detail.visit.url}`); +}); + +// Remove the listener... +removeStartEventListener(); +``` + + + +Combined with hooks, you can automatically remove the event listener when components unmount. + + + +```vue Vue icon="vuejs" +import { router } from '@inertiajs/vue3' import { onUnmounted } from 'vue' +onUnmounted( router.on('start', (event) => { console.log(`Starting a visit to +${event.detail.visit.url}`) }) ) +``` + +```jsx React icon="react" +import { useEffect } from "react"; +import { router } from "@inertiajs/react"; + +useEffect(() => { + return router.on("start", (event) => { + console.log(`Starting a visit to ${event.detail.visit.url}`); + }); +}, []); +``` + +```js Svelte 4 icon="s" +import { router } from "@inertiajs/svelte"; +import { onMount } from "svelte"; + +onMount(() => { + return router.on("start", (event) => { + console.log(`Starting a visit to ${event.detail.visit.url}`); + }); +}); +``` + +```js Svelte 5 icon="s" +import { router } from "@inertiajs/svelte"; + +$effect(() => { + return router.on("start", (event) => { + console.log(`Starting a visit to ${event.detail.visit.url}`); + }); +}); +``` + + + +Alternatively, if you're using native browser events, you can remove the event listener using `removeEventListener()`. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +let startEventListener = (event) => { + console.log(`Starting a visit to ${event.detail.visit.url}`); +}; + +document.addEventListener("inertia:start", startEventListener); + +// Remove the listener... +document.removeEventListener("inertia:start", startEventListener); +``` + +```jsx React icon="react" +import { router } from "@inertiajs/react"; + +let startEventListener = (event) => { + console.log(`Starting a visit to ${event.detail.visit.url}`); +}; + +document.addEventListener("inertia:start", startEventListener); + +// Remove the listener... +document.removeEventListener("inertia:start", startEventListener); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +let startEventListener = (event) => { + console.log(`Starting a visit to ${event.detail.visit.url}`); +}; + +document.addEventListener("inertia:start", startEventListener); + +// Remove the listener... +document.removeEventListener("inertia:start", startEventListener); +``` + + + +## Cancelling Events + +Some events, such as `before`, `exception`, and `invalid`, support cancellation, allowing you to prevent Inertia's default behavior. Just like native events, the event will be cancelled if only one event listener calls `event.preventDefault()`. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.on("before", (event) => { + if (!confirm("Are you sure you want to navigate away?")) { + event.preventDefault(); + } +}); +``` + +```jsx React icon="react" +import { router } from "@inertiajs/react"; + +router.on("before", (event) => { + if (!confirm("Are you sure you want to navigate away?")) { + event.preventDefault(); + } +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.on("before", (event) => { + if (!confirm("Are you sure you want to navigate away?")) { + event.preventDefault(); + } +}); +``` + + + +For convenience, if you register your event listener using `router.on()`, you can cancel the event by returning `false` from the listener. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.on("before", (event) => { + return confirm("Are you sure you want to navigate away?"); +}); +``` + +```jsx React icon="react" +import { router } from "@inertiajs/react"; + +router.on("before", (event) => { + return confirm("Are you sure you want to navigate away?"); +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.on("before", (event) => { + return confirm("Are you sure you want to navigate away?"); +}); +``` + + + +Note, browsers do not allow cancelling the native `popstate` event, so preventing forward and back history visits while using Inertia.js is not possible. + +## Before + +The `before` event fires when a request is about to be made to the server. This is useful for intercepting visits. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.on("before", (event) => { + console.log(`About to make a visit to ${event.detail.visit.url}`); +}); +``` + +```jsx React icon="react" +import { router } from "@inertiajs/react"; + +router.on("before", (event) => { + console.log(`About to make a visit to ${event.detail.visit.url}`); +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.on("before", (event) => { + console.log(`About to make a visit to ${event.detail.visit.url}`); +}); +``` + + + +The primary purpose of this event is to allow you to prevent a visit from happening. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.on("before", (event) => { + return confirm("Are you sure you want to navigate away?"); +}); +``` + +```jsx React icon="react" +import { router } from "@inertiajs/react"; + +router.on("before", (event) => { + return confirm("Are you sure you want to navigate away?"); +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.on("before", (event) => { + return confirm("Are you sure you want to navigate away?"); +}); +``` + + + +## Start + +The `start` event fires when a request to the server has started. This is useful for displaying loading indicators. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.on("start", (event) => { + console.log(`Starting a visit to ${event.detail.visit.url}`); +}); +``` + +```jsx React icon="react" +import { router } from "@inertiajs/react"; + +router.on("start", (event) => { + console.log(`Starting a visit to ${event.detail.visit.url}`); +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.on("start", (event) => { + console.log(`Starting a visit to ${event.detail.visit.url}`); +}); +``` + + + +The `start` event is not cancelable. + +## Progress + +The `progress` event fires as progress increments during file uploads. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.on("progress", (event) => { + this.form.progress = event.detail.progress.percentage; +}); +``` + +```jsx React icon="react" +import { router } from "@inertiajs/react"; + +router.on("progress", (event) => { + this.form.progress = event.detail.progress.percentage; +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.on("progress", (event) => { + this.form.progress = event.detail.progress.percentage; +}); +``` + + + +The `progress` event is not cancelable. + +## Success + +The `success` event fires on successful page visits, unless validation errors are present. However, this does _not_ include history visits. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.on("success", (event) => { + console.log(`Successfully made a visit to ${event.detail.page.url}`); +}); +``` + +```jsx React icon="react" +import { router } from "@inertiajs/react"; + +router.on("success", (event) => { + console.log(`Successfully made a visit to ${event.detail.page.url}`); +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.on("success", (event) => { + console.log(`Successfully made a visit to ${event.detail.page.url}`); +}); +``` + + + +The `success` event is not cancelable. + +## Flash + +v2.3.3+ + +The `flash` event fires when [flash data](/v3/data-props/flash-data) is received from the server. This is useful for displaying toast notifications or handling temporary data in a central location. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.on("flash", (event) => { + if (event.detail.flash.toast) { + showToast(event.detail.flash.toast); + } +}); +``` + +```jsx React icon="react" +import { router } from "@inertiajs/react"; + +router.on("flash", (event) => { + if (event.detail.flash.toast) { + showToast(event.detail.flash.toast); + } +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.on("flash", (event) => { + if (event.detail.flash.toast) { + showToast(event.detail.flash.toast); + } +}); +``` + + + +The `flash` event is not cancelable. [Partial reloads](/v3/data-props/partial-reloads) will only trigger the event if the flash data has changed. + +## Error + +The `error` event fires when validation errors are present on "successful" page visits. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.on("error", (errors) => { + console.log(errors); +}); +``` + +```jsx React icon="react" +import { router } from "@inertiajs/react"; + +router.on("error", (errors) => { + console.log(errors); +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.on("error", (errors) => { + console.log(errors); +}); +``` + + + +The `error` event is not cancelable. + +## Invalid + +The `invalid` event fires when a non-Inertia response is received from the server, such as an HTML or vanilla JSON response. A valid Inertia response is a response that has the `X-Inertia` header set to `true` with a `json` payload containing [the page object](/v3/core-concepts/the-protocol#the-page-object). + +This event is fired for all response types, including `200`, `400`, and `500`response codes. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.on("invalid", (event) => { + console.log(`An invalid Inertia response was received.`); + console.log(event.detail.response); +}); +``` + +```jsx React icon="react" +import { router } from "@inertiajs/react"; + +router.on("invalid", (event) => { + console.log(`An invalid Inertia response was received.`); + console.log(event.detail.response); +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.on("invalid", (event) => { + console.log(`An invalid Inertia response was received.`); + console.log(event.detail.response); +}); +``` + + + +You may cancel the `invalid` event to prevent Inertia from showing the non-Inertia response modal. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.on("invalid", (event) => { + event.preventDefault(); + + // Handle the invalid response yourself... +}); +``` + +```jsx React icon="react" +import { router } from "@inertiajs/react"; + +router.on("invalid", (event) => { + event.preventDefault(); + + // Handle the invalid response yourself... +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.on("invalid", (event) => { + event.preventDefault(); + + // Handle the invalid response yourself... +}); +``` + + + +## Exception + +The `exception` event fires on unexpected XHR errors such as network interruptions. In addition, this event fires for errors generated when resolving page components. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.on("exception", (event) => { + console.log(`An unexpected error occurred during an Inertia visit.`); + console.log(event.detail.error); +}); +``` + +```jsx React icon="react" +import { router } from "@inertiajs/react"; + +router.on("exception", (event) => { + console.log(`An unexpected error occurred during an Inertia visit.`); + console.log(event.detail.error); +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.on("exception", (event) => { + console.log(`An unexpected error occurred during an Inertia visit.`); + console.log(event.detail.error); +}); +``` + + + +You may cancel the `exception` event to prevent the error from being thrown. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.on("exception", (event) => { + event.preventDefault(); + // Handle the error yourself +}); +``` + +```jsx React icon="react" +import { router } from "@inertiajs/react"; + +router.on("exception", (event) => { + event.preventDefault(); + // Handle the error yourself +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.on("exception", (event) => { + event.preventDefault(); + // Handle the error yourself +}); +``` + + + +This event will _not_ fire for XHR requests that receive `400` and `500` level responses or for non-Inertia responses, as these situations are handled in other ways by Inertia. Please consult the [error handling documentation](/v3/advanced/error-handling) for more information. + +## Finish + +The `finish` event fires after an XHR request has completed for both "successful" and "unsuccessful" responses. This event is useful for hiding loading indicators. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.on("finish", (event) => { + NProgress.done(); +}); +``` + +```jsx React icon="react" +import { router } from "@inertiajs/react"; + +router.on("finish", (event) => { + NProgress.done(); +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.on("finish", (event) => { + NProgress.done(); +}); +``` + + + +The `finish` event is not cancelable. + +## Navigate + +The `navigate` event fires on successful page visits, as well as when navigating through history. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.on("navigate", (event) => { + console.log(`Navigated to ${event.detail.page.url}`); +}); +``` + +```jsx React icon="react" +import { router } from "@inertiajs/react"; + +router.on("navigate", (event) => { + console.log(`Navigated to ${event.detail.page.url}`); +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.on("navigate", (event) => { + console.log(`Navigated to ${event.detail.page.url}`); +}); +``` + + + +The `navigate` event is not cancelable. + +## Prefetching + +The `prefetching` event fires when the router starts prefetching a page. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.on("prefetching", (event) => { + console.log(`Prefetching ${event.detail.page.url}`); +}); +``` + +```jsx React icon="react" +import { router } from "@inertiajs/react"; + +router.on("prefetching", (event) => { + console.log(`Prefetching ${event.detail.page.url}`); +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.on("prefetching", (event) => { + console.log(`Prefetching ${event.detail.page.url}`); +}); +``` + + + +The `prefetching` event is not cancelable. + +## Prefetched + +The `prefetched` event fires when the router has successfully prefetched a page. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.on("prefetched", (event) => { + console.log(`Prefetched ${event.detail.page.url}`); +}); +``` + +```jsx React icon="react" +import { router } from "@inertiajs/react"; + +router.on("prefetched", (event) => { + console.log(`Prefetched ${event.detail.page.url}`); +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.on("prefetched", (event) => { + console.log(`Prefetched ${event.detail.page.url}`); +}); +``` + + + +The `prefetched` event is not cancelable. + +## Event Callbacks + +In addition to the global events described throughout this page, Inertia also provides a number of [event callbacks](/v3/the-basics/manual-visits#event-callbacks) that fire when manually making Inertia visits. diff --git a/v3/advanced/progress-indicators.mdx b/v3/advanced/progress-indicators.mdx new file mode 100644 index 0000000..6827af9 --- /dev/null +++ b/v3/advanced/progress-indicators.mdx @@ -0,0 +1,387 @@ +--- +title: Progress Indicators +--- + +Since Inertia requests are made via XHR, there would typically not be a browser loading indicator when navigating from one page to another. To solve this, Inertia displays a progress indicator at the top of the page whenever you make an Inertia visit. However, [asynchronous requests](#visit-options) do not show the progress indicator unless explicitly configured. + +Of course, if you prefer, you can disable Inertia's default loading indicator and provide your own custom implementation. We'll discuss both approaches below. + +## Default + +Inertia's default progress indicator is a light-weight wrapper around the [NProgress](https://ricostacruz.com/nprogress/) library. You can customize it via the `progress` property of the `createInertiaApp()` function. + +```js +createInertiaApp({ + progress: { + // The delay after which the progress bar will appear, in milliseconds... + delay: 250, + // The color of the progress bar... + color: "#29d", + // Whether to include the default NProgress styles... + includeCSS: true, + // Whether the NProgress spinner will be shown... + showSpinner: false, + }, + // ... +}); +``` + +You can disable Inertia's default loading indicator by setting the `progress` property to `false`. + +```js +createInertiaApp({ + progress: false, + // ... +}); +``` + +## Programmatic Access + +When you need to control the progress indicator outside of Inertia requests, for example, when making requests with Axios or other libraries, you can use Inertia's progress methods directly. + + + +```js Vue icon="vuejs" +import { progress } from "@inertiajs/vue3"; + +progress.start(); // Begin progress animation +progress.set(0.25); // Set to 25% complete +progress.finish(); // Complete and fade out +progress.reset(); // Reset to start +progress.remove(); // Complete and remove from DOM +progress.hide(); // Hide progress bar +progress.reveal(); // Show progress bar + +progress.isStarted(); // Returns boolean +progress.getStatus(); // Returns current percentage or null +``` + +```js React icon="react" +import { progress } from "@inertiajs/react"; + +progress.start(); // Begin progress animation +progress.set(0.25); // Set to 25% complete +progress.finish(); // Complete and fade out +progress.reset(); // Reset to start +progress.remove(); // Complete and remove from DOM +progress.hide(); // Hide progress bar +progress.reveal(); // Show progress bar + +progress.isStarted(); // Returns boolean +progress.getStatus(); // Returns current percentage or null +``` + +```js Svelte icon="s" +import { progress } from "@inertiajs/svelte"; + +progress.start(); // Begin progress animation +progress.set(0.25); // Set to 25% complete +progress.finish(); // Complete and fade out +progress.reset(); // Reset to start +progress.remove(); // Complete and remove from DOM +progress.hide(); // Hide progress bar +progress.reveal(); // Show progress bar + +progress.isStarted(); // Returns boolean +progress.getStatus(); // Returns current percentage or null +``` + + + +The `hide()` and `reveal()` methods work together to prevent conflicts when separate parts of your code need to control progress visibility. Each `hide()` call increments an internal counter, while `reveal()` decrements it. The progress bar only appears when this counter reaches zero. + +However, `reveal()` accepts an optional `force` parameter that bypasses this counter. Inertia uses this pattern internally to hide progress during prefetching while ensuring it appears for actual navigation. + +```js +progress.hide(); // Counter = 1, bar hidden +progress.hide(); // Counter = 2, bar still hidden +progress.reveal(); // Counter = 1, bar still hidden +progress.reveal(); // Counter = 0, bar now visible + +// Force reveal bypasses the counter +progress.reveal(true); +``` + +If you've disabled the progress indicator with `progress: false` in `createInertiaApp()`, these programmatic methods will not work. + +## Custom + +It's also possible to setup your own custom page loading indicators using Inertia [events](/v3/advanced/events). Let's explore how to do this using the [NProgress](https://ricostacruz.com/nprogress/) library as an example. + +First, disable Inertia's default loading indicator. + +```js +createInertiaApp({ + progress: false, + // ... +}); +``` + +Next, install the NProgress library. + +```bash +npm install nprogress +``` + +After installation, you'll need to add the NProgress [styles](https://github.com/rstacruz/nprogress/blob/master/nprogress.css) to your project. You can do this using a CDN hosted copy of the styles. + +```html + +``` + +Then, import both `NProgress` and the Inertia `router` into your application. + + + +```js Vue icon="vuejs" +import NProgress from "nprogress"; +import { router } from "@inertiajs/vue3"; +``` + +```js React icon="react" +import NProgress from "nprogress"; +import { router } from "@inertiajs/react"; +``` + +```js Svelte icon="s" +import NProgress from "nprogress"; +import { router } from "@inertiajs/svelte"; +``` + + + +Next, add a `start` event listener. We'll use this listener to show the progress bar when a new Inertia visit begins. + +```js +router.on("start", () => NProgress.start()); +``` + +Finally, add a `finish` event listener to hide the progress bar when the page visit finishes. + +```js +router.on("finish", () => NProgress.done()); +``` + +That's it! Now, as you navigate from one page to another, the progress bar will be added and removed from the page. + +### Handling Cancelled Visits + +While this custom progress implementation works great for page visits that finish properly, it would be nice to handle cancelled visits as well. First, for interrupted visits (those that get cancelled as a result of a new visit), the progress bar should simply be reset back to the start position. Second, for manually cancelled visits, the progress bar should be immediately removed from the page. + +We can accomplish this by inspecting the `event.detail.visit` object that's provided to the finish event. + +```js +router.on("finish", (event) => { + if (event.detail.visit.completed) { + NProgress.done(); + } else if (event.detail.visit.interrupted) { + NProgress.set(0); + } else if (event.detail.visit.cancelled) { + NProgress.done(); + NProgress.remove(); + } +}); +``` + +### File Upload Progress + +Let's take this a step further. When files are being uploaded, it would be great to update the loading indicator to reflect the upload progress. This can be done using the `progress` event. + +```js +router.on("progress", (event) => { + if (event.detail.progress.percentage) { + NProgress.set((event.detail.progress.percentage / 100) * 0.9); + } +}); +``` + +Now, instead of the progress bar "trickling" while the files are being uploaded, it will actually update it's position based on the progress of the request. We limit the progress here to 90%, since we still need to wait for a response from the server. + +### Loading Indicator Delay + +The last thing we're going to implement is a loading indicator delay. It's often preferable to delay showing the loading indicator until a request has taken longer than 250-500 milliseconds. This prevents the loading indicator from appearing constantly on quick page visits, which can be visually distracting. + +To implement the delay behavior, we'll use the `setTimeout` and `clearTimeout` functions. Let's start by defining a variable to keep track of the timeout. + +```js +let timeout = null; +``` + +Next, let's update the `start` event listener to start a new timeout that will show the progress bar after 250 milliseconds. + +```js +router.on("start", () => { + timeout = setTimeout(() => NProgress.start(), 250); +}); +``` + +Next, we'll update the `finish` event listener to clear any existing timeouts in the event that the page visit finishes before the timeout does. + +```js +router.on("finish", (event) => { + clearTimeout(timeout); + // ... +}); +``` + +In the `finish` event listener, we need to determine if the progress bar has actually started displaying progress, otherwise we'll inadvertently cause it to show before the timeout has finished. + +```js +router.on("finish", (event) => { + clearTimeout(timeout); + + if (!NProgress.isStarted()) { + return; + } + // ... +}); +``` + +And, finally, we need to do the same check in the `progress` event listener. + +```js +router.on("progress", (event) => { + if (!NProgress.isStarted()) { + return; + } + // ... +}); +``` + +That's it, you now have a beautiful custom page loading indicator! + +### Complete Example + +For convenience, here is the full source code of the final version of our custom loading indicator. + + + +```js Vue icon="vuejs" +import NProgress from "nprogress"; +import { router } from "@inertiajs/vue3"; + +let timeout = null; + +router.on("start", () => { + timeout = setTimeout(() => NProgress.start(), 250); +}); + +router.on("progress", (event) => { + if (NProgress.isStarted() && event.detail.progress.percentage) { + NProgress.set((event.detail.progress.percentage / 100) * 0.9); + } +}); + +router.on("finish", (event) => { + clearTimeout(timeout); + + if (!NProgress.isStarted()) { + return; + } + + if (event.detail.visit.completed) { + NProgress.done(); + } else if (event.detail.visit.interrupted) { + NProgress.set(0); + } else if (event.detail.visit.cancelled) { + NProgress.done(); + NProgress.remove(); + } +}); +``` + +```js React icon="react" +import NProgress from "nprogress"; +import { router } from "@inertiajs/react"; + +let timeout = null; + +router.on("start", () => { + timeout = setTimeout(() => NProgress.start(), 250); +}); + +router.on("progress", (event) => { + if (NProgress.isStarted() && event.detail.progress.percentage) { + NProgress.set((event.detail.progress.percentage / 100) * 0.9); + } +}); + +router.on("finish", (event) => { + clearTimeout(timeout); + + if (!NProgress.isStarted()) { + return; + } + + if (event.detail.visit.completed) { + NProgress.done(); + } else if (event.detail.visit.interrupted) { + NProgress.set(0); + } else if (event.detail.visit.cancelled) { + NProgress.done(); + NProgress.remove(); + } +}); +``` + +```js Svelte icon="s" +import NProgress from "nprogress"; +import { router } from "@inertiajs/svelte"; + +let timeout = null; + +router.on("start", () => { + timeout = setTimeout(() => NProgress.start(), 250); +}); + +router.on("progress", (event) => { + if (NProgress.isStarted() && event.detail.progress.percentage) { + NProgress.set((event.detail.progress.percentage / 100) * 0.9); + } +}); + +router.on("finish", (event) => { + clearTimeout(timeout); + + if (!NProgress.isStarted()) { + return; + } + + if (event.detail.visit.completed) { + NProgress.done(); + } else if (event.detail.visit.interrupted) { + NProgress.set(0); + } else if (event.detail.visit.cancelled) { + NProgress.done(); + NProgress.remove(); + } +}); +``` + + + +## Visit Options + +In addition to these configurations, Inertia.js provides two visit options to control the loading indicator on a per-request basis: `showProgress` and `async`. These options offer greater control over how Inertia.js handles asynchronous requests and manages progress indicators. + +### Showprogress + +The `showProgress` option provides fine-grained control over the visibility of the loading indicator during requests. + +```js +router.get("/settings", {}, { showProgress: false }); +``` + +### Async + +The `async` option allows you to perform asynchronous requests without displaying the default progress indicator. It can be used in combination with the `showProgress` option. + +```js +// Disable the progress indicator +router.get("/settings", {}, { async: true }); +// Enable the progress indicator with async requests +router.get("/settings", {}, { async: true, showProgress: true }); +``` diff --git a/v3/advanced/scroll-management.mdx b/v3/advanced/scroll-management.mdx new file mode 100644 index 0000000..56bc844 --- /dev/null +++ b/v3/advanced/scroll-management.mdx @@ -0,0 +1,135 @@ +--- +title: Scroll Management +--- + +## Scroll Resetting + +When navigating between pages, Inertia mimics default browser behavior by automatically resetting the scroll position of the document body (as well as any [scroll regions](#scroll-regions) you've defined) back to the top. + +In addition, Inertia keeps track of the scroll position of each page and automatically restores that scroll position as you navigate forward and back in history. + +## Scroll Preservation + +Sometimes it's desirable to prevent the default scroll resetting when making visits. You can disable this behavior by setting the `preserveScroll` option to `true`. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.visit(url, { preserveScroll: true }); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.visit(url, { preserveScroll: true }); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.visit(url, { preserveScroll: true }); +``` + + + +If you'd like to only preserve the scroll position if the response includes validation errors, set the `preserveScroll` option to "errors". + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.visit(url, { preserveScroll: "errors" }); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.visit(url, { preserveScroll: "errors" }); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.visit(url, { preserveScroll: "errors" }); +``` + + + +You can also lazily evaluate the `preserveScroll` option based on the response by providing a callback. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.post("/users", data, { + preserveScroll: (page) => page.props.someProp === "value", +}); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.post("/users", data, { + preserveScroll: (page) => page.props.someProp === "value", +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.post("/users", data, { + preserveScroll: (page) => page.props.someProp === "value", +}); +``` + + + +When using an [Inertia link](/v3/the-basics/links), you can preserve the scroll position using the `preserveScroll` prop. + + + +```vue Vue icon="vuejs" +import { Link } from '@inertiajs/vue3' + +Home +``` + +```jsx React icon="react" +import { Link } from "@inertiajs/react"; + + + Home +; +``` + +```svelte Svelte icon="s" +import { inertia, Link } from '@inertiajs/svelte' + +Home + +Home +``` + + + +## Scroll Regions + +If your app doesn't use document body scrolling, but instead has scrollable elements (using the `overflow` CSS property), scroll resetting will not work. + +In these situations, you must tell Inertia which scrollable elements to manage by adding the `scroll-region` attribute to the element. + +```html +
+ +
+``` + +## Text Fragments + +[Text fragments](https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Fragment/Text_fragments) allow you to link directly to specific text on a page using a special URL syntax like `#:~:text=term`. However, the browser removes the fragment directive before any JavaScript runs, so text fragments only work if the targeted text is present in the initial HTML response. + +To use text fragments with your Inertia pages, enable [server-side rendering](/v3/advanced/server-side-rendering). diff --git a/v3/advanced/server-side-rendering.mdx b/v3/advanced/server-side-rendering.mdx new file mode 100644 index 0000000..ac1ebda --- /dev/null +++ b/v3/advanced/server-side-rendering.mdx @@ -0,0 +1,418 @@ +--- +title: Server-Side Rendering (SSR) +--- + +import { ClientSpecific } from "/snippets/client-specific.jsx" +import { ReactSpecific } from "/snippets/react-specific.jsx" +import { SvelteSpecific } from "/snippets/svelte-specific.jsx" +import { Svelte4Specific } from "/snippets/svelte4-specific.jsx" +import { Svelte5Specific } from "/snippets/svelte5-specific.jsx" +import { VueSpecific } from "/snippets/vue-specific.jsx" + +Server-side rendering pre-renders your JavaScript pages on the server, allowing your visitors to receive fully rendered HTML when they visit your application. Since fully rendered HTML is served by your application, it's also easier for search engines to index your site. + +Server-side rendering uses Node.js to render your pages in a background process; therefore, Node must be available on your server for server-side rendering to function properly. + +## Laravel Starter Kits + +If you are using [Laravel Starter Kits](https://laravel.com/docs/starter-kits), Inertia SSR is [supported](https://laravel.com/docs/starter-kits#inertia-ssr) through a build command: + +```bash +npm run build:ssr +``` + +## Install Dependencies + +If you are not using a Laravel starter kit and would like to manually configure SSR, we'll first install the additional dependencies required for server-side rendering. This is only necessary for the Vue adapters, so you can skip this step if you're using React or Svelte. + + + +```bash Vue icon="vuejs" +npm install @vue/server-renderer +``` + +```js React icon="react" +// No additional dependencies required +``` + +```js Svelte icon="s" +// No additional dependencies required +``` + + + +## Add Server Entry-Point + +Next, we'll create a `resources/js/ssr.js` file within our Laravel project that will serve as our SSR entry point. + +```bash +touch resources/js/ssr.js +``` + +This file is going to look very similar to your `resources/js/app.js` file, except it's not going to run in the browser, but rather in Node.js. Here's a complete example. + + + +```js Vue icon="vuejs" +import { createInertiaApp } from '@inertiajs/vue3' +import createServer from '@inertiajs/vue3/server' +import { renderToString } from '@vue/server-renderer' +import { createSSRApp, h } from 'vue' + +createServer(page => + createInertiaApp({ + page, + render: renderToString, + resolve: name => { + const pages = import.meta.glob('./Pages/**/*.vue', { eager: true }) + return pages[`./Pages/${name}.vue`] + }, + setup({ App, props, plugin }) { + return createSSRApp({ + render: () => h(App, props), + }).use(plugin) + }, + }), +) +``` + +```jsx React icon="react" +import { createInertiaApp } from '@inertiajs/react' +import createServer from '@inertiajs/react/server' +import ReactDOMServer from 'react-dom/server' + +createServer(page => + createInertiaApp({ + page, + render: ReactDOMServer.renderToString, + resolve: name => { + const pages = import.meta.glob('./Pages/**/*.jsx', { eager: true }) + return pages[`./Pages/${name}.jsx`] + }, + setup: ({ App, props }) => , + }), +) +``` + +```js Svelte 4 icon="s" +import { createInertiaApp } from '@inertiajs/svelte' +import createServer from '@inertiajs/svelte/server' + +createServer(page => + createInertiaApp({ + page, + resolve: name => { + const pages = import.meta.glob('./Pages/**/*.svelte', { eager: true }) + return pages[`./Pages/${name}.svelte`] + }, + setup({ App, props }) { + return App.render(props) + }, + }), +) +``` + +```js Svelte 5 icon="s" +import { createInertiaApp } from '@inertiajs/svelte' +import createServer from '@inertiajs/svelte/server' +import { render } from 'svelte/server' + +createServer(page => + createInertiaApp({ + page, + resolve: name => { + const pages = import.meta.glob('./Pages/**/*.svelte', { eager: true }) + return pages[`./Pages/${name}.svelte`] + }, + setup({ App, props }) { + return render(App, { props }) + }, + }), +) +``` + + + +When creating this file, be sure to add anything that's missing from your `app.js` file that makes sense to run in SSR mode, such as plugins or custom mixins. + +### Clustering + +By default, the SSR server will run on a single thread. Clustering starts multiple Node servers on the same port, requests are then handled by each thread in a round-robin way. + +You can enable clustering by passing a second argument of options to `createServer`. + + + +```js Vue icon="vuejs" +import { createInertiaApp } from '@inertiajs/vue3' +import createServer from '@inertiajs/vue3/server' +import { renderToString } from '@vue/server-renderer' +import { createSSRApp, h } from 'vue' + +createServer(page => + createInertiaApp({ + // ... + }), + { cluster: true }, +) +``` + +```jsx React icon="react" +import { createInertiaApp } from '@inertiajs/react' +import createServer from '@inertiajs/react/server' +import ReactDOMServer from 'react-dom/server' + +createServer(page => + createInertiaApp({ + // ... + }), + { cluster: true }, +) +``` + +```js Svelte 4 icon="s" +import { createInertiaApp } from '@inertiajs/svelte' +import createServer from '@inertiajs/svelte/server' + +createServer(page => + createInertiaApp({ + // ... + }), + { cluster: true }, +) +``` + +```js Svelte 5 icon="s" +import { createInertiaApp } from '@inertiajs/svelte' +import createServer from '@inertiajs/svelte/server' +import { render } from 'svelte/server' + +createServer(page => + createInertiaApp({ + // ... + }), + { cluster: true }, +) +``` + + + +## Setup Vite + +Next, we need to update our Vite configuration to build our new `ssr.js` file. We can do this by adding a `ssr` property to Laravel's Vite plugin configuration in our `vite.config.js`file. + +```js vite.config.js +export default defineConfig({ + plugins: [ + laravel({ + input: ['resources/css/app.css', 'resources/js/app.js'], + ssr: 'resources/js/ssr.js', // [!code ++] + refresh: true, + }), + // ... + ], +}) +``` + +## Update NPM Script + +Next, let's update the `build` script in our `package.json` file to also build our new `ssr.js` file. + +```json package.json +"scripts": { + "dev": "vite", + "build": "vite build" // [!code --] + "build": "vite build && vite build --ssr" // [!code ++] +}, +``` + +Now you can build both your client-side and server-side bundles. + +```bash +npm run build +``` + +## Running the SSR Server + +Now that you have built both your client-side and server-side bundles, you should be able run the Node-based Inertia SSR server using the following command. + +```bash +php artisan inertia:start-ssr +``` + +You may use the `--runtime` option to specify which runtime you want to use. This allows you to switch from the default Node.js runtime to Bun. + +```bash +php artisan inertia:start-ssr --runtime=bun +``` + +With the server running, you should be able to access your app within the browser with server-side rendering enabled. In fact, you should be able to disable JavaScript entirely and still navigate around your application. + +## Client-Side Hydration + + +Since your website is now being server-side rendered, you can instruct VueReactSvelte to "hydrate" the static markup and make it interactive instead of re-rendering all the HTML that we just generated. + + +To enable client-side hydration in a Vue app, update your `ssr.js` file to use `createSSRApp` instead of `createApp`. + +To enable client-side hydration in a React app, update your `ssr.js` file to use `hydrateRoot` instead of `createRoot`. + +To enable client-side hydration in a Svelte 4 app, set the `hydrate` option to `true` in your `ssr.js` file. + +To enable client-side hydration in a Svelte 5 app, update your `ssr.js` file to use `hydrate` instead of `mount` when server rendering. + + + +```js Vue icon="vuejs" +import { createApp, h } from 'vue' // [!code --] +import { createSSRApp, h } from 'vue' // [!code ++] +import { createInertiaApp } from '@inertiajs/vue3' + +createInertiaApp({ + resolve: name => { + const pages = import.meta.glob('./Pages/**/*.vue', { eager: true }) + return pages[`./Pages/${name}.vue`] + }, + setup({ el, App, props, plugin }) { + createApp({ render: () => h(App, props) }) // [!code --] + createSSRApp({ render: () => h(App, props) }) // [!code ++] + .use(plugin) + .mount(el) + }, +}) +``` + +```js React icon="react" +import { createInertiaApp } from '@inertiajs/react' +import { createRoot } from 'react-dom/client' // [!code --] +import { hydrateRoot } from 'react-dom/client' // [!code ++] + +createInertiaApp({ + resolve: name => { + const pages = import.meta.glob('./Pages/**/*.jsx', { eager: true }) + return pages[`./Pages/${name}.jsx`] + }, + setup({ el, App, props }) { + createRoot(el).render() // [!code --] + hydrateRoot(el, ) // [!code ++] + }, +}) +``` + +```js Svelte 4 icon="s" +import { createInertiaApp } from '@inertiajs/svelte' + +createInertiaApp({ + resolve: name => { + const pages = import.meta.glob('./Pages/**/*.svelte', { eager: true }) + return pages[`./Pages/${name}.svelte`] + }, + setup({ el, App, props }) { + new App({ target: el, props }) // [!code --] + new App({ target: el, props, hydrate: true }) // [!code ++] + }, +}) +``` + +```js Svelte 5 icon="s" +import { createInertiaApp } from '@inertiajs/svelte' +import { mount } from 'svelte' // [!code --] +import { hydrate, mount } from 'svelte' // [!code ++] + +createInertiaApp({ + resolve: name => { + const pages = import.meta.glob('./Pages/**/*.svelte', { eager: true }) + return pages[`./Pages/${name}.svelte`] + }, + setup({ el, App, props }) { + mount(App, { target: el, props }) // [!code --] + if (el.dataset.serverRendered === 'true') { // [!code ++:5] + hydrate(App, { target: el, props }) + } else { + mount(App, { target: el, props }) + } + }, +}) +``` + + + + + +You will also need to set the `hydratable` compiler option to `true` in your `vite.config.js` file: + +```js vite.config.js +import { svelte } from '@sveltejs/vite-plugin-svelte' +import laravel from 'laravel-vite-plugin' +import { defineConfig } from 'vite' + +export default defineConfig({ + plugins: [ + laravel.default({ + input: ['resources/css/app.css', 'resources/js/app.js'], + ssr: 'resources/js/ssr.js', + refresh: true, + }), + svelte(), // [!code --] + svelte({ // [!code ++:5] + compilerOptions: { + hydratable: true, + }, + }), + ], +}) +``` + + +## Disabling SSR + +Sometimes you may wish to disable server-side rendering for certain pages or routes in your application. You may do so by setting the `inertia.ssr.enabled` configuration value to `false` for the current request, typically in a service provider or middleware. + +```php +if (request()->is('admin/*')) { + config(['inertia.ssr.enabled' => false]); +} +``` + +## Deployment + +When deploying your SSR enabled app to production, you'll need to build both the client-side (`app.js`) and server-side bundles (`ssr.js`), and then run the SSR server as a background process, typically using a process monitoring tool such as Supervisor. + +```bash +php artisan inertia:start-ssr +``` + +To stop the SSR server, for instance when you deploy a new version of your website, you may utilize the `inertia:stop-ssr` Artisan command. Your process monitor (such as Supervisor) should be responsible for automatically restarting the SSR server after it has stopped. + +```bash +php artisan inertia:stop-ssr +``` + +You may use the `inertia:check-ssr` Artisan command to verify that the SSR server is running. This can be helpful after deployment and works well as a Docker health check to ensure the server is responding as expected. + +```bash +php artisan inertia:check-ssr +``` + +By default, a check is performed to ensure the server-side bundle exists before dispatching a request to the SSR server. In some cases, such as when your app runs on multiple servers or is containerized, the web server may not have access to the SSR bundle. To disable this check, you may set the `inertia.ssr.ensure_bundle_exists` configuration value to `false`. + +### Laravel Cloud + +To run the SSR server on Laravel Cloud, you may use Cloud's [native support for Inertia SSR](https://cloud.laravel.com/docs/compute#inertia-ssr). + +### Laravel Forge + +To run the SSR server on Forge, you should create a new daemon that runs `php artisan inertia:start-ssr` from the root of your app. Or, you may utilize the built-in Inertia integration from your Forge application's management dashboard. + +Next, whenever you deploy your application, you can automatically restart the SSR server by calling the `php artisan inertia:stop-ssr` command. This will stop the existing SSR server, forcing a new one to be started by your process monitor. + +### Heroku + +To run the SSR server on Heroku, update the `web` configuration in your `Procfile` to run the SSR server before starting your web server. + +```bash +web: php artisan inertia:start-ssr & vendor/bin/heroku-php-apache2 public/ +``` + +Note, you must have the `heroku/nodejs` buildpack installed in addition to the `heroku/php` buildback for the SSR server to run. diff --git a/v3/advanced/testing.mdx b/v3/advanced/testing.mdx new file mode 100644 index 0000000..26fcfdf --- /dev/null +++ b/v3/advanced/testing.mdx @@ -0,0 +1,246 @@ +--- +title: Testing +--- + +There are many different ways to test an Inertia application. This page provides a quick overview of the tools available. + +## End-to-end Tests + +One popular approach to testing your JavaScript page components is to use an end-to-end testing tool like [Cypress](https://www.cypress.io/) or [Pest](https://pestphp.com). These are browser automation tools that allow you to run real simulations of your app in the browser. These tests are known to be slower; however, since they test your application at the same layer as your end users, they can provide a lot of confidence that your app is working correctly. And, since these tests are run in the browser, your JavaScript code is actually executed and tested as well. + +## Client-Side Unit Tests + +Another approach to testing your page components is using a client-side unit testing framework, such as [Vitest](https://vitest.dev/), [Jest](https://jestjs.io/), or [Mocha](https://mochajs.org/). This approach allows you to test your JavaScript page components in isolation using Node.js. + +## Endpoint Tests + +In addition to testing your JavaScript page components, you will likely want to also test the Inertia responses that are returned by your server-side framework. A popular approach to doing this is using endpoint tests, where you make requests to your application and examine the responses. Laravel [provides tooling](https://laravel.com/docs/http-tests) for executing these types of tests. + +However, to make this process even easier, Inertia's Laravel adapter provides additional HTTP testing tools. Let's take a look at an example. + +```php +use Inertia\Testing\AssertableInertia as Assert; + +class PodcastsControllerTest extends TestCase +{ + public function test_can_view_podcast() + { + $this->get('/podcasts/41') + ->assertInertia(fn (Assert $page) => $page + ->component('Podcasts/Show') + ->has('podcast', fn (Assert $page) => $page + ->where('id', $podcast->id) + ->where('subject', 'The Laravel Podcast') + ->where('description', 'The Laravel Podcast brings you Laravel and PHP development news and discussion.') + ->has('seasons', 4) + ->has('seasons.4.episodes', 21) + ->has('host', fn (Assert $page) => $page + ->where('id', 1) + ->where('name', 'Matt Stauffer') + ) + ->has('subscribers', 7, fn (Assert $page) => $page + ->where('id', 2) + ->where('name', 'Claudio Dekker') + ->where('platform', 'Apple Podcasts') + ->etc() + ->missing('email') + ->missing('password') + ) + ) + ); + } +} +``` + +As you can see in the example above, you may use these assertion methods to assert against the content of the data provided to the Inertia response. In addition, you may assert that array data has a given length as well as scope your assertions. + +You may use the `inertiaProps` method to retrieve the props returned in the response. You can pass a key to retrieve a specific property, and nested properties are supported using "dot" notation. + +```php +$response = $this->get('/podcasts/41'); + +// Returns all props... +$response->inertiaProps(); + +// Returns a specific prop... +$response->inertiaProps('podcast'); + +// Returns a nested prop using "dot" notation... +$response->inertiaProps('podcast.id'); +``` + +Let's dig into the `assertInertia` method and the available assertions in detail. First, to assert that the Inertia response has a property, you may use the `has` method. You can think of this method as being similar to PHP's `isset` function. + +```php +$response->assertInertia(fn (Assert $page) => $page + // Checking if a root-level property has 7 items... + ->has('podcasts', 7) + + // Checking nested properties using "dot" notation... + ->has('podcast.subscribers', 7) +); +``` + +To assert that an Inertia property has a specified amount of items, you may provide the expected size as the second argument to the `has` method. + +```php +$response->assertInertia(fn (Assert $page) => $page + // Checking if a root-level property has 7 items... + ->has('podcasts', 7) + + // Checking nested properties using "dot" notation... + ->has('podcast.subscribers', 7) +); +``` + +The `has` method may also be used to scope properties in order to lessen repetition when asserting against nested properties. + +```php +$response->assertInertia(fn (Assert $page) => $page + // Creating a single-level property scope... + ->has('message', fn (Assert $page) => $page + // We can now continue chaining methods... + ->has('subject') + ->has('comments', 5) + + // And can even create a deeper scope using "dot" notation... + ->has('comments.0', fn (Assert $page) => $page + ->has('body') + ->has('files', 1) + ->has('files.0', fn (Assert $page) => $page + ->has('url') + ) + ) + ) +); +``` + +When scoping into Inertia properties that are arrays or collections, you may also assert that a specified number of items are present in addition to scoping into the first item. + +```php +$response->assertInertia(fn (Assert $page) => $page + // Assert that there are 5 comments and automatically scope into the first comment... + ->has('comments', 5, fn (Assert $page) => $page + ->has('body') + // ... + ) +); +``` + +To assert that an Inertia property has an expected value, you may use the `where` assertion. + +```php +$response->assertInertia(fn (Assert $page) => $page + ->has('message', fn (Assert $page) => $page + // Assert that the subject prop matches the given message... + ->where('subject', 'This is an example message') + + // Or, assert against deeply nested values... + ->where('comments.0.files.0.name', 'example-attachment.pdf') + ) +); +``` + +Inertia's testing methods will automatically fail when you haven't interacted with at least one of the props in a scope. While this is generally useful, you might run into situations where you're working with unreliable data (such as from an external feed), or with data that you really don't want interact with in order to keep your test simple. For these situations, the `etc` method exists. + +```php +$response->assertInertia(fn (Assert $page) => $page + ->has('message', fn (Assert $page) => $page + ->has('subject') + ->has('comments') + ->etc() + ) +); +``` + +The `missing` method is the exact opposite of the `has` method, ensuring that the property does not exist. This method makes a great companion to the `etc` method. + +```php +$response->assertInertia(fn (Assert $page) => $page + ->has('message', fn (Assert $page) => $page + ->has('subject') + ->missing('published_at') + ->etc() + ) +); +``` + +### Testing Partial Reloads + +You may use the `reloadOnly` and `reloadExcept` methods to test how your application responds to [partial reloads](/v3/data-props/partial-reloads). These methods perform a follow-up request and allow you to make assertions against the response. + +```php +$response->assertInertia(fn (Assert $page) => $page + ->has('orders') + ->missing('statuses') + ->reloadOnly('statuses', fn (Assert $reload) => $reload + ->missing('orders') + ->has('statuses', 5) + ) +); +``` + +Instead of passing a single prop as a string, you may also pass an array of props to `reloadOnly` or `reloadExcept`. + +### Testing Deferred Props + +You may use the `loadDeferredProps` method to test how your application responds to [deferred props](/v3/data-props/deferred-props). This method performs a follow-up request to load the deferred properties and allows you to make assertions against the response. + +```php +$response->assertInertia(fn (Assert $page) => $page + ->has('users') + ->has('roles') + ->missing('permissions') // Deferred prop not in initial response + ->loadDeferredProps(fn (Assert $reload) => $reload + ->has('permissions') + ->where('permissions.0.name', 'edit users') + ) +); +``` + +You may also load specific deferred prop groups by passing the group name as the first argument to the `loadDeferredProps` method. + +```php +$response->assertInertia(fn (Assert $page) => $page + ->has('users') + ->missing('teams') + ->missing('projects') + ->loadDeferredProps('attributes', fn (Assert $reload) => $reload + ->has('teams', 5) + ->has('projects') + ->missing('permissions') // Different group + ) +); +``` + +Instead of passing a single group as a string, you may also pass an array of groups to `loadDeferredProps`. + +```php +$response->assertInertia(fn (Assert $page) => $page + ->loadDeferredProps(['default', 'attributes'], fn (Assert $reload) => $reload + ->has('permissions') + ->has('teams') + ->has('projects') + ) +); +``` + +### Testing Flash Data + +You may use the `hasFlash` and `missingFlash` methods to test [flash data](/v3/data-props/flash-data) in your Inertia responses. + +```php +$response->assertInertia(fn (Assert $page) => $page + // Assert flash data exists... + ->hasFlash('message') + + // Assert flash data has a specific value... + ->hasFlash('message', 'Item saved!') + + // Nested values are supported using "dot" notation... + ->hasFlash('notification.type', 'success') + + // Assert flash data does not exist... + ->missingFlash('error') +); +``` diff --git a/v3/advanced/typescript.mdx b/v3/advanced/typescript.mdx new file mode 100644 index 0000000..7423c0e --- /dev/null +++ b/v3/advanced/typescript.mdx @@ -0,0 +1,380 @@ +--- +title: TypeScript +--- + +Inertia provides first-class TypeScript support. You may configure global types using declaration merging, and pass generics to hooks and router methods for type-safe props, forms, and state management. + +## Using pnpm + +Due to pnpm's strict dependency isolation, `@inertiajs/core` is not accessible at `node_modules/@inertiajs/core`. Instead, it's nested inside `.pnpm/`, which prevents TypeScript module augmentation from resolving the module. + +You may fix this by configuring pnpm to [hoist the package](https://pnpm.io/settings#publichoistpattern). Add the following to your `.npmrc` file and run `pnpm install`. + +```ini +public-hoist-pattern[]=@inertiajs/core +``` + +Alternatively, you may add `@inertiajs/core` as a direct dependency in your project. + +```bash +pnpm add @inertiajs/core +``` + +## Global Configuration + +You may configure Inertia's types globally by augmenting the `InertiaConfig` interface in the `@inertiajs/core` module. This is typically done in a `global.d.ts` file in your project's root or `types` directory. + +```ts +// global.d.ts +declare module "@inertiajs/core" { + export interface InertiaConfig { + sharedPageProps: { + auth: { user: { id: number; name: string } | null }; + appName: string; + }; + flashDataType: { + toast?: { type: "success" | "error"; message: string }; + }; + errorValueType: string[]; + } +} +``` + + + For module augmentation to work, your `tsconfig.json` needs to include `.d.ts` + files. Make sure a pattern like `"resources/js/**/*.d.ts"` is present in the + `include` array, adjusted to match your project's directory structure. + + +### Shared Page Props + +The `sharedPageProps` option defines the type of data that is [shared](/v3/data-props/shared-data) with every page in your application. With this configuration, `page.props.auth` and `page.props.appName` will be properly typed everywhere. + +```ts +sharedPageProps: { + auth: { user: { id: number; name: string } | null } + appName: string +} +``` + +### Flash Data + +The `flashDataType` option defines the type of [flash data](/v3/data-props/flash-data) in your application. + +```ts +flashDataType: { + toast?: { type: 'success' | 'error'; message: string } +} +``` + +### Error Values + +By default, validation error values are typed as `string`. You may configure TypeScript to expect arrays instead for [multiple errors per field](/v3/the-basics/validation#multiple-errors-per-field). + +```ts +errorValueType: string[] +``` + + + The next version of [Laravel + Wayfinder](https://github.com/laravel/wayfinder/tree/next) may automatically + generate these types for you by analyzing your Laravel application. It + generates TypeScript types for shared props, page props, form requests, and + Eloquent models. This version is currently in beta. + + +## Page Components + +You may type the `import.meta.glob` result for better type safety when resolving page components. + + + +```ts Vue icon="vuejs" +import { createInertiaApp } from "@inertiajs/vue3"; +import type { DefineComponent } from "vue"; + +createInertiaApp({ + resolve: (name) => { + const pages = import.meta.glob("./Pages/**/*.vue", { + eager: true, + }); + return pages[`./Pages/${name}.vue`]; + }, + // ... +}); +``` + +```tsx React icon="react" +import { createInertiaApp, type ResolvedComponent } from "@inertiajs/react"; + +createInertiaApp({ + resolve: (name) => { + const pages = import.meta.glob("./Pages/**/*.tsx", { + eager: true, + }); + return pages[`./Pages/${name}.tsx`]; + }, + // ... +}); +``` + +```ts Svelte icon="s" +import { createInertiaApp, type ResolvedComponent } from "@inertiajs/svelte"; + +createInertiaApp({ + resolve: (name) => { + const pages = import.meta.glob("./Pages/**/*.svelte", { + eager: true, + }); + return pages[`./Pages/${name}.svelte`]; + }, + // ... +}); +``` + + + +## Page Props + +You may type page-specific props by passing a generic to `usePage()`. These are merged with your global `sharedPageProps`, giving you autocomplete and type checking for both shared and page-specific data. + + + +```vue Vue icon="vuejs" + +``` + +```tsx React icon="react" +import { usePage } from "@inertiajs/react"; + +export default function Posts() { + const page = usePage<{ + posts: { id: number; title: string }[]; + }>(); + + return ( +
    + {page.props.posts.map((post) => ( +
  • {post.title}
  • + ))} +
+ ); +} +``` + +```svelte Svelte icon="s" + +``` + +
+ +## Form Helper + +The [form helper](/v3/the-basics/forms#form-helper) accepts a generic type parameter for type-safe form data and error handling. This provides autocomplete for form fields and errors, and prevents typos in field names. + + + +```vue Vue icon="vuejs" + +``` + +```tsx React icon="react" +import { useForm } from "@inertiajs/react"; + +export default function CreateUser() { + const form = useForm<{ + name: string; + email: string; + company: { name: string }; + }>({ + name: "", + email: "", + company: { name: "" }, + }); + + return null; +} +``` + +```svelte Svelte icon="s" + +``` + + + +### Nested Data and Arrays + +Form types fully support nested objects and arrays. You may access and update nested fields using dot notation, and error keys are automatically typed to match. + +```ts +import { useForm } from "@inertiajs/react"; + +const form = useForm<{ + user: { name: string; email: string }; + tags: { id: number; label: string }[]; +}>({ + user: { name: "", email: "" }, + tags: [], +}); +``` + +## Remembering State + +The `useRemember` hook accepts a generic type parameter for type-safe local state persistence, providing autocomplete and ensuring values match the expected types. + + + +```vue Vue icon="vuejs" + +``` + +```tsx React icon="react" +import { useRemember } from "@inertiajs/react"; + +export default function Users() { + const [filters, setFilters] = useRemember<{ + search: string; + status: "active" | "inactive" | "all"; + }>({ + search: "", + status: "all", + }); + + return null; +} +``` + +```svelte Svelte icon="s" + +``` + + + +## Restoring State + +The `router.restore()` method accepts a generic for typing state restored from [history](/v3/data-props/remembering-state#manually-saving-state). + +```ts +import { router } from "@inertiajs/react"; + +interface TableState { + sortBy: string; + sortDesc: boolean; + page: number; +} + +const restored = router.restore("table-state"); + +if (restored) { + console.log(restored.sortBy); +} +``` + +## Router Requests + +Router methods accept a generic for typing request data, providing type checking for the data being sent. + +```ts +import { router } from "@inertiajs/react"; + +interface CreateUserData { + name: string; + email: string; +} + +router.post("/users", { + name: "John", + email: "john@example.com", +}); +``` + +## Scoped Flash Data + +The `router.flash()` method accepts a generic for typing page or section-specific flash data, separate from the global `flashDataType` configuration. + +```ts +import { router } from "@inertiajs/react"; + +router.flash<{ paymentError: string }>({ paymentError: "Card declined" }); +``` + +## Client-Side Visits + +The `router.push()` and `router.replace()` methods accept a generic for typing [client-side visit](/v3/the-basics/manual-visits#client-side-visits) props. + +```ts +import { router } from "@inertiajs/react"; + +interface UserPageProps { + user: { id: number; name: string }; +} + +router.push({ + component: "Users/Show", + url: "/users/1", + props: { user: { id: 1, name: "John" } }, +}); + +router.replace({ + props: (current) => ({ + ...current, + user: { ...current.user, name: "Updated" }, + }), +}); +``` diff --git a/v3/core-concepts/how-it-works.mdx b/v3/core-concepts/how-it-works.mdx new file mode 100644 index 0000000..ede4765 --- /dev/null +++ b/v3/core-concepts/how-it-works.mdx @@ -0,0 +1,21 @@ +--- +title: How It Works +--- + +## Use the Tools You Love + +With Inertia you build applications just like you've always done with your server-side web framework of choice. You use your framework's existing functionality for routing, controllers, middleware, authentication, authorization, data fetching, and more. + +However, Inertia replaces your application's view layer. Instead of using server-side rendering via PHP or Ruby templates, the views returned by your application are JavaScript page components. This allows you to build your entire frontend using React, Vue, or Svelte, while still enjoying the productivity of Laravel or your preferred server-side framework. + +## Intercepting Requests + +As you might expect, simply creating your frontend in JavaScript doesn't give you a single-page application experience. If you were to click a link, your browser would make a full page visit, which would then cause your client-side framework to reboot on the subsequent page load. This is where Inertia changes everything. + +At its core, Inertia is essentially a client-side routing library. It allows you to make page visits without forcing a full page reload. This is done using the `` component, a light-weight wrapper around a normal anchor link. When you click an Inertia link, Inertia intercepts the click and makes the visit via XHR instead. You can even make these visits programmatically in JavaScript using `router.visit()`. + +When Inertia makes an XHR visit, the server detects that it's an Inertia visit and, instead of returning a full HTML response, it returns a JSON response with the JavaScript page component name and data (props). Inertia then dynamically swaps out the previous page component with the new page component and updates the browser's history state. + +**The end result is a silky smooth single-page experience.** + +To learn more about the nitty-gritty, technical details of how Inertia works under the hood, check out the [protocol page](/v3/core-concepts/the-protocol). diff --git a/v3/core-concepts/the-protocol.mdx b/v3/core-concepts/the-protocol.mdx new file mode 100644 index 0000000..e5482c7 --- /dev/null +++ b/v3/core-concepts/the-protocol.mdx @@ -0,0 +1,559 @@ +--- +title: The Protocol +--- + +This page contains a detailed specification of the Inertia protocol. Be sure to read the [how it works](/v3/core-concepts/how-it-works) page first for a high-level overview. + +## HTML Responses + +The very first request to an Inertia app is just a regular, full-page browser request, with no special Inertia headers or data. For these requests, the server returns a full HTML document. + +This HTML response includes the site assets (CSS, JavaScript) as well as a root `
` in the page's body. The root `
` serves as a mounting point for the client-side app, and includes a `data-page` attribute with a JSON encoded [page object](#the-page-object) for the initial page. Inertia uses this information to boot your client-side framework and display the initial page component. + +```http +REQUEST +GET: https://example.com/events/80 +Accept: text/html, application/xhtml+xml + +RESPONSE +HTTP/1.1 200 OK +Content-Type: text/html; charset=utf-8 + + + + My app + + + + +
+ + +``` + +While the initial response is HTML, Inertia does not server-side render the JavaScript page components. For information on server-side rendering, see the [SSR documentation](/v3/advanced/server-side-rendering). + +## Inertia Responses + +Once the Inertia app has been booted, all subsequent requests to the site are made via XHR with a `X-Inertia` header set to `true`. This header indicates that the request is being made by Inertia and isn't a standard full-page visit. + +When the server detects the `X-Inertia` header, instead of responding with a full HTML document, it returns a JSON response with an encoded [page object](#the-page-object). + +```http +REQUEST +GET: https://example.com/events/80 +Accept: text/html, application/xhtml+xml +X-Requested-With: XMLHttpRequest +X-Inertia: true +X-Inertia-Version: 6b16b94d7c51cbe5b1fa42aac98241d5 + +RESPONSE +HTTP/1.1 200 OK +Content-Type: application/json +Vary: X-Inertia +X-Inertia: true + +{ + "component": "Event", + "props": { + "errors": {}, + "event": { + "id": 80, + "title": "Birthday party", + "start_date": "2019-06-02", + "description": "Come out and celebrate Jonathan's 36th birthday party!" + } + }, + "url": "/events/80", + "version": "6b16b94d7c51cbe5b1fa42aac98241d5", + "encryptHistory": true, + "clearHistory": false +} +``` + +## Request Lifecycle Diagram + +The diagram below illustrates the request lifecycle within an Inertia application. The initial visit generates a standard request to the server, which returns an HTML application skeleton containing a root element with hydrated data. For subsequent user interactions and navigation, Inertia sends XHR requests that return JSON data. Inertia uses this response to dynamically hydrate and swap the page component without a full page reload. + +```mermaid +sequenceDiagram + participant Client + participant Server + + Client->>Server: First Visit + Server-->>Client: Returns HTML Skeleton + + Note over Client: Inertia.js is loaded + + Client->>Server: Inertia Request (X-Inertia: true) + Server-->>Client: Returns JSON Payload (Component Name, Props, etc.) + + Note over Client: Inertia.js swaps components +``` + +## Request Headers + +The following headers are automatically sent by Inertia when making requests. You don't need to set these manually, they're handled by the Inertia client-side adapter. + + + Set to `true` to indicate this is an Inertia request. + + + + Set to `XMLHttpRequest` on all Inertia requests. + + + + Set to `text/html, application/xhtml+xml` to indicate acceptable response + types. + + + + The current asset version to check for asset mismatches. + + + + Set to `prefetch` when making [prefetch](/v3/data-props/prefetching) requests. + + + + The component name for [partial reloads](/v3/data-props/partial-reloads). + + + + Comma-separated list of props to include in partial reloads. + + + + Comma-separated list of props to exclude from partial reloads. + + + + Comma-separated list of props to reset on navigation. + + + + Set to `no-cache` for reload requests to prevent serving stale content. + + + + Specifies which error bag to use for [validation + errors](/v3/the-basics/validation). + + + + Indicates whether the requested data should be appended or prepended when + using [Infinite scroll](/v3/data-props/infinite-scroll). + + + + Comma-separated list of non-expired [once prop](/v3/data-props/once-props) + keys already loaded on the client. The server will skip resolving these props + unless explicitly requested via a partial reload or force refreshed + server-side. + + +The following headers are used for [Precognition](/v3/the-basics/forms#precognition) validation requests. + + + Set to `true` to indicate this is a Precognition validation request. + + + + Comma-separated list of field names to validate. + + +## Response Headers + +The following headers should be sent by your server-side adapter in Inertia responses. If you're using an official server-side adapter, these are handled automatically. + + + Set to `true` to indicate this is an Inertia response. + + + + Used for external redirects when a `409 Conflict` response is returned due to + asset version mismatches. + + + + Set to `X-Inertia` to help browsers correctly differentiate between HTML and + JSON responses. + + +The following headers are used for [Precognition](/v3/the-basics/forms#precognition) validation responses. + + + Set to `true` to indicate this is a Precognition validation response. + + + + Set to `true` when validation passes with no errors, combined with a `204 No + Content` status code. + + + + Set to `Precognition` on all responses when the Precognition middleware is + applied. + + +## The Page Object + +Inertia shares data between the server and client via a page object. This object includes the necessary information required to render the page component, update the browser's history state, and track the site's asset version. The page object can include the following properties: + + + The name of the JavaScript page component. + + + + The page props. Contains all of the page data along with an `errors` object + (defaults to `{}` if there are no errors). + + + + The page URL. + + + + The current [asset version](/v3/advanced/asset-versioning). + + + + Whether or not to [encrypt the current page's history + state](/v3/security/history-encryption). + + + + Whether or not to clear any [encrypted history + state](/v3/security/history-encryption#clearing-history). + + + + Array of prop keys that should be [merged](/v3/data-props/merging-props) + (appended) during navigation. + + + + Array of prop keys that should be [prepended](/v3/data-props/merging-props) + during navigation. + + + + Array of prop keys that should be [deep + merged](/v3/data-props/merging-props#deep-merge) during navigation. + + + + Array of prop keys to use for [matching when merging + props](/v3/data-props/merging-props#matching-items). + + + + Configuration for [infinite scroll](/v3/data-props/infinite-scroll) prop + merging behavior. + + + + Configuration for client-side [lazy loading of + props](/v3/data-props/deferred-props). + + + + Configuration for [once props](/v3/data-props/once-props) that should only be + resolved once and reused on subsequent pages. Each entry maps a key to an + object containing the `prop` name and optional `expiresAt` timestamp (in + milliseconds). + + +On standard full page visits, the page object is JSON encoded into the `data-page` attribute in the root `
`. On Inertia visits (as indicated by the presence of the `X-Inertia` header), the page object is returned as the JSON payload. + +### Basic Page Object + +A minimal page object contains the core properties. + +```json +{ + "component": "User/Edit", + "props": { + "errors": {}, + "user": { + "name": "Jonathan" + } + }, + "url": "/user/123", + "version": "6b16b94d7c51cbe5b1fa42aac98241d5", + "clearHistory": false, + "encryptHistory": false +} +``` + +### Page Object with Deferred Props + +When using deferred props, the page object includes a `deferredProps` configuration. Note that deferred props are not included in the initial props since they are loaded in a subsequent request. + +```json +{ + "component": "Posts/Index", + "props": { + "errors": {}, + "user": { + "name": "Jonathan" + } + }, + "url": "/posts", + "version": "6b16b94d7c51cbe5b1fa42aac98241d5", + "clearHistory": false, + "encryptHistory": false, + "deferredProps": { + "default": ["comments", "analytics"], + "sidebar": ["relatedPosts"] + } +} +``` + +### Page Object with Merge Props + +When using merge props, additional configuration is included. + +```json +{ + "component": "Feed/Index", + "props": { + "errors": {}, + "user": { + "name": "Jonathan" + }, + "posts": [ + { + "id": 1, + "title": "First Post" + } + ], + "notifications": [ + { + "id": 2, + "message": "New comment" + } + ], + "conversations": { + "data": [ + { + "id": 1, + "title": "Support Chat", + "participants": ["John", "Jane"] + } + ] + } + }, + "url": "/feed", + "version": "6b16b94d7c51cbe5b1fa42aac98241d5", + "clearHistory": false, + "encryptHistory": false, + "mergeProps": ["posts"], + "prependProps": ["notifications"], + "deepMergeProps": ["conversations"], + "matchPropsOn": ["posts.id", "notifications.id", "conversations.data.id"] +} +``` + +### Page Object with Scroll Props + +When using [Infinite scroll](/v3/data-props/infinite-scroll), the page object includes a `scrollProps` configuration. + +```json +{ + "component": "Posts/Index", + "props": { + "errors": {}, + "posts": { + "data": [ + { + "id": 1, + "title": "First Post" + }, + { + "id": 2, + "title": "Second Post" + } + ] + } + }, + "url": "/posts?page=1", + "version": "6b16b94d7c51cbe5b1fa42aac98241d5", + "clearHistory": false, + "encryptHistory": false, + "mergeProps": ["posts.data"], + "scrollProps": { + "posts": { + "pageName": "page", + "previousPage": null, + "nextPage": 2, + "currentPage": 1 + } + } +} +``` + +### Page Object with Once Props + +When using [once props](/v3/data-props/once-props), the page object includes an `onceProps` configuration. Each entry maps a key to the prop name and an optional expiration timestamp. + +```json +{ + "component": "Billing/Plans", + "props": { + "errors": {}, + "plans": [ + { + "id": 1, + "name": "Basic" + }, + { + "id": 2, + "name": "Pro" + } + ] + }, + "url": "/billing/plans", + "version": "6b16b94d7c51cbe5b1fa42aac98241d5", + "clearHistory": false, + "encryptHistory": false, + "onceProps": { + "plans": { + "prop": "plans", + "expiresAt": null + } + } +} +``` + +When navigating to a subsequent page that includes the same once prop, the client sends the loaded keys in the `X-Inertia-Except-Once-Props` header. The server skips resolving these props and excludes them from the response. The client reuses the previously loaded values. + +```http +REQUEST +GET: https://example.com/billing/upgrade +Accept: text/html, application/xhtml+xml +X-Requested-With: XMLHttpRequest +X-Inertia: true +X-Inertia-Version: 6b16b94d7c51cbe5b1fa42aac98241d5 +X-Inertia-Except-Once-Props: plans + +RESPONSE +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "component": "Billing/Upgrade", + "props": { + "errors": {}, + "currentPlan": { + "id": 1, + "name": "Basic" + } + }, + "url": "/billing/upgrade", + "version": "6b16b94d7c51cbe5b1fa42aac98241d5", + "clearHistory": false, + "encryptHistory": false, + "onceProps": { + "plans": { + "prop": "plans", + "expiresAt": null + } + } +} +``` + +Note that `plans` is included in `onceProps` but not in `props` since it was already loaded on the client. The `onceProps` key identifies the once prop across pages, while `prop` specifies the actual prop name. These may differ when using [custom keys](/v3/data-props/once-props#custom-keys). + +## Asset Versioning + +One common challenge with single-page apps is refreshing site assets when they've been changed. Inertia makes this easy by optionally tracking the current version of the site's assets. In the event that an asset changes, Inertia will automatically make a full-page visit instead of an XHR visit. + +The Inertia [page object](#the-page-object) includes a `version` identifier. This version identifier is set server-side and can be a number, string, file hash, or any other value that represents the current "version" of your site's assets, as long as the value changes when the site's assets have been updated. + +Whenever an Inertia request is made, Inertia will include the current asset version in the `X-Inertia-Version` header. When the server receives the request, it compares the asset version provided in the `X-Inertia-Version` header with the current asset version. This is typically handled in the middleware layer of your server-side framework. + +If the asset versions are the same, the request simply continues as expected. However, if the asset versions are different, the server immediately returns a `409 Conflict` response, and includes the URL in a `X-Inertia-Location` header. This header is necessary, since server-side redirects may have occurred. This tells Inertia what the final intended destination URL is. + + + Note, `409 Conflict` responses are only sent for `GET` requests, and not for + `POST/PUT/PATCH/DELETE` requests. That said, they will be sent in the event + that a `GET` redirect occurs after one of these requests. + + +When the Inertia client receives a `409 Conflict` response, it checks for the presence of the `X-Inertia-Location` header. If this header exists, Inertia performs a full-page visit to the URL specified in the header. This ensures that the user always has the latest assets loaded. + +If "flash" session data exists when a `409 Conflict` response occurs, Inertia's server-side framework adapters will automatically reflash this data. + +```http +REQUEST +GET: https://example.com/events/80 +Accept: text/html, application/xhtml+xml +X-Requested-With: XMLHttpRequest +X-Inertia: true +X-Inertia-Version: 6b16b94d7c51cbe5b1fa42aac98241d5 + +RESPONSE +409: Conflict +X-Inertia-Location: https://example.com/events/80 +``` + +You can read more about this on the [asset versioning](/v3/advanced/asset-versioning) page. + +## Partial Reloads + +When making Inertia requests, the partial reload option allows you to request a subset of the props (data) from the server on subsequent visits to the _same_ page component. This can be a helpful performance optimization if it's acceptable that some page data becomes stale. See the [partial reloads](/v3/data-props/partial-reloads) documentation for details. + +When a partial reload request is made, Inertia includes the `X-Inertia-Partial-Component` header and may include `X-Inertia-Partial-Data` and/or `X-Inertia-Partial-Except` headers with the request. + +The `X-Inertia-Partial-Data` header is a comma separated list of the desired props (data) keys that should be returned. + +The `X-Inertia-Partial-Except` header is a comma separated list of the props (data) keys that should not be returned. When only the `X-Inertia-Partial-Except` header is included, all props (data) except those listed will be sent. If both `X-Inertia-Partial-Data` and `X-Inertia-Partial-Except` headers are included, the `X-Inertia-Partial-Except` header will take precedence. + +The `X-Inertia-Partial-Component` header includes the name of the component that is being partially reloaded. This is necessary, since partial reloads only work for requests made to the same page component. If the final destination is different for some reason (eg. the user was logged out and is now on the login page), then no partial reloading will occur. + +```http +REQUEST +GET: https://example.com/events +Accept: text/html, application/xhtml+xml +X-Requested-With: XMLHttpRequest +X-Inertia: true +X-Inertia-Version: 6b16b94d7c51cbe5b1fa42aac98241d5 +X-Inertia-Partial-Data: events +X-Inertia-Partial-Component: Events + +RESPONSE +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "component": "Events", + "props": { + "auth": {...}, // NOT included + "categories": [...], // NOT included + "events": [...], // Included + "errors": {} // ALWAYS included + }, + "url": "/events/80", + "version": "6b16b94d7c51cbe5b1fa42aac98241d5" +} +``` + +## HTTP Status Codes + +Inertia uses specific HTTP status codes to handle different scenarios. + +| Status Code | Description | +| :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **200 OK** | Standard successful response for both HTML and Inertia JSON responses. | +| **302 Found** | Standard redirect response. Inertia's server-side adapters automatically convert this to `303 See Other` when returned after `PUT`, `PATCH`, or `DELETE` requests. | +| **303 See Other** | Used for redirects after non-GET requests. This status code tells the browser to make a `GET` request to the redirect URL, preventing duplicate form submissions that could occur if the browser repeated the original request method. | +| **409 Conflict** | Returned when there's an asset version mismatch or for external redirects. For asset mismatches, this prompts a full page reload. For external redirects, the response includes an `X-Inertia-Location` header and triggers a `window.location` redirect client-side. | + +The following status codes are used for [Precognition](/v3/the-basics/forms#precognition) validation requests. + +| Status Code | Description | +| :--------------------------- | :--------------------------------------------------------------------------------------------- | +| **204 No Content** | Successful Precognition validation request with no validation errors. | +| **422 Unprocessable Entity** | Precognition validation request with validation errors. The response body contains the errors. | diff --git a/v3/core-concepts/who-is-it-for.mdx b/v3/core-concepts/who-is-it-for.mdx new file mode 100644 index 0000000..4a68a74 --- /dev/null +++ b/v3/core-concepts/who-is-it-for.mdx @@ -0,0 +1,15 @@ +--- +title: Who Is Inertia.js For? +--- + +Inertia was crafted for development teams and solo hackers who typically build server-side rendered applications using frameworks like Laravel, Ruby on Rails, Django, or Phoenix. You're used to creating controllers, retrieving data from the database (via an ORM), and rendering views. + +But what happens when you want to replace your server-side rendered views with a modern, JavaScript-based single-page application frontend? The answer is always "you need to build an API". Because that's how modern SPAs are built. + +This means building a REST or GraphQL API. It means figuring out authentication and authorization for that API. It means client-side state management. It means setting up a new Git repository. It means a more complicated deployment strategy. And this list goes on. It's a complete paradigm shift, and often a complete mess. We think there is a better way. + +> **Inertia empowers you to build a modern, JavaScript-based single-page application without the tiresome complexity.** + +Inertia works just like a classic server-side rendered application. You create controllers, you get data from the database (via your ORM), and you render views. But, Inertia views are JavaScript page components written in React, Vue, or Svelte. + +This means you get all the power of a client-side application and modern SPA experience, but you don't need to build an API. We think it's a breath of fresh air that will supercharge your productivity. diff --git a/v3/data-props/deferred-props.mdx b/v3/data-props/deferred-props.mdx new file mode 100644 index 0000000..5788ff8 --- /dev/null +++ b/v3/data-props/deferred-props.mdx @@ -0,0 +1,189 @@ +--- +title: Deferred Props +--- + +Inertia's deferred props feature allows you to defer the loading of certain page data until after the initial page render. This can be useful for improving the perceived performance of your app by allowing the initial page render to happen as quickly as possible. + +## Server Side + +To defer a prop, you can use the `Inertia::defer()` method when returning your response. This method receives a callback that returns the prop data. The callback will be executed in a separate request after the initial page render. + +```php +Route::get('/users', function () { + return Inertia::render('Users/Index', [ + 'users' => User::all(), + 'roles' => Role::all(), + 'permissions' => Inertia::defer(fn () => Permission::all()), + ]); +}); +``` + +### Grouping Requests + +By default, all deferred props get fetched in one request after the initial page is rendered, but you can choose to fetch data in parallel by grouping props together. + +```php +Route::get('/users', function () { + return Inertia::render('Users/Index', [ + 'users' => User::all(), + 'roles' => Role::all(), + 'permissions' => Inertia::defer(fn () => Permission::all()), + 'teams' => Inertia::defer(fn () => Team::all(), 'attributes'), + 'projects' => Inertia::defer(fn () => Project::all(), 'attributes'), + 'tasks' => Inertia::defer(fn () => Task::all(), 'attributes'), + ]); +}); +``` + +In the example above, the `teams`, `projects`, and `tasks` props will be fetched in one request, while the `permissions` prop will be fetched in a separate request in parallel. Group names are arbitrary strings and can be anything you choose. + +## Client Side + +On the client side, Inertia provides the `Deferred` component to help you manage deferred props. This component will automatically wait for the specified deferred props to be available before rendering its children. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +import { Deferred } from "@inertiajs/react"; + +export default () => ( + Loading...
}> + + +); +``` + +```svelte Svelte 4 icon="s" + + + + +
Loading...
+
+ + {#each permissions as permission} + + {/each} +
+``` + +```svelte Svelte 5 icon="s" + + + + {#snippet fallback()} +
Loading...
+ {/snippet} + + {#each permissions as permission} + + {/each} +
+``` + + + +## Multiple Deferred Props + +If you need to wait for multiple deferred props to become available, you can specify an array to the `data` prop. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +import { Deferred } from "@inertiajs/react"; + +export default () => ( + Loading...
}> + + +); +``` + +```svelte Svelte 4 icon="s" + + + + +
Loading...
+
+ + +
+``` + +```svelte Svelte 5 icon="s" + + + + {#snippet fallback()} +
Loading...
+ {/snippet} + + +
+``` + + + +## Combining with Once Props + +You may chain the `once()` modifier onto a deferred prop to ensure the data is resolved only once and remembered by the client across subsequent navigations. + +```php +return Inertia::render('Dashboard', [ + 'stats' => Inertia::defer(fn () => Stats::generate())->once(), +]); +``` + +For more information on once props, see the [once props](/v3/data-props/once-props) documentation. diff --git a/v3/data-props/flash-data.mdx b/v3/data-props/flash-data.mdx new file mode 100644 index 0000000..b841231 --- /dev/null +++ b/v3/data-props/flash-data.mdx @@ -0,0 +1,269 @@ +--- +title: Flash Data +--- + +v2.3.3+ + +Sometimes you may wish to send one-time data to your frontend that shouldn't reappear when users navigate through browser history. Unlike regular props, flash data isn't persisted in history state, making it ideal for success messages, newly created IDs, or other temporary values. + +## Flashing Data + +You may flash data using the `Inertia::flash()` method, passing a key and value or an array of key-value pairs. + +```php +public function store(Request $request) +{ + $user = User::create($request->validated()); + + Inertia::flash('message', 'User created successfully!'); + + // Or flash multiple values at once... + Inertia::flash([ + 'message' => 'User created!', + 'newUserId' => $user->id, + ]); + + return back(); +} +``` + +Chaining with `back()` is also supported. + +```php +return Inertia::flash('newUserId', $user->id)->back(); +``` + +You may also chain `flash()` onto `render()`, or vice versa. + +```php +return Inertia::render('Projects/Index', [ + 'projects' => $projects, +])->flash('highlight', $project->id); + +// Or... + +return Inertia::flash('highlight', $project->id) + ->render('Projects/Index', ['projects' => $projects]); +``` + +Flash data is scoped to the current request. The middleware automatically persists it to the session when redirecting. After the flash data is sent to the client, it is cleared and will not appear in subsequent requests. + +## Accessing Flash Data + +Flash data is available on `page.flash`. You may also listen for the global `flash` event or use the `onFlash` callback. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +import { usePage } from "@inertiajs/react"; + +export default function Layout({ children }) { + const { flash } = usePage(); + + return ( + <> + {flash.toast &&
{flash.toast.message}
} + {children} + + ); +} +``` + +```svelte Svelte icon="s" + + +{#if $page.flash.toast} +
{$page.flash.toast.message}
+{/if} +``` + +
+ +## The onFlash Callback + +You may use the `onFlash` callback to handle flash data when making requests. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.post("/users", data, { + onFlash: ({ newUserId }) => { + form.userId = newUserId; + }, +}); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.post("/users", data, { + onFlash: ({ newUserId }) => { + form.userId = newUserId; + }, +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.post("/users", data, { + onFlash: ({ newUserId }) => { + form.userId = newUserId; + }, +}); +``` + + + +## Global Flash Event + +You may use the global `flash` event to handle flash data in a central location, such as a layout component. For more information on events, see the [events documentation](/v3/advanced/events). + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.on("flash", (event) => { + if (event.detail.flash.toast) { + showToast(event.detail.flash.toast); + } +}); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.on("flash", (event) => { + if (event.detail.flash.toast) { + showToast(event.detail.flash.toast); + } +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.on("flash", (event) => { + if (event.detail.flash.toast) { + showToast(event.detail.flash.toast); + } +}); +``` + + + + + Event listeners registered inside components should be cleaned up when the + component unmounts to prevent them from accumulating and firing multiple + times. This is especially important in non-persistent layouts. See [removing + event listeners](/v3/advanced/events#removing-listeners) for more information. + + +Native browser events are also supported. + + + +```js Vue icon="vuejs" +document.addEventListener("inertia:flash", (event) => { + console.log(event.detail.flash); +}); +``` + +```js React icon="react" +document.addEventListener("inertia:flash", (event) => { + console.log(event.detail.flash); +}); +``` + +```js Svelte icon="s" +document.addEventListener("inertia:flash", (event) => { + console.log(event.detail.flash); +}); +``` + + + +The `flash` event is not cancelable. During [partial reloads](/v3/data-props/partial-reloads), it only fires if the flash data has changed. + +## Client-Side Flash + +You may set flash data on the client without a server request using the `router.flash()` method. Values are merged with existing flash data. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.flash("foo", "bar"); +router.flash({ foo: "bar" }); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.flash("foo", "bar"); +router.flash({ foo: "bar" }); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.flash("foo", "bar"); +router.flash({ foo: "bar" }); +``` + + + +A callback may also be passed to access the current flash data or replace it entirely. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.flash((current) => ({ ...current, bar: "baz" })); +router.flash(() => ({})); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.flash((current) => ({ ...current, bar: "baz" })); +router.flash(() => ({})); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.flash((current) => ({ ...current, bar: "baz" })); +router.flash(() => ({})); +``` + + + +## TypeScript + +You may configure the flash data type globally using [TypeScript's declaration merging](/v3/advanced/typescript#flash-data). + +## Testing + +For information on testing flash data, see the [testing documentation](/v3/advanced/testing#testing-flash-data). diff --git a/v3/data-props/infinite-scroll.mdx b/v3/data-props/infinite-scroll.mdx new file mode 100644 index 0000000..6b9665e --- /dev/null +++ b/v3/data-props/infinite-scroll.mdx @@ -0,0 +1,1065 @@ +--- +title: 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. + +```php +Route::get('/users', function () { + return Inertia::render('Users/Index', [ + 'users' => 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](https://laravel.com/docs/eloquent-resources). For more details, check out the [Inertia::scroll() method](#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. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +import { InfiniteScroll } from "@inertiajs/react"; + +export default function Users({ users }) { + return ( + + {users.data.map((user) => ( +
{user.name}
+ ))} +
+ ); +} +``` + +```svelte Svelte icon="s" + + + + {#each users.data as user (user.id)} +
{user.name}
+ {/each} +
+``` + +
+ +The component uses [intersection observers ](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) 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. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" + + {/* ... */} + +``` + +```svelte Svelte icon="s" + + + +``` + + + +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. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" + + {/* ... */} + +``` + +```svelte Svelte icon="s" + + + +``` + + + +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. + +## Resetting + +When filters or other parameters change, you may need to reset the infinite scroll data to start from the beginning. Without resetting, new results will merge with existing content instead of replacing it. + +You can reset data using the `reset` visit option. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +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 ( + + + + + + {users.data.map(user => ( +
+ {user.name} +
+ ))} +
+ + ) +} +``` + +```svelte Svelte icon="s" + + + + + + + {#each users.data as user (user.id)} +
{user.name}
+ {/each} +
+``` + +
+ +For more information about the reset option, see the [Resetting props](/v3/data-props/merging-props#resetting-props) documentation. + +## 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. + + + +```vue Vue icon="vuejs" + + + + + + + + + + + + + + +``` + +```jsx React icon="react" +{ + /* Only load the next page */ +} + + {/* ... */} +; + +{ + /* Only load the previous page */ +} + + {/* ... */} +; + +{ + /* Load in both directions (default) */ +} +{/* ... */}; +``` + +```svelte Svelte icon="s" + + + + + + + + + + + + + + +``` + + + +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. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" + + {/* ... */} + +``` + +```svelte Svelte icon="s" + + + +``` + + + +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"`. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" + + {/* ... */} + +``` + +```svelte Svelte icon="s" + + + +``` + + + +## 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](#slots) documentation. + + + +```vue Vue icon="vuejs" + +``` + +```jsx React icon="react" +import { InfiniteScroll } from "@inertiajs/react"; + +export default ({ users }) => ( + + hasMore && ( + + ) + } + next={({ loading, fetch, hasMore }) => + hasMore && ( + + ) + } + > + {users.data.map((user) => ( +
{user.name}
+ ))} +
+); +``` + +```svelte Svelte icon="s" + + + +
+ {#if hasMore} + + {/if} +
+ + {#each users.data as user (user.id)} +
{user.name}
+ {/each} + +
+ {#if hasMore} + + {/if} +
+
+``` + +
+ +You can also configure the component to automatically switch to manual mode after a certain number of pages using the `manualAfter` prop. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" + + {/* ... */} + +``` + +```svelte Svelte icon="s" + + + +``` + + + +## 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. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" + + {({ loading, loadingPrevious, loadingNext }) => ( +
{/* Your content with access to loading states */}
+ )} +
+``` + +```svelte Svelte icon="s" + + + +``` + +
+ +### 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. + + + +```vue Vue icon="vuejs" + +``` + +```jsx React icon="react" + "Loading more users..."}> + {/* Your content */} + +``` + +```svelte Svelte icon="s" + + + +
+ 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. + +```vue + +``` + +The `loading`, `previous`, and `next` slots receive the following properties: + +| Property | Description | +| :---------------- | :--------------------------------------------- | +| `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. + + + +```vue Vue icon="vuejs" + +``` + +```jsx React icon="react" + + {products.data.map((product) => ( +
  • {product.name}
  • + ))} +
    +``` + +```svelte Svelte icon="s" + + {#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](#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. + + + +```vue Vue icon="vuejs" + +``` + +```jsx React icon="react" + + + + + + + + + {users.data.map((user) => ( + + + + ))} + +
    Name
    {user.name}
    +
    +``` + +```svelte Svelte icon="s" + + + + + + + {#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. + + + +```vue Vue icon="vuejs" + +``` + +```jsx React icon="react" + + + + + + + + + {users.data.map((user) => ( + + + + ))} + + + + + + +
    Name
    {user.name}
    Footer
    +
    +``` + +```svelte Svelte icon="s" + + + + + + + {#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. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +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
    +
    + ); +}; +``` + +```svelte Svelte icon="s" + + + 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. + + + +```vue Vue icon="vuejs" + +``` + +```jsx React icon="react" +
    + + {users.data.map((user) => ( +
    {user.name}
    + ))} +
    +
    +``` + +```svelte Svelte icon="s" +
    + + {#each users.data as user (user.id)} +
    {user.name}
    + {/each} +
    +
    +``` + +
    + +### Multiple Scroll Containers + +Sometimes you may need to render multiple infinite scroll components on a single page. However, if both components use the default `page` query parameter for [URL synchronization](#url-synchronization), they will conflict with each other. To resolve this, instruct each paginator to use a custom `pageName`. + +```php +Route::get('/dashboard', function () { + return Inertia::render('Dashboard', [ + 'users' => Inertia::scroll( + fn() => User::paginate(pageName: 'users') + ), + 'orders' => Inertia::scroll( + fn() => Order::paginate(pageName: 'orders') + ), + ]); +}); +``` + +The `Inertia::scroll()` method automatically detects the `pageName` from each paginator, allowing both scroll containers to maintain independent pagination state. This results in URLs like `?users=2&orders=3` instead of conflicting `?page=` parameters. + +For more information about pagination page names, see [Laravel's documentation](https://laravel.com/docs/pagination#multiple-paginator-instances-per-page). + +## Programmatic Access + +When you need to trigger loading actions programmatically, you may use a template ref. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +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}
    + ))} +
    + + ) +} +``` + +```svelte Svelte icon="s" + + + + + + {#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. + +```php +// 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))); +``` + +If you don't use Laravel's paginator or use a different transformation layer, you may use the additional arguments that `scroll()` accepts. + +```php +// Customize the data wrapper key (defaults to 'data')... +Inertia::scroll($customPaginatedData, wrapper: 'items'); + +// Provide custom metadata resolution... +Inertia::scroll($data, metadata: $metadataProvider); +``` + +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. + +```php +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 {} +} +``` + +You may then use this custom metadata provider in your scroll function. + +```php +// 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) +); +``` + +To avoid repeating this setup in multiple controllers, you may define a macro. + +```php +// 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) +]); +``` diff --git a/v3/data-props/load-when-visible.mdx b/v3/data-props/load-when-visible.mdx new file mode 100644 index 0000000..03093eb --- /dev/null +++ b/v3/data-props/load-when-visible.mdx @@ -0,0 +1,519 @@ +--- +title: Load When Visible +--- + +Inertia supports lazy loading data on scroll using the Intersection Observer API. It provides the `WhenVisible` component as a convenient way to load data when an element becomes visible in the viewport. + +The `WhenVisible` component accepts a `data` prop that specifies the key of the prop to load. It also accepts a `fallback` prop that specifies a component to render while the data is loading. The `WhenVisible` component should wrap the component that depends on the data. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +import { WhenVisible } from "@inertiajs/react"; + +export default () => ( +
    Loading...
    }> + +
    +); +``` + +```svelte Svelte 4 icon="s" + + + + +
    Loading...
    +
    + + {#each permissions as permission} + + {/each} +
    +``` + +```svelte Svelte 5 icon="s" + + + + {#snippet fallback()} +
    Loading...
    + {/snippet} + + {#each permissions as permission} + + {/each} +
    +``` + +
    + +If you'd like to load multiple props when an element becomes visible, you can provide an array to the `data` prop. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +import { WhenVisible } from "@inertiajs/react"; + +export default () => ( +
    Loading...
    }> + +
    +); +``` + +```svelte Svelte 4 icon="s" + + + + +
    Loading...
    +
    + + +
    +``` + +```svelte Svelte 5 icon="s" + + + + {#snippet fallback()} +
    Loading...
    + {/snippet} + + +
    +``` + +
    + +## Loading Before Visible + +If you'd like to start loading data before the element is visible, you can provide a value to the `buffer` prop. The buffer value is a number that represents the number of pixels before the element is visible. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +import { WhenVisible } from "@inertiajs/react"; + +export default () => ( +
    Loading...
    } + > + +
    +); +``` + +```svelte Svelte 4 icon="s" + + + + +
    Loading...
    +
    + + {#each permissions as permission} + + {/each} +
    +``` + +```svelte Svelte 5 icon="s" + + + + {#snippet fallback()} +
    Loading...
    + {/snippet} + + {#each permissions as permission} + + {/each} +
    +``` + +
    + +In the above example, the data will start loading 500 pixels before the element is visible. + +By default, the `WhenVisible` component wraps the fallback template in a `div` element so it can ensure the element is visible in the viewport. If you want to customize the wrapper element, you can provide the `as` prop. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +import { WhenVisible } from "@inertiajs/react"; + +export default () => ( + + + +); +``` + +```svelte Svelte 4 icon="s" + + + + + +``` + +```svelte Svelte 5 icon="s" + + + + + +``` + + + +## Always Trigger + +By default, the `WhenVisible` component will only trigger once when the element becomes visible. If you want to always trigger the data loading when the element is visible, you can provide the `always`prop. + +This is useful when you want to load data every time the element becomes visible, such as when the element is at the end of an infinite scroll list and you want to load more data. Alternatively, you can use the [Infinite scroll](/v3/data-props/infinite-scroll) component which handles this use case for you. + +Note that if the data loading request is already in flight, the component will wait until it is finished to start the next request if the element is still visible in the viewport. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +import { WhenVisible } from "@inertiajs/react"; + +export default () => ( + + + +); +``` + +```svelte Svelte 4 icon="s" + + + + + +``` + +```svelte Svelte 5 icon="s" + + + + + +``` + + + +### Fetching State + +The `WhenVisible` component exposes a `fetching` slot prop that you may use to display a loading indicator during subsequent requests. This is useful because the `fallback` is only shown on the initial load, while `fetching` allows you to indicate that data is being refreshed on subsequent loads. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +import { WhenVisible } from "@inertiajs/react"; + +export default () => ( +
    Loading...
    }> + {({ fetching }) => ( + <> + + {fetching &&
    Refreshing...
    } + + )} +
    +); +``` + +```svelte Svelte 4 icon="s" + + + + + {#if fetching} +
    Refreshing...
    + {/if} + + +
    Loading...
    +
    +
    +``` + +```svelte Svelte 5 icon="s" + + + + + {#if fetching} +
    Refreshing...
    + {/if} + + {#snippet fallback()} +
    Loading...
    + {/snippet} +
    +``` + +
    + +## Form Submissions + +When submitting forms, you may want to use the `except` option to exclude the props that are being used by the `WhenVisible` component. This prevents the props from being reloaded when you get redirected back to the current page because of validation errors. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +import { useForm, WhenVisible } from "@inertiajs/react"; + +export default function CreateUser() { + const { data, setData, post } = useForm({ + name: "", + email: "", + }); + + function submit(e) { + e.preventDefault(); + post("/users", { + except: ["permissions"], + }); + } + + return ( + <> +
    {/* ... */}
    + + {/* ... */} + + ); +} +``` + +```svelte Svelte 4 icon="s" + + +
    + +
    + + + + +``` + +```svelte Svelte 5 icon="s" + + +
    + +
    + + + + +``` + +
    diff --git a/v3/data-props/merging-props.mdx b/v3/data-props/merging-props.mdx new file mode 100644 index 0000000..8dada03 --- /dev/null +++ b/v3/data-props/merging-props.mdx @@ -0,0 +1,148 @@ +--- +title: Merging Props +--- + +Inertia overwrites props with the same name when reloading a page. However, you may need to merge new data with existing data instead. For example, when implementing a "load more" button for paginated results. The [Infinite scroll](/v3/data-props/infinite-scroll) component uses prop merging under the hood. + +Prop merging only works during [partial reloads](/v3/data-props/partial-reloads). Full page visits will always replace props entirely, even if you've marked them for merging. + +## Merge Methods + +To merge a prop instead of overwriting it, you may use the `Inertia::merge()` method when returning your response. + +```php +Route::get('/items', function () { + // Static array of tags... + $allTags = [ + 'Laravel', 'React', 'Vue', 'Tailwind', 'Inertia', + 'PHP', 'JavaScript', 'TypeScript', 'Docker', 'Vite', + ]; + + // Get chunk of tags by page... + $page = request()->input('page', 1); + $perPage = 5; + $offset = ($page - 1) * $perPage; + $tags = array_slice($allTags, $offset, $perPage); + + return Inertia::render('Tags/Index', [ + 'tags' => Inertia::merge($tags), + ]); +}); +``` + +The `Inertia::merge()` method will append new items to existing arrays at the root level. You may change this behavior to prepend items instead. + +```php +// Append at root level (default)... +Inertia::merge($items); + +// Prepend at root level... +Inertia::merge($items)->prepend(); +``` + +For more precise control, you can target specific nested properties for merging while replacing the rest of the object. + +```php +// Only append to the 'data' array, replace everything else... +Inertia::merge(User::paginate())->append('data'); + +// Prepend to the 'messages' array... +Inertia::merge($chatData)->prepend('messages'); +``` + +You can combine multiple operations and target several properties at once. + +```php +Inertia::merge($forumData) + ->append('posts') + ->prepend('announcements'); + +// Target multiple properties... +Inertia::merge($dashboardData) + ->append(['notifications', 'activities']); +``` + +On the client side, Inertia handles all the merging automatically according to your server-side configuration. + +## Matching Items + +When merging arrays, you may use the `matchOn` parameter to match existing items by a specific field and update them instead of appending new ones. + +```php +// Match posts by ID, update existing ones... +Inertia::merge($postData)->append('data', matchOn: 'id'); + +// Multiple properties with different match fields... +Inertia::merge($complexData)->append([ + 'users.data' => 'id', + 'messages' => 'uuid', +]); +``` + +In the first example, Inertia will iterate over the `data` array and attempt to match each item by its `id` field. If a match is found, the existing item will be replaced. If no match is found, the new item will be appended. + +## Deep Merge + +Instead of specifying which nested paths should be merged, you may use `Inertia::deepMerge()`to ensure a deep merge of the entire structure. + +```php +Route::get('/chat', function () { + $chatData = [ + 'messages' => [ + ['id' => 4, 'text' => 'Hello there!', 'user' => 'Alice'], + ['id' => 5, 'text' => 'How are you?', 'user' => 'Bob'], + ], + 'online' => 12, + ]; + + return Inertia::render('Chat', [ + 'chat' => Inertia::deepMerge($chatData)->matchOn('messages.id'), + ]); +}); +``` + +`Inertia::deepMerge()` was introduced before `Inertia::merge()` had support for prepending and targeting nested paths. In most cases, `Inertia::merge()` with its append and prepend methods should be sufficient. + +## Client Side Visits + +You can also merge props directly on the client side without making a server request using [client side visits](/v3/the-basics/manual-visits#client-side-visits). Inertia provides [prop helper methods](/v3/the-basics/manual-visits#prop-helpers) that allow you to append, prepend, or replace prop values. + +## Combining with Deferred Props + +You may combine [deferred props](/v3/data-props/deferred-props) with mergeable props to defer the loading of the prop and ultimately mark it as mergeable once it's loaded. + +```php +Route::get('/users', function () { + $page = request()->input('page', 1); + $perPage = request()->input('per_page', 10); + + return Inertia::render('Users/Index', [ + 'results' => Inertia::defer(fn() => User::paginate($perPage, page: $page))->deepMerge(), + ]); +}); +``` + +## Combining with Once Props + +You may chain the `once()` modifier onto a merge prop to ensure the data is resolved only once and remembered by the client across subsequent navigations. + +```php +return Inertia::render('Users/Index', [ + 'activity' => Inertia::merge(fn () => $user->recentActivity())->once(), +]); +``` + +For more information on once props, see the [once props](/v3/data-props/once-props) documentation. + +## Resetting Props + +On the client side, you can indicate to the server that you would like to reset the prop. This is useful when you want to clear the prop value before merging new data, such as when the user enters a new search query on a paginated list. + +The `reset` request option accepts an array of the props keys you would like to reset. + +```js +router.reload({ + reset: ["results"], + // ... +}); +``` diff --git a/v3/data-props/once-props.mdx b/v3/data-props/once-props.mdx new file mode 100644 index 0000000..69bdeef --- /dev/null +++ b/v3/data-props/once-props.mdx @@ -0,0 +1,143 @@ +--- +title: Once Props +--- + +Some data rarely changes, is expensive to compute, or is simply large. Rather than including this data in every response, you may use _once props_. These props are remembered by the client and reused on subsequent pages that include the same prop. This makes them ideal for [shared data](/v3/data-props/shared-data). + +## Creating Once Props + +To create a once prop, use the `Inertia::once()` method when returning your response. This method receives a callback that returns the prop data. + +```php +return Inertia::render('Billing', [ + 'plans' => Inertia::once(fn () => Plan::all()), +]); +``` + +After the client has received this prop, subsequent requests will skip resolving the callback and exclude the prop from the response. The client only remembers once props while navigating between pages that include them. + +Navigating to a page without the once prop will forget the remembered value, and it will be resolved again on the next page that has it. In practice, this is rarely an issue since once props are typically used as shared data or within a specific section of your application. + +## Forcing a Refresh + +You may force a once prop to be refreshed using the `fresh()` method. + +```php +return Inertia::render('Billing', [ + 'plans' => Inertia::once(fn () => Plan::all())->fresh(), +]); +``` + +This method also accepts a boolean, allowing you to conditionally refresh the prop. + +```php +return Inertia::render('Billing', [ + 'plans' => Inertia::once(fn () => Plan::all())->fresh($condition), +]); +``` + +## Refreshing from the Client + +You may refresh a once prop from the client-side using a [partial reload](/v3/data-props/partial-reloads). The server will always resolve a once prop when explicitly requested. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.reload({ only: ["plans"] }); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.reload({ only: ["plans"] }); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.reload({ only: ["plans"] }); +``` + + + +## Expiration + +You may set an expiration time using the `until()` method. This method accepts a `DateTimeInterface`, `DateInterval`, or an integer (seconds). The prop will be refreshed on a subsequent visit after the expiration time has passed. + +```php +return Inertia::render('Dashboard', [ + 'rates' => Inertia::once(fn () => ExchangeRate::all())->until(now()->addDay()), +]); +``` + +## Custom Keys + +You may assign a custom key to the prop using the `as()` method. This is useful when you want to share data across multiple pages while using different prop names. + +```php +// Team member list... +return Inertia::render('Team/Index', [ + 'memberRoles' => Inertia::once(fn () => Role::all())->as('roles'), +]); + +// Invite form... +return Inertia::render('Team/Invite', [ + 'availableRoles' => Inertia::once(fn () => Role::all())->as('roles'), +]); +``` + +Both pages share the same underlying data because they use the same custom key, so the prop is only resolved for whichever page you visit first. + +## Sharing Once Props + +You may share once props globally using the `Inertia::share()` method. + +```php +Inertia::share('countries', Inertia::once(fn () => Country::all())); +``` + +Or, for convenience, you may use the `shareOnce()` method. + +```php +Inertia::shareOnce('countries', fn () => Country::all()); +``` + +You may also chain `as()`, `fresh()`, and `until()` onto the `shareOnce` method. + +```php +Inertia::shareOnce('countries', fn () => Country::all())->until(now()->addDay()); +``` + +Additionally, you may define a dedicated `shareOnce()` method in your middleware. The middleware will evaluate both `share()` and `shareOnce()`, merging the results. + +```php +class HandleInertiaRequests extends Middleware +{ + public function shareOnce(Request $request): array + { + return array_merge(parent::shareOnce($request), [ + 'countries' => fn () => Country::all(), + ]); + } +} +``` + +## Prefetching + +Once props are compatible with [prefetching](/v3/data-props/prefetching). The client automatically includes any remembered once props in prefetched responses, so navigating to a prefetched page will already have the once props available. + +Prefetched pages containing an expired once prop will be invalidated from the cache. + +## Combining with Other Prop Types + +The `once()` modifier may be chained onto [deferred](/v3/data-props/deferred-props), [merge](/v3/data-props/merging-props), and [optional](/v3/data-props/partial-reloads#lazy-data-evaluation) props. + +```php +return Inertia::render('Dashboard', [ + 'permissions' => Inertia::defer(fn () => Permission::all())->once(), + 'activity' => Inertia::merge(fn () => $user->recentActivity())->once(), + 'categories' => Inertia::optional(fn () => Category::all())->once(), +]); +``` diff --git a/v3/data-props/partial-reloads.mdx b/v3/data-props/partial-reloads.mdx new file mode 100644 index 0000000..167423c --- /dev/null +++ b/v3/data-props/partial-reloads.mdx @@ -0,0 +1,179 @@ +--- +title: Partial Reloads +--- + +When making visits to the same page you are already on, it's not always necessary to re-fetch all of the page's data from the server. In fact, selecting only a subset of the data can be a helpful performance optimization if it's acceptable that some page data becomes stale. Inertia makes this possible via its "partial reload" feature. + +As an example, consider a "user index" page that includes a list of users, as well as an option to filter the users by their company. On the first request to the page, both the `users` and `companies`props are passed to the page component. However, on subsequent visits to the same page (maybe to filter the users), you can request only the `users` data from the server without requesting the `companies` data. Inertia will then automatically merge the partial data returned from the server with the data it already has in memory client-side. + +Partial reloads only work for visits made to the same page component. + +## Only Certain Props + +To perform a partial reload, use the `only` visit option to specify which data the server should return. This option should be an array of keys which correspond to the keys of the props. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.visit(url, { + only: ["users"], +}); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.visit(url, { + only: ["users"], +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.visit(url, { + only: ["users"], +}); +``` + + + +## Except Certain Props + +In addition to the `only` visit option you can also use the `except` option to specify which data the server should exclude. This option should also be an array of keys which correspond to the keys of the props. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.visit(url, { + except: ["users"], +}); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.visit(url, { + except: ["users"], +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.visit(url, { + except: ["users"], +}); +``` + + + +## Router Shorthand + +Since partial reloads can only be made to the same page component the user is already on, it almost always makes sense to just use the `router.reload()` method, which automatically uses the current URL. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.reload({ only: ["users"] }); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.reload({ only: ["users"] }); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.reload({ only: ["users"] }); +``` + + + +## Using Links + +It's also possible to perform partial reloads with Inertia links using the `only` property. + + + +```vue Vue icon="vuejs" +import { Link } from '@inertiajs/vue3' + +Show active +``` + +```jsx React icon="react" +import { Link } from "@inertiajs/react"; + + + Show active +; +``` + +```svelte Svelte icon="s" +import { inertia, Link } from '@inertiajs/svelte' + +Show active + +Show active +``` + + + +## Lazy Data Evaluation + +For partial reloads to be most effective, be sure to also use lazy data evaluation when returning props from your server-side routes or controllers. This can be accomplished by wrapping all optional page data in a closure. + +```php +return Inertia::render('Users/Index', [ + 'users' => fn () => User::all(), + 'companies' => fn () => Company::all(), +]); +``` + +When Inertia performs a request, it will determine which data is required and only then will it evaluate the closure. This can significantly increase the performance of pages that contain a lot of optional data. + +Additionally, Inertia provides an `Inertia::optional()` method to specify that a prop should never be included unless explicitly requested using the `only` option: + +```php +return Inertia::render('Users/Index', [ + 'users' => Inertia::optional(fn () => User::all()), +]); +``` + +On the inverse, you can use the `Inertia::always()` method to specify that a prop should always be included, even if it has not been explicitly required in a partial reload. + +```php +return Inertia::render('Users/Index', [ + 'users' => Inertia::always(User::all()), +]); +``` + +Here's a summary of each approach: + +| Approach | Standard Visits | Partial Reloads | Evaluated | | +| :------------------------------------------------------------------------------------ | :-------------- | :-------------- | :--------------- | --- | +| `User::all()` | Always | Optionally | Always | +| `fn () => User::all()` | Always | Optionally | Only when needed | | +| `Inertia::optional(fn () => User::all())` | Never | Optionally | Only when needed | | +| `Inertia::always(fn () => User::all())` | Always | Always | Always | | + +## Combining with Once Props + +You may chain the `once()` modifier onto an optional prop to ensure the data is resolved only once and remembered by the client across subsequent navigations. + +```php +return Inertia::render('Users/Index', [ + 'users' => Inertia::optional(fn () => User::all())->once(), +]); +``` + +For more information on once props, see the [once props](/v3/data-props/once-props) documentation. diff --git a/v3/data-props/polling.mdx b/v3/data-props/polling.mdx new file mode 100644 index 0000000..3e9be7d --- /dev/null +++ b/v3/data-props/polling.mdx @@ -0,0 +1,154 @@ +--- +title: Polling +--- + +## Poll Helper + +Polling your server for new information on the current page is common, so Inertia provides a poll helper designed to help reduce the amount of boilerplate code. In addition, the poll helper will automatically stop polling when the page is unmounted. + +The only required argument is the polling interval in milliseconds. + + + +```js Vue icon="vuejs" +import { usePoll } from '@inertiajs/vue3' + +usePoll(2000) +``` + +```jsx React icon="react" +import { usePoll } from '@inertiajs/react' + +usePoll(2000) +``` + +```js Svelte icon="s" +import { usePoll } from '@inertiajs/svelte' + +usePoll(2000) +``` + + + +If you need to pass additional request options to the poll helper, you can pass any of the `router.reload` options as the second parameter. + + + +```js Vue icon="vuejs" +import { usePoll } from '@inertiajs/vue3' + +usePoll(2000, { + onStart() { + console.log('Polling request started') + }, + onFinish() { + console.log('Polling request finished') + } +}) +``` + +```jsx React icon="react" +import { usePoll } from '@inertiajs/react' + +usePoll(2000, { + onStart() { + console.log('Polling request started') + }, + onFinish() { + console.log('Polling request finished') + } +}) +``` + +```js Svelte icon="s" +import { usePoll } from '@inertiajs/svelte' + +usePoll(2000, { + onStart() { + console.log('Polling request started') + }, + onFinish() { + console.log('Polling request finished') + } +}) +``` + + + +If you'd like more control over the polling behavior, the poll helper provides `stop` and `start` methods that allow you to manually start and stop polling. You can pass the `autoStart: false` option to the poll helper to prevent it from automatically starting polling when the component is mounted. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +import { usePoll } from '@inertiajs/react' + +export default () => { + const { start, stop } = usePoll(2000, {}, { + autoStart: false, + }) + + return ( +
    + + +
    + ) +} +``` + +```js Svelte icon="s" +import { usePoll } from '@inertiajs/svelte' + +const { start, stop } = usePoll(2000, {}, { + autoStart: false, +}) +``` + +
    + +## Throttling + +By default, the poll helper will throttle requests by 90% when the browser tab is in the background. If you'd like to disable this behavior, you can pass the `keepAlive` option to the poll helper. + + + +```js Vue icon="vuejs" +import { usePoll } from '@inertiajs/vue3' + +usePoll(2000, {}, { + keepAlive: true, +}) +``` + +```jsx React icon="react" +import { usePoll } from '@inertiajs/react' + +usePoll(2000, {}, { + keepAlive: true, +}) +``` + +```js Svelte icon="s" +import { usePoll } from '@inertiajs/svelte' + +usePoll(2000, {}, { + keepAlive: true, +}) +``` + + diff --git a/v3/data-props/prefetching.mdx b/v3/data-props/prefetching.mdx new file mode 100644 index 0000000..2dc8f95 --- /dev/null +++ b/v3/data-props/prefetching.mdx @@ -0,0 +1,467 @@ +--- +title: Prefetching +--- + +Inertia supports prefetching data for pages that are likely to be visited next. This can be useful for improving the perceived performance of your app by allowing the data to be fetched in the background while the user is still interacting with the current page. + +## Link Prefetching + +To prefetch data for a page, you can add the `prefetch` prop to the Inertia link component. By default, Inertia will prefetch the data for the page when the user hovers over the link for more than 75ms. You may customize this hover delay by setting the `prefetch.hoverDelay` option in your [application defaults](/v3/installation/client-side-setup#configuring-defaults). + + + +```vue Vue icon="vuejs" +import { Link } from '@inertiajs/vue3' + +Users +``` + +```jsx React icon="react" +import { Link } from "@inertiajs/react"; + + + Users +; +``` + +```svelte Svelte icon="s" +import { inertia } from '@inertiajs/svelte' + +Users +``` + + + +By default, data is cached for 30 seconds before being evicted. You may customize this default value by setting the `prefetch.cacheFor` option in your [application defaults](/v3/installation/client-side-setup#configuring-defaults). You may also customize the cache duration on a per-link basis by passing a `cacheFor` prop to the `Link` component. + + + +```vue Vue icon="vuejs" +import { Link } from '@inertiajs/vue3' + +Users +Users +Users +``` + +```jsx React icon="react" +import { Link } from '@inertiajs/react' + +Users +Users +Users +``` + +```svelte Svelte icon="s" +import { inertia } from '@inertiajs/svelte' + +Users +Users +Users +``` + + + +Instead of prefetching on hover, you can also start prefetching on `mousedown` by passing the `click` value to the `prefetch` prop. + + + +```vue Vue icon="vuejs" +import { Link } from '@inertiajs/vue3' + +Users +``` + +```jsx React icon="react" +import { Link } from "@inertiajs/react"; + + + Users +; +``` + +```svelte Svelte icon="s" +import { inertia } from '@inertiajs/svelte' + +Users +``` + + + +If you're confident that the user will visit a page next, you can prefetch the data on mount as well. + + + +```vue Vue icon="vuejs" +import { Link } from '@inertiajs/vue3' + +Users +``` + +```jsx React icon="react" +import { Link } from "@inertiajs/react"; + + + Users +; +``` + +```svelte Svelte icon="s" +import { inerta } from '@inertiajs/svelte' + +Users +``` + + + +You can also combine prefetch strategies by passing an array of values to the `prefetch` prop. + + + +```vue Vue icon="vuejs" +import { Link } from '@inertiajs/vue3' + +Users +``` + +```jsx React icon="react" +import { Link } from "@inertiajs/react"; + + + Users +; +``` + +```svelte Svelte icon="s" +import { inertia } from '@inertiajs/svelte' + +Users +``` + + + +## Programmatic Prefetching + +You can prefetch data programmatically using `router.prefetch`. This method's signature is identical to `router.visit` with the exception of a third argument that allows you to specify prefetch options. + +When the `cacheFor` option is not specified, it defaults to 30 seconds. + +```js +router.prefetch("/users", { method: "get", data: { page: 2 } }); + +router.prefetch( + "/users", + { method: "get", data: { page: 2 } }, + { cacheFor: "1m" }, +); +``` + +Inertia also provides a `usePrefetch` hook that allows you to track the prefetch state for the current page. It returns information about whether the page is currently prefetching, has been prefetched, when it was last updated, and a `flush` method that flushes the cache for the current page only. + + + +```js Vue icon="vuejs" +import { usePrefetch } from "@inertiajs/vue3"; + +const { lastUpdatedAt, isPrefetching, isPrefetched, flush } = usePrefetch(); +``` + +```js React icon="react" +import { usePrefetch } from "@inertiajs/react"; + +const { lastUpdatedAt, isPrefetching, isPrefetched, flush } = usePrefetch(); +``` + +```js Svelte icon="s" +import { usePrefetch } from "@inertiajs/svelte"; + +const { lastUpdatedAt, isPrefetching, isPrefetched, flush } = usePrefetch(); +``` + + + +You can also pass visit options when you need to differentiate between different request configurations for the same URL. + + + +```js Vue icon="vuejs" +import { usePrefetch } from "@inertiajs/vue3"; + +const { lastUpdatedAt, isPrefetching, isPrefetched, flush } = usePrefetch({ + headers: { "X-Custom-Header": "value" }, +}); +``` + +```js React icon="react" +import { usePrefetch } from "@inertiajs/react"; + +const { lastUpdatedAt, isPrefetching, isPrefetched, flush } = usePrefetch({ + headers: { "X-Custom-Header": "value" }, +}); +``` + +```js Svelte icon="s" +import { usePrefetch } from "@inertiajs/svelte"; + +const { lastUpdatedAt, isPrefetching, isPrefetched, flush } = usePrefetch({ + headers: { "X-Custom-Header": "value" }, +}); +``` + + + +## Cache Tags + +Cache tags allow you to group related prefetched data and invalidate all cached data with that tag when specific events occur. + +To tag cached data, pass a `cacheTags` prop to your `Link` component. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +import { Link } from '@inertiajs/react' + +Users +Dashboard +``` + +```svelte Svelte icon="s" +import { inertia } from '@inertiajs/svelte' + +Users +Dashboard +``` + + + +When prefetching programmatically, pass `cacheTags` in the third argument to `router.prefetch`. + +```js +router.prefetch("/users", {}, { cacheTags: "users" }); +router.prefetch("/dashboard", {}, { cacheTags: ["dashboard", "stats"] }); +``` + +## Cache Invalidation + +You can manually flush the prefetch cache by calling `router.flushAll` to remove all cached data, or `router.flush` to remove cache for a specific page. + +```js +// Flush all prefetch cache +router.flushAll(); + +// Flush cache for a specific page +router.flush("/users", { method: "get", data: { page: 2 } }); + +// Using the usePrefetch hook +const { flush } = usePrefetch(); + +// Flush cache for the current page +flush(); +``` + +For more granular control, you can flush cached data by their tags using `router.flushByCacheTags`. This removes any cached response that contains _any_ of the specified tags. + +```js +// Flush all responses tagged with 'users' +router.flushByCacheTags("users"); + +// Flush all responses tagged with 'dashboard' OR 'stats' +router.flushByCacheTags(["dashboard", "stats"]); +``` + +### Automatic Cache Flushing + +By default, Inertia does not automatically flush the prefetch cache when you navigate to new pages. Cached data is only evicted when it expires based on the cache duration. If you want to flush all cached data on every navigation, you can set up an event listener. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.on("navigate", () => router.flushAll()); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.on("navigate", () => router.flushAll()); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.on("navigate", () => router.flushAll()); +``` + + + +### Invalidate on Requests + +To automatically invalidate caches when making requests, pass an `invalidateCacheTags` prop to the `Form` component. The specified tags will be flushed when the form submission succeeds. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +import { Form } from "@inertiajs/react"; + +export default () => ( +
    + + + +
    +); +``` + +```svelte Svelte icon="s" + + +
    + + + +
    +``` + +
    + +When using the `useForm` helper, you can include `invalidateCacheTags` in the visit options. + + + +```js Vue icon="vuejs" +import { useForm } from "@inertiajs/vue3"; + +const form = useForm({ + name: "", + email: "", +}); + +const submit = () => { + form.post("/users", { + invalidateCacheTags: ["users", "dashboard"], + }); +}; +``` + +```js React icon="react" +import { useForm } from "@inertiajs/react"; + +const { data, setData, post } = useForm({ + name: "", + email: "", +}); + +function submit(e) { + e.preventDefault(); + post("/users", { + invalidateCacheTags: ["users", "dashboard"], + }); +} +``` + +```js Svelte icon="s" +import { useForm } from "@inertiajs/svelte"; + +const form = useForm({ + name: "", + email: "", +}); + +function submit() { + $form.post("/users", { + invalidateCacheTags: ["users", "dashboard"], + }); +} +``` + + + +You can also invalidate cache tags with programmatic visits by including `invalidateCacheTags` in the options. + +```js +router.delete( + `/users/${userId}`, + {}, + { + invalidateCacheTags: ["users", "dashboard"], + }, +); + +router.post("/posts", postData, { + invalidateCacheTags: ["posts", "recent-posts"], +}); +``` + +## Stale While Revalidate + +By default, Inertia will fetch a fresh copy of the data when the user visits the page if the cached data is older than the cache duration. You can customize this behavior by passing a tuple to the `cacheFor`prop. + +The first value in the array represents the number of seconds the cache is considered fresh, while the second value defines how long it can be served as stale data before fetching data from the server is necessary. + + + +```vue Vue icon="vuejs" +import { Link } from '@inertiajs/vue3' + +Users +``` + +```jsx React icon="react" +import { Link } from "@inertiajs/react"; + + + Users +; +``` + +```svelte Svelte icon="s" +import { inertia } from '@inertiajs/svelte' + +Users +``` + + + +If a request is made within the fresh period (before the first value), the cache is returned immediately without making a request to the server. + +If a request is made during the stale period (between the two values), the stale value is served to the user, and a request is made in the background to refresh the cached data. Once the fresh data is returned, it is merged into the page so the user has the most recent data. + +If a request is made after the second value, the cache is considered expired, and the page and data is fetched from the sever as a regular request. diff --git a/v3/data-props/remembering-state.mdx b/v3/data-props/remembering-state.mdx new file mode 100644 index 0000000..b26ec81 --- /dev/null +++ b/v3/data-props/remembering-state.mdx @@ -0,0 +1,224 @@ +--- +title: Remembering State +--- + +When navigating browser history, Inertia restores pages using prop data cached in history state. However, Inertia does not restore local page component state since this is beyond its reach. This can lead to outdated pages in your browser history. + +For example, if a user partially completes a form, then navigates away, and then returns back, the form will be reset and their work will be lost. + +To mitigate this issue, you can tell Inertia which local component state to save in the browser history. + +## Saving Local State + +To save local component state to the history state, use the `useRemember` feature to tell Inertia which data it should remember. + + + +```js Vue icon="vuejs" +import { useRemember } from "@inertiajs/vue3"; + +const form = useRemember({ + first_name: null, + last_name: null, +}); +``` + +```jsx React icon="react" +import { useRemember } from "@inertiajs/react"; + +export default function Profile() { + const [formState, setFormState] = useRemember({ + first_name: null, + last_name: null, + // ... + }); + + // ... +} +``` + +```js Svelte icon="s" +import { useRemember } from "@inertiajs/svelte"; + +const form = useRemember({ + first_name: null, + last_name: null, +}); + +// ... +``` + + + +Now, whenever your local `form` state changes, Inertia will automatically save this data to the history state and will also restore it on history navigation. + +## Multiple Components + +If your page contains multiple components that use the remember functionality provided by Inertia, you need to provide a unique key for each component so that Inertia knows which data to restore to each component. + + + +```js Vue icon="vuejs" +import { useRemember } from "@inertiajs/vue3"; + +const form = useRemember( + { + first_name: null, + last_name: null, + }, + "Users/Create", +); +``` + +```jsx React icon="react" +import { useRemember } from "@inertiajs/react"; + +export default function Profile() { + const [formState, setFormState] = useRemember( + { + first_name: null, + last_name: null, + }, + "Users/Create", + ); +} +``` + +```js Svelte icon="s" +import { page, useRemember } from "@inertiajs/svelte"; + +const form = useRemember( + { + first_name: null, + last_name: null, + }, + "Users/Create", +); +``` + + + +If you have multiple instances of the same component on the page using the remember functionality, be sure to also include a unique key for each component instance, such as a model identifier. + + + +```js Vue icon="vuejs" +import { useRemember } from "@inertiajs/vue3"; + +const props = defineProps({ user: Object }); + +const form = useRemember( + { + first_name: null, + last_name: null, + }, + `Users/Edit:${props.user.id}`, +); +``` + +```jsx React icon="react" +import { useRemember } from "@inertiajs/react"; + +export default function Profile() { + const [formState, setFormState] = useRemember( + { + first_name: props.user.first_name, + last_name: props.user.last_name, + }, + `Users/Edit:${this.user.id}`, + ); +} +``` + +```js Svelte icon="s" +import { page, useRemember } from "@inertiajs/svelte"; + +const form = useRemember( + { + first_name: $page.props.user.first_name, + last_name: $page.props.user.last_name, + }, + `Users/Edit:${$page.props.user.id}`, +); +``` + + + +## Form Helper + +If you're using the [Inertia form helper](/v3/the-basics/forms#form-helper), you can pass a unique form key as the first argument when instantiating your form. This will cause the form data and errors to automatically be remembered. + + + +```js Vue icon="vuejs" +import { useForm } from "@inertiajs/vue3"; + +const form = useForm("CreateUser", data); +const form = useForm(`EditUser:${props.user.id}`, data); +``` + +```js React icon="react" +import { useForm } from "@inertiajs/react"; + +const form = useForm("CreateUser", data); +const form = useForm(`EditUser:${user.id}`, data); +``` + +```js Svelte icon="s" +import { useForm } from "@inertiajs/svelte"; + +const form = useForm("CreateUser", data); +const form = useForm(`EditUser:${user.id}`, data); +``` + + + +You may [exclude specific fields](/v3/the-basics/forms#excluding-fields) from being remembered using the `dontRemember()` method. This is useful for sensitive fields like passwords that should not be stored in history state. + +## Manually Saving State + +The `useRemember` hook watches for data changes and automatically saves those changes to the history state. Then, Inertia will restore the data on page load. + +However, it's also possible to manage this manually using the underlying `remember()` and `restore()` methods exposed by Inertia. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +// Save local component state to history state +router.remember(data, "my-key"); + +// Restore local component state from history state +let data = router.restore("my-key"); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +// Save local component state to history state +router.remember(data, "my-key"); + +// Restore local component state from history state +let data = router.restore("my-key"); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +// Save local component state to history state +router.remember(data, "my-key"); + +// Restore local component state from history state +let data = router.restore("my-key"); +``` + + + + + Some browsers limit the number of `history.replaceState()` calls allowed + within a short time period. Inertia catches this error and logs it to the + console, but the state update will be lost. Avoid calling `router.remember()` + too frequently, and consider debouncing or batching state updates in + high-frequency scenarios. + diff --git a/v3/data-props/shared-data.mdx b/v3/data-props/shared-data.mdx new file mode 100644 index 0000000..87383d3 --- /dev/null +++ b/v3/data-props/shared-data.mdx @@ -0,0 +1,150 @@ +--- +title: Shared Data +--- + +Sometimes you need to access specific pieces of data on numerous pages within your application. For example, you may need to display the current user in the site header. Passing this data manually in each response across your entire application is cumbersome. Thankfully, there is a better option: shared data. + +## Sharing Data + +Inertia's server-side adapters all provide a method of making shared data available for every request. This is typically done outside of your controllers. Shared data will be automatically merged with the page props provided in your controller. + +In Laravel applications, this is typically handled by the `HandleInertiaRequests` middleware that is automatically installed when installing the [server-side adapter](/v3/installation/server-side-setup#middleware). + +```php +class HandleInertiaRequests extends Middleware +{ + public function share(Request $request) + { + return array_merge(parent::share($request), [ + // Synchronously... + 'appName' => config('app.name'), + + // Lazily... + 'auth.user' => fn () => $request->user() + ? $request->user()->only('id', 'name', 'email') + : null, + ]); + } +} +``` + +Alternatively, you can manually share data using the `Inertia::share` method. + +```php +use Inertia\Inertia; + +// Synchronously... +Inertia::share('appName', config('app.name')); + +// Lazily... +Inertia::share('user', fn (Request $request) => $request->user() + ? $request->user()->only('id', 'name', 'email') + : null +); +``` + +Shared data should be used sparingly as all shared data is included with every response. + +Page props and shared data are merged together, so be sure to namespace your shared data appropriately to avoid collisions. + +## Sharing Once Props + +You may share data that is resolved only once and remembered by the client across subsequent navigations using [once props](/v3/data-props/once-props). + +```php +class HandleInertiaRequests extends Middleware +{ + public function share(Request $request) + { + return array_merge(parent::share($request), [ + 'countries' => Inertia::once(fn () => Country::all()), + ]); + } +} +``` + +Alternatively, you may define a dedicated `shareOnce()` method in the middleware. The middleware will evaluate both `share()` and `shareOnce()`, merging the results. + +```php +class HandleInertiaRequests extends Middleware +{ + public function shareOnce(Request $request): array + { + return array_merge(parent::shareOnce($request), [ + 'countries' => fn () => Country::all(), + ]); + } +} +``` + +You may also share once props manually using the `Inertia::shareOnce()` method. + +```php +Inertia::shareOnce('countries', fn () => Country::all()); +``` + +## Accessing Shared Data + +Once you have shared the data server-side, you will be able to access it within any of your pages or components. Here's an example of how to access shared data in a layout component. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +import { usePage } from "@inertiajs/react"; + +export default function Layout({ children }) { + const { auth } = usePage().props; + + return ( +
    +
    You are logged in as: {auth.user.name}
    +
    {children}
    +
    + ); +} +``` + +```svelte Svelte icon="s" + + +
    +
    + You are logged in as: {$page.props.auth.user.name} +
    +
    + +
    +
    +``` + +
    + +## TypeScript + +You may configure the shared props type globally using [TypeScript's declaration merging](/v3/advanced/typescript#shared-page-props). + +## Flash Data + +For one-time notifications like toast messages or success alerts, you may use [flash data](/v3/data-props/flash-data). Unlike shared data, flash data is not persisted in the browser's history state, so it won't reappear when navigating through history. diff --git a/v3/getting-started/demo-application.mdx b/v3/getting-started/demo-application.mdx new file mode 100644 index 0000000..8423277 --- /dev/null +++ b/v3/getting-started/demo-application.mdx @@ -0,0 +1,29 @@ +--- +title: Demo Application +--- + +We've setup a demo app for Inertia.js called [Ping CRM](https://demo.inertiajs.com). This application is built using Laravel and Vue. You can find the source code on [GitHub](https://github.com/inertiajs/pingcrm). + +The Ping CRM demo is hosted on Heroku and the database is reset every hour. Please be respectful when editing data. + + +[![Ping CRM](/images/pingcrm.png)](https://demo.inertiajs.com) + + +In addition to the Vue version of Ping CRM, we also maintain a Svelte version of the application, which you can find [on GitHub](https://github.com/inertiajs/pingcrm-svelte). + +## Third Party + +Beyond our official demo app, Ping CRM has also been translated into numerous different languages and frameworks. + +| Platform | Author | +|:-------------------------------------|:---------------------| +| [Clojure/React](https://github.com/prestancedesign/pingcrm-clojure) | Michaël Salihi | +| [Echo/Vue](https://github.com/kohkimakimoto/pingcrm-echo) | Kohki Makimoto | +| [Grails/Vue](https://github.com/matrei/pingcrm-grails) | Mattias Reichel | +| [Laravel/React](https://github.com/Landish/pingcrm-react) | Lado Lomidze | +| [Laravel/Mithril.js](https://github.com/tbreuss/pingcrm-mithril) | Thomas Breuss | +| [Laravel/Svelte](https://github.com/zgabievi/pingcrm-svelte) | Zura Gabievi | +| [Ruby on Rails/Vue](https://github.com/ledermann/pingcrm/) | Georg Ledermann | +| [Symfony/Vue](https://github.com/aleksblendwerk/pingcrm-symfony) | Aleks Seltenreich | +| [Yii 2/Vue](https://github.com/tbreuss/pingcrm-yii2) | Thomas Breuss | diff --git a/v3/getting-started/index.mdx b/v3/getting-started/index.mdx new file mode 100644 index 0000000..b71528e --- /dev/null +++ b/v3/getting-started/index.mdx @@ -0,0 +1,17 @@ +--- +title: Introduction +--- + +Inertia is a new approach to building classic server-driven web apps. We call it the modern monolith. + +Inertia allows you to create fully client-side rendered, single-page apps, without the complexity that comes with modern SPAs. It does this by leveraging existing server-side patterns that you already love. + +Inertia has no client-side routing, nor does it require an API. Simply build controllers and page views like you've always done! Inertia works great with any backend framework, but it's fine-tuned for [Laravel](https://laravel.com). + +## Not a Framework + +Inertia isn't a framework, nor is it a replacement for your existing server-side or client-side frameworks. Rather, it's designed to work with them. Think of Inertia as glue that connects the two. Inertia does this via adapters. We currently have three official client-side adapters (React, Vue, and Svelte) and three server-side adapters (Laravel, Rails, and Phoenix). + +## Next Steps + +Want to learn a bit more before diving in? Check out the [who is it for](/v3/core-concepts/who-is-it-for) and [how it works](/v3/core-concepts/how-it-works) pages. Or, if you're ready to get started, jump right into the [installation instructions](/v3/installation/server-side-setup). diff --git a/v3/getting-started/upgrade-guide.mdx b/v3/getting-started/upgrade-guide.mdx new file mode 100644 index 0000000..cf2dabb --- /dev/null +++ b/v3/getting-started/upgrade-guide.mdx @@ -0,0 +1,104 @@ +--- +title: Upgrade Guide for v2.0 +--- + +You can find the legacy docs for Inertia.js v1.0 at [inertiajs.com/docs/v1](/v1). + +## What's New + +Inertia.js v2.0 is a huge step forward for Inertia! The core library has been completely rewritten to architecturally support asynchronous requests, enabling a whole set of new features, including: + + + + Keep data fresh by automatically polling the server at a specified interval. + + + Speed up navigation by prefetching data for links when they become visible. + + + Load non-essential data after the initial page load to improve performance. + + + Seamlessly load more data as the user scrolls down the page. + + + Load data only when it becomes visible in the viewport. + + + Clear page data from browser history state when logging out of an + application. + + + +## Upgrade Dependencies + +To upgrade to the Inertia.js v2.0, first use npm to install the client-side adapter of your choice: + + + +```bash Vue icon="vuejs" +npm install @inertiajs/vue3@^2.0 +``` + +```bash React icon="react" +npm install @inertiajs/react@^2.0 +``` + +```bash Svelte icon="s" +npm install @inertiajs/svelte@^2.0 +``` + + + +Next, upgrade the `inertiajs/inertia-laravel` package to use the `2.x` dev branch: + +```bash +composer require inertiajs/inertia-laravel:^2.0 +``` + +## Breaking Changes + +While a significant release, Inertia.js v2.0 doesn't introduce many breaking changes. Here's a list of all the breaking changes: + +### Dropped Laravel 8 and 9 Support + +The Laravel adapter now requires Laravel 10 and PHP 8.1 at a minimum. + +### Dropped Vue 2 Support + +The Vue 2 adapter has been removed. Vue 2 reached End of Life on December 3, 2023, so this felt like it was time. + +### Router `replace` Method + +The previously deprecated `router.replace` method has been re-instated, but its functionality has changed. It is now used to make [Client Side](/v3/the-basics/manual-visits#client-side-visits) page visits. To make server-side visits that replace the current history entry in the browser, use the `replace` option: + +```javascript +router.get("/users", { search: "John" }, { replace: true }); +``` + +### Svelte Adapter + +- Dropped support for Svelte 3 as it reached End of Life on June 20, 2023. +- The `remember` helper has been rename to `useRemember` to be consistent with other helpers. +- Updated `setup` callback in `app.js`. You need to pass `props` when initializing the `App` component. [See setup in app.js](/v3/installation/client-side-setup#initialize-the-inertia-app) +- `setup` callback is now required in `ssr.js`. [See setup in ssr.js](/v3/advanced/server-side-rendering#add-server-entry-point) + +### Partial Reloads Are Now Async + +Previously partial reloads in Inertia were synchronous, just like all Inertia requests. In v2.0, partial reloads are now asynchronous. Generally this is desirable, but if you were relying on these requests being synchronous, you may need to adjust your code. diff --git a/v3/installation/client-side-setup.mdx b/v3/installation/client-side-setup.mdx new file mode 100644 index 0000000..9284ccf --- /dev/null +++ b/v3/installation/client-side-setup.mdx @@ -0,0 +1,268 @@ +--- +title: Client-Side Setup +--- + +Once you have your [server-side framework configured](/v3/installation/server-side-setup), you then need to setup your client-side framework. Inertia currently provides support for React, Vue, and Svelte. + + +Laravel's starter kits provide out-of-the-box scaffolding for new Inertia applications. + +These starter kits are the absolute fastest way to start building a new Inertia project using Laravel and Vue or React. However, if you would like to manually install Inertia into your application, please consult the documentation below. + + + +## Installation + + + + First, install the Inertia client-side adapter corresponding to your framework of choice. + + + ```bash Vue icon="vuejs" + npm install @inertiajs/vue3 + ``` + + ```bash React icon="react" + npm install @inertiajs/react + ``` + + ```bash Svelte icon="s" + npm install @inertiajs/svelte + ``` + + + + + Next, update your main JavaScript file to boot your Inertia app. To accomplish this, we'll initialize the client-side framework with the base Inertia component. + + + ```js Vue icon="vuejs" + import { createApp, h } from 'vue' + import { createInertiaApp } from '@inertiajs/vue3' + + createInertiaApp({ + resolve: name => { + const pages = import.meta.glob('./Pages/**/*.vue', { eager: true }) + return pages[`./Pages/${name}.vue`] + }, + setup({ el, App, props, plugin }) { + createApp({ render: () => h(App, props) }) + .use(plugin) + .mount(el) + }, + }) + ``` + + ```jsx React icon="react" + import { createInertiaApp } from '@inertiajs/react' + import { createRoot } from 'react-dom/client' + + createInertiaApp({ + resolve: name => { + const pages = import.meta.glob('./Pages/**/*.jsx', { eager: true }) + return pages[`./Pages/${name}.jsx`] + }, + setup({ el, App, props }) { + createRoot(el).render() + }, + }) + ``` + + ```js Svelte 4 icon="s" + import { createInertiaApp } from '@inertiajs/svelte' + + createInertiaApp({ + resolve: name => { + const pages = import.meta.glob('./Pages/**/*.svelte', { eager: true }) + return pages[`./Pages/${name}.svelte`] + }, + setup({ el, App, props }) { + new App({ target: el, props }) + }, + }) + ``` + + ```js Svelte 5 icon="s" + import { createInertiaApp } from '@inertiajs/svelte' + import { mount } from 'svelte' + + createInertiaApp({ + resolve: name => { + const pages = import.meta.glob('./Pages/**/*.svelte', { eager: true }) + return pages[`./Pages/${name}.svelte`] + }, + setup({ el, App, props }) { + mount(App, { target: el, props }) + }, + }) + ``` + + + The `setup` callback receives everything necessary to initialize the client-side framework, including the root Inertia `App` component. + + + + +## Configuring Defaults + +You may pass a `defaults` object to `createInertiaApp()` to configure default settings for various features. You don't have to pass a default for every key, just the ones you want to tweak. + +```js +createInertiaApp({ + // ... + defaults: { + form: { + recentlySuccessfulDuration: 5000, + }, + prefetch: { + cacheFor: "1m", + hoverDelay: 150, + }, + visitOptions: (href, options) => { + return { + headers: { + ...options.headers, + "X-Custom-Header": "value", + }, + }; + }, + }, +}); +``` + +The `visitOptions` callback receives the target URL and the current visit options, and should return an object with any options you want to override. For more details on the available configuration options, see the [forms](/v3/the-basics/forms#form-errors), [prefetching](/v3/data-props/prefetching), and [manual visits](/v3/the-basics/manual-visits#global-visit-options) documentation. + +### Updating Configuration at Runtime + +You may also update configuration values at runtime using the exported `config` instance. This is particularly useful when you need to adjust settings based on user preferences or application state. + + + +```js Vue icon="vuejs" +import { config } from "@inertiajs/vue3"; + +// Set a single value using dot notation... +config.set("form.recentlySuccessfulDuration", 1000); +config.set("prefetch.cacheFor", "5m"); + +// Set multiple values at once... +config.set({ + "form.recentlySuccessfulDuration": 1000, + "prefetch.cacheFor": "5m", +}); +``` + +```js React icon="react" +import { config } from "@inertiajs/react"; + +// Set a single value using dot notation... +config.set("form.recentlySuccessfulDuration", 1000); +config.set("prefetch.cacheFor", "5m"); + +// Set multiple values at once... +config.set({ + "form.recentlySuccessfulDuration": 1000, + "prefetch.cacheFor": "5m", +}); + +// Get a configuration value... +const duration = config.get("form.recentlySuccessfulDuration"); +``` + +```js Svelte icon="s" +import { config } from "@inertiajs/svelte"; + +// Set a single value using dot notation... +config.set("form.recentlySuccessfulDuration", 1000); +config.set("prefetch.cacheFor", "5m"); + +// Set multiple values at once... +config.set({ + "form.recentlySuccessfulDuration": 1000, + "prefetch.cacheFor": "5m", +}); + +// Get a configuration value... +const duration = config.get("form.recentlySuccessfulDuration"); +``` + + + +## Resolving Components + +The `resolve` callback tells Inertia how to load a page component. It receives a page name (string), and returns a page component module. How you implement this callback depends on which bundler (Vite or Webpack) you're using. + + + +```js Vue icon="vuejs" +// Vite +resolve: name => { + const pages = import.meta.glob('./Pages/**/*.vue', { eager: true }) + return pages[`./Pages/${name}.vue`] +}, + +// Webpack +resolve: name => require(`./Pages/${name}`), +``` + +```js React icon="react" +// Vite +resolve: name => { + const pages = import.meta.glob('./Pages/**/*.jsx', { eager: true }) + return pages[`./Pages/${name}.jsx`] +}, + +// Webpack +resolve: name => require(`./Pages/${name}`), +``` + +```js Svelte icon="s" +// Vite +resolve: name => { + const pages = import.meta.glob('./Pages/**/*.svelte', { eager: true }) + return pages[`./Pages/${name}.svelte`] +}, + +// Webpack +resolve: name => require(`./Pages/${name}.svelte`), +``` + + + +By default we recommend eager loading your components, which will result in a single JavaScript bundle. However, if you'd like to lazy-load your components, see our [code splitting](/v3/advanced/code-splitting) documentation. + +## Defining a Root Element + +By default, Inertia assumes that your application's root template has a root element with an `id` of `app`. If your application's root element has a different `id`, you can provide it using the `id` property. + +```js +createInertiaApp({ + id: "my-app", + // ... +}); +``` + +If you change the `id` of the root element, be sure to update it [server-side](/v3/installation/server-side-setup#root-template) as well. + +## Script Element for Page Data + +By default, Inertia stores the initial page data in a `data-page` attribute on the root element. You may configure Inertia to use a ` + + +``` + +```jsx React icon="react" +import { useForm } from "@inertiajs/react"; + +const { data, setData, post, progress } = useForm({ + name: null, + avatar: null, +}); + +function submit(e) { + e.preventDefault(); + post("/users"); +} + +return ( +
    + setData("name", e.target.value)} + /> + setData("avatar", e.target.files[0])} /> + {progress && ( + + {progress.percentage}% + + )} + +
    +); +``` + +```svelte Svelte 4 icon="s" + + +
    + + $form.avatar = e.target.files[0]} /> + {#if $form.progress} + + {$form.progress.percentage}% + + {/if} + +
    +``` + +```svelte Svelte 5 icon="s" + + +
    + + $form.avatar = e.target.files[0]} /> + {#if $form.progress} + + {$form.progress.percentage}% + + {/if} + +
    +``` + + + +This example uses the [Inertia form helper](/v3/the-basics/forms#form-helper) for convenience, since the form helper provides easy access to the current upload progress. However, you are free to submit your forms using [manual Inertia visits](/v3/the-basics/manual-visits) as well. + +## Multipart Limitations + +Uploading files using a `multipart/form-data` request is not natively supported in some server-side frameworks when using the `PUT`,`PATCH`, or `DELETE` HTTP methods. The simplest workaround for this limitation is to simply upload files using a `POST` request instead. + +However, some frameworks, such as [Laravel](https://laravel.com/docs/routing#form-method-spoofing) and [Rails ](https://guides.rubyonrails.org/form_helpers.html#how-do-forms-with-patch-put-or-delete-methods-work-questionmark), support form method spoofing, which allows you to upload the files using `POST`, but have the framework handle the request as a `PUT` or `PATCH` request. This is done by including a `_method` attribute in the data of your request. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.post(`/users/${user.id}`, { + _method: "put", + avatar: form.avatar, +}); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.post(`/users/${user.id}`, { + _method: "put", + avatar: form.avatar, +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.post(`/users/${user.id}`, { + _method: "put", + avatar: form.avatar, +}); +``` + + diff --git a/v3/the-basics/forms.mdx b/v3/the-basics/forms.mdx new file mode 100644 index 0000000..42b760f --- /dev/null +++ b/v3/the-basics/forms.mdx @@ -0,0 +1,2667 @@ +--- +title: Forms +--- + +import { ClientSpecific } from "/snippets/client-specific.jsx"; +import { ReactSpecific } from "/snippets/react-specific.jsx"; +import { SvelteSpecific } from "/snippets/svelte-specific.jsx"; +import { VueSpecific } from "/snippets/vue-specific.jsx"; + +Inertia provides two primary ways to build forms: the `
    ` component and the `useForm` helper. Both integrate with your server-side framework's validation and handle form submissions without full page reloads. + +## Form Component + +Inertia provides a `` component that behaves much like a classic HTML form, but uses Inertia under the hood to avoid full page reloads. This is the simplest way to get started with forms in Inertia. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +import { Form } from "@inertiajs/react"; + +export default () => ( + + + + + +); +``` + +```svelte Svelte icon="s" + + +
    + + + +
    +``` + +
    + + + Just like a traditional HTML form, there is no need to attach{" "} + a `v-model` + an `onChange` handler + a `bind:` to your input fields, just give + each input a `name` attribute{" "} + and a `defaultValue` (if applicable) and the + `Form` component will handle the data submission for you. + + +The component also supports nested data structures, file uploads, and dotted key notation. + + + +```vue Vue icon="vuejs" + +``` + +```jsx React icon="react" +
    + + + + + +
    +``` + +```svelte Svelte icon="s" +
    + + + + + +
    +``` + +
    + +You can pass a `transform` prop to modify the form data before submission. This is useful for injecting additional fields or transforming existing data, although hidden inputs work too. + + + +```vue Vue icon="vuejs" + +``` + +```jsx React icon="react" +
    ({ ...data, user_id: 123 })} +> + + +
    +``` + +```svelte Svelte icon="s" +
    ({ ...data, user_id: 123 })} +> + + +
    +``` + +
    + +### Wayfinder + +When using [Wayfinder](https://github.com/laravel/wayfinder), you can pass the resulting object directly to the `action` prop. The Form component will infer the HTTP method and URL from the Wayfinder object. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +import { Form } from "@inertiajs/react"; +import { store } from "App/Http/Controllers/UserController"; + +export default () => ( +
    + + + +
    +); +``` + +```svelte Svelte icon="s" + + +
    + + + +
    +``` + +
    + +### Default Values + + + You can set default values for form inputs using standard HTML attributes. Use{" "} + `defaultValue` + `defaultValue` + `value`for text inputs and textareas, and{" "} + `defaultChecked` + `defaultChecked` + `checked`for checkboxes and radios. + + + + +```vue Vue icon="vuejs" + +``` + +```jsx React icon="react" +
    + + + + + + + +
    +``` + +```svelte Svelte icon="s" +
    + + + + + + + +
    +``` + +
    + +### Checkbox Inputs + +When working with checkboxes, you may want to add an explicit `value` attribute such as `value="1"`. Without a value attribute, checked checkboxes will submit as `"on"`, which some server-side validation rules may not recognize as a proper boolean value. + +### Slot Props + +The `
    ` component exposes reactive state and helper methods through its default slot, giving you access to form processing state, errors, and utility functions. + + + +```vue Vue icon="vuejs" + +``` + +```jsx React icon="react" + + {({ + errors, + hasErrors, + processing, + progress, + wasSuccessful, + recentlySuccessful, + setError, + clearErrors, + resetAndClearErrors, + defaults, + isDirty, + reset, + submit, + }) => ( + + + {errors.name &&
    {errors.name}
    } + + + + {wasSuccessful &&
    User created successfully!
    } + )} + +``` + +```svelte Svelte 4 icon="s" +
    + + + {#if errors.name} +
    {errors.name}
    + {/if} + + + + {#if wasSuccessful} +
    User created successfully!
    + {/if} +
    +``` + +```svelte Svelte 5 icon="s" +
    + {#snippet children({ + errors, + hasErrors, + processing, + progress, + wasSuccessful, + recentlySuccessful, + setError, + clearErrors, + resetAndClearErrors, + defaults, + isDirty, + reset, + submit, + })} + + + {#if errors.name} +
    {errors.name}
    + {/if} + + + + {#if wasSuccessful} +
    User created successfully!
    + {/if} + {/snippet} +
    +``` + +
    + +The `defaults` method allows you to update the form's default values to match the current field values. When called, subsequent `reset()` calls will restore fields to these new defaults, and the `isDirty` property will track changes from these updated defaults. Unlike `useForm`, this method accepts no arguments and always uses all current form values. + +The `errors` object uses dotted notation for nested fields, allowing you to display validation messages for complex form structures. + + + +```vue Vue icon="vuejs" +
    + +
    {{ errors['user.name'] }}
    +
    +``` + +```jsx React icon="react" +
    + {({ errors }) => ( + + {errors['user.name'] &&
    {errors['user.name']}
    } + )} +
    +``` + +```svelte Svelte 4 icon="s" +
    + + {#if errors['user.name']} +
    {errors['user.name']}
    + {/if} +
    +``` + +```svelte Svelte 5 icon="s" +
    + {#snippet children({ errors })} + + {#if errors['user.name']} +
    {errors['user.name']}
    + {/if} + {/snippet} +
    +``` + +
    + +### Props and Options + +In addition to `action` and `method`, the `
    ` component accepts several props. Many of them are identical to the options available in Inertia's [visit options](/v3/the-basics/manual-visits). + + + +```vue Vue icon="vuejs" + +``` + +```jsx React icon="react" + ({ ...data, timestamp: Date.now() })} + invalidateCacheTags={["users", "dashboard"]} + disableWhileProcessing + options={{ + preserveScroll: true, + preserveState: true, + preserveUrl: true, + replace: true, + only: ["users", "flash"], + except: ["secret"], + reset: ["page"], + }} +> + + + +``` + +```svelte Svelte icon="s" +
    ({ ...data, timestamp: Date.now() })} + invalidateCacheTags={['users', 'dashboard']} + disableWhileProcessing + options={{ + preserveScroll: true, + preserveState: true, + preserveUrl: true, + replace: true, + only: ['users', 'flash'], + except: ['secret'], + reset: ['page'], + }} +> + + +
    +``` + +
    + +Some props are intentionally grouped under `options` instead of being top-level to avoid confusion. For example, `only`, `except`, and `reset` relate to _partial reloads_, not _partial submissions_. The general rule: top-level props are for the form submission itself, while `options` control how Inertia handles the subsequent visit. + + + When setting the `disableWhileProcessing` + `disableWhileProcessing` + `disable-while-processing`prop, the `Form` + component will add the `inert` attribute to the HTML `form`tag while the form + is processing to prevent user interaction. + + +To style the form while it's processing, you can target the inert form in the following ways. + + + +```jsx Tailwind 4 +
    + {/* Your form fields here */} +
    +``` + +```css CSS +form[inert] { + opacity: 0.5; + pointer-events: none; +} +``` + +
    + +### Events + +The `
    ` component emits all the standard visit [events](/v3/advanced/events) for form submissions. + + + +```vue Vue icon="vuejs" + +``` + +```jsx React icon="react" + + + + +``` + +```svelte Svelte 4 icon="s" +
    + + +
    +``` + +```svelte Svelte 5 icon="s" +
    + + +
    +``` + +
    + +### Resetting the Form + +The `Form` component provides several attributes that allow you to reset the form after a submission. + +`resetOnSuccess` may be used to reset the form after a successful submission. + + + +```vue Vue icon="vuejs" + +``` + +```jsx React icon="react" +// Reset the entire form on success +
    + + + +
    + +// Reset specific fields on success +
    + + + +
    +``` + +```svelte Svelte icon="s" + +
    + + + +
    + + +
    + + + +
    +``` + +
    + +`resetOnError` may be used to reset the form after errors. + + + +```vue Vue icon="vuejs" + +``` + +```jsx React icon="react" +// Reset the entire form on success +
    + + + +
    + +// Reset specific fields on success +
    + + + +
    +``` + +```svelte Svelte icon="s" + +
    + + + +
    + + +
    + + + +
    +``` + +
    + +### Setting New Default Values + +The `Form` component provides the `setDefaultsOnSuccess` attribute to set the current form values as the new defaults after a successful submission. + + + +```vue Vue icon="vuejs" + +``` + +```jsx React icon="react" +
    + + + +
    +``` + +```svelte Svelte icon="s" +
    + + + +
    +``` + +
    + +### Dotted Key Notation + +The `
    ` component supports dotted key notation for creating nested objects from flat input names. This provides a convenient way to structure form data. + + + +```vue Vue icon="vuejs" + +``` + +```jsx React icon="react" + + + + + + +``` + +```svelte Svelte icon="s" +
    + + + + +
    +``` + +
    + +The example above would generate the following data structure. + +```json +{ + "user": { + "name": "John Doe", + "skills": ["JavaScript"] + }, + "address": { + "street": "123 Main St" + } +} +``` + +If you need literal dots in your field names (not as nested object separators), you can escape them using backslashes. + + + +```vue Vue icon="vuejs" + +``` + +```jsx React icon="react" +
    + + + +
    +``` + +```svelte Svelte icon="s" +
    + + + +
    +``` + +
    + +The example above would generate the following data structure. + +```json +{ + "app.name": "My Application", + "settings": { + "theme.mode": "dark" + } +} +``` + +### Programmatic Access + +You can access the form's methods programmatically using refs. This provides an alternative to [slot props](#slot-props) when you need to trigger form actions from outside the form. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +import { useRef } from 'react' +import { Form } from '@inertiajs/react' + +export default function CreateUser() { + const formRef = useRef() + + const handleSubmit = () => { + formRef.current.submit() + } + + return ( +
    + + +
    + + + ) +} +``` + +```svelte Svelte icon="s" + + +
    + + +
    + + +``` + +
    + +In React and Vue, refs provide access to all form methods and reactive state. In Svelte, refs expose only methods, so reactive state like `isDirty` and `errors` should be accessed via [slot props](#slot-props) instead. + +### Form Context + +v2.3.9+ + +Sometimes you may wish to access form state or methods from deeply nested child components without passing props through multiple levels. The `useFormContext` hook provides access to the parent `
    ` component's state and methods from any child component. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +import { useFormContext } from "@inertiajs/react"; + +export default function FormActions() { + const form = useFormContext(); + + if (!form) { + return null; + } + + return ( +
    + {form.isDirty && Unsaved changes} + {form.errors.name && {form.errors.name}} + + +
    + ); +} +``` + +```svelte Svelte icon="s" + + +{#if $form} + {#if $form.isDirty}Unsaved changes{/if} + {#if $form.errors.name}{$form.errors.name}{/if} + + +{/if} +``` + +
    + +The context provides access to all the same properties and methods available through [slot props](#slot-props). + +### Precognition + +v2.3+ + +The `` component includes built-in support for [Laravel Precognition](https://laravel.com/docs/precognition), enabling real-time form validation without duplicating your server-side validation rules on the client. + + + Precognition requires server-side support. Laravel users should see the + [Laravel Precognition documentation](https://laravel.com/docs/precognition) + for setup instructions. For other frameworks, see the [protocol + page](/v3/core-concepts/the-protocol#request-headers) for implementation + details. + + +Once your server is configured, call `validate()` with a field name to trigger validation for that field. The `invalid()` helper checks if a field has validation errors, while `validating` indicates when a request is in progress. + + + +```vue Vue icon="vuejs" + +``` + +```jsx React icon="react" + + {({ errors, invalid, validate, validating }) => ( + <> + + validate("name")} /> + {invalid("name") &&

    {errors.name}

    } + + + validate("email")} /> + {invalid("email") &&

    {errors.email}

    } + + {validating &&

    Validating...

    } + + + + )} + +``` + +```svelte Svelte 4 icon="s" +
    + + validate('name')} /> + {#if invalid('name')} +

    {errors.name}

    + {/if} + + + validate('email')} /> + {#if invalid('email')} +

    {errors.email}

    + {/if} + + {#if validating} +

    Validating...

    + {/if} + + +
    +``` + +```svelte Svelte 5 icon="s" +
    + {#snippet children({ errors, invalid, validate, validating })} + + validate('name')} /> + {#if invalid('name')} +

    {errors.name}

    + {/if} + + + validate('email')} /> + {#if invalid('email')} +

    {errors.email}

    + {/if} + + {#if validating} +

    Validating...

    + {/if} + + + {/snippet} +
    +``` + +
    + +You may also use the `valid()` helper to check if a field has passed validation. + + + +```vue Vue icon="vuejs" +
    + +

    Valid email address

    +

    {{ errors.email }}

    +
    +``` + +```jsx React icon="react" +
    + {({ errors, invalid, valid, validate }) => ( + <> + validate("email")} /> + {valid("email") &&

    Valid email address

    } + {invalid("email") &&

    {errors.email}

    } + + )} +
    +``` + +```svelte Svelte 4 icon="s" +
    + validate('email')} /> + {#if valid('email')} +

    Valid email address

    + {/if} + {#if invalid('email')} +

    {errors.email}

    + {/if} +
    +``` + +```svelte Svelte 5 icon="s" +
    + {#snippet children({ errors, invalid, valid, validate })} + validate('email')} /> + {#if valid('email')} +

    Valid email address

    + {/if} + {#if invalid('email')} +

    {errors.email}

    + {/if} + {/snippet} +
    +``` + +
    + + + A form input will only appear as valid or invalid once it has changed and a + validation response has been received. + + +#### Validating Multiple Fields + +You may validate multiple fields at once using the `only` option. This is particularly useful when building wizard-style forms where you want to validate all visible fields before proceeding to the next step. + + + +```vue Vue icon="vuejs" +
    + + + + + +
    +``` + +```jsx React icon="react" +
    + {({ validate }) => ( + <> + {/* Step 1 fields */} + + + + + + )} +
    +``` + +```svelte Svelte 4 icon="s" +
    + + + + + +
    +``` + +```svelte Svelte 5 icon="s" +
    + {#snippet children({ validate })} + + + + + + {/snippet} +
    +``` + +
    + +#### Touch and Validate + +The `touch()` method marks fields as "touched" without triggering validation. You may then validate all touched fields by calling `validate()` without arguments. + + + +```vue Vue icon="vuejs" +
    + + + + + + +

    Name has been touched

    +
    +``` + +```jsx React icon="react" +
    + {({ validate, touch, touched }) => ( + <> + touch("name")} /> + touch("email")} /> + touch("phone")} /> + + + + {touched("name") &&

    Name has been touched

    } + + )} +
    +``` + +```svelte Svelte 4 icon="s" +
    + touch('name')} /> + touch('email')} /> + touch('phone')} /> + + + + {#if touched('name')} +

    Name has been touched

    + {/if} +
    +``` + +```svelte Svelte 5 icon="s" +
    + {#snippet children({ validate, touch, touched })} + touch('name')} /> + touch('email')} /> + touch('phone')} /> + + + + {#if touched('name')} +

    Name has been touched

    + {/if} + {/snippet} +
    +``` + +
    + +The `touched()` helper may also be called without arguments to check if any field has been touched. The `reset()` method clears the touched state for reset fields. + +#### Options + +The `validate()` method accepts an options object with callbacks and configuration. + +```js +validate("username", { + onSuccess: () => { + // Validation passed... + }, + onValidationError: (response) => { + // Validation failed (422 response)... + }, + onBeforeValidation: (newRequest, oldRequest) => { + // Return false to prevent validation... + }, + onFinish: () => { + // Always runs after validation... + }, +}); +``` + +You may also call `validate()` with only an options object to validate specific fields. + +```js +validate({ + only: ["name", "email"], + onSuccess: () => goToNextStep(), +}); +``` + +Validation requests are automatically debounced. The first request fires immediately, then subsequent changes are debounced (1500ms by default). You may customize this timeout. + + + +```vue Vue icon="vuejs" +
    + +
    +``` + +```jsx React icon="react" +
    + {/* ... */} +
    +``` + +```svelte Svelte icon="s" +
    + +
    +``` + +
    + +By default, files are excluded from validation requests to avoid unnecessary uploads. You may enable file validation when you need to validate file inputs like size or mime type. + + + +```vue Vue icon="vuejs" +
    + +
    +``` + +```jsx React icon="react" +
    + {/* ... */} +
    +``` + +```svelte Svelte icon="s" +
    + +
    +``` + +
    + +By default, validation errors are simplified to strings (the first error message). You may keep errors as arrays to display all error messages for fields with multiple validation rules. + + + +```vue Vue icon="vuejs" +
    + +
    +``` + +```jsx React icon="react" +
    + {/* ... */} +
    +``` + +```svelte Svelte icon="s" +
    + +
    +``` + +
    + +## Form Helper + +In addition to the `
    ` component, Inertia also provides a `useForm` helper for when you need programmatic control over your form's data and submission behavior. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +import { useForm } from "@inertiajs/react"; + +const { data, setData, post, processing, errors } = useForm({ + email: "", + password: "", + remember: false, +}); + +function submit(e) { + e.preventDefault(); + post("/login"); +} + +return ( + + setData("email", e.target.value)} + /> + {errors.email &&
    {errors.email}
    } + setData("password", e.target.value)} + /> + {errors.password &&
    {errors.password}
    } + setData("remember", e.target.checked)} + />{" "} + Remember Me + + +); +``` + +```svelte Svelte 4 icon="s" + + +
    + + {#if $form.errors.email} +
    {$form.errors.email}
    + {/if} + + {#if $form.errors.password} +
    {$form.errors.password}
    + {/if} + Remember Me + +
    +``` + +```svelte Svelte 5 icon="s" + + +
    + + {#if $form.errors.email} +
    {$form.errors.email}
    + {/if} + + {#if $form.errors.password} +
    {$form.errors.password}
    + {/if} + Remember Me + +
    +``` + +
    + +To submit the form, you may use the `get`, `post`, `put`, `patch`and `delete` methods. + + + +```js Vue icon="vuejs" +form.submit(method, url, options); +form.get(url, options); +form.post(url, options); +form.put(url, options); +form.patch(url, options); +form.delete(url, options); +``` + +```js React icon="react" +const { submit, get, post, put, patch, delete: destroy } = useForm({ ... }) + +submit(method, url, options) +get(url, options) +post(url, options) +put(url, options) +patch(url, options) +destroy(url, options) +``` + +```js Svelte icon="s" +$form.submit(method, url, options); +$form.get(url, options); +$form.post(url, options); +$form.put(url, options); +$form.patch(url, options); +$form.delete(url, options); +``` + + + +The submit methods support all of the typical [visit options](/v3/the-basics/manual-visits), such as `preserveState`, `preserveScroll`, and event callbacks, which can be helpful for performing tasks on successful form submissions. For example, you might use the `onSuccess` callback to reset inputs to their original state. + + + +```js Vue icon="vuejs" +form.post("/profile", { + preserveScroll: true, + onSuccess: () => form.reset("password"), +}); +``` + +```js React icon="react" +const { post, reset } = useForm({ ... }) + +post('/profile', { + preserveScroll: true, + onSuccess: () => reset('password'), +}) +``` + +```js Svelte icon="s" +$form.post("/profile", { + preserveScroll: true, + onSuccess: () => $form.reset("password"), +}); +``` + + + +If you need to modify the form data before it's sent to the server, you can do so via the `transform()` method. + + + +```js Vue icon="vuejs" +form + .transform((data) => ({ + ...data, + remember: data.remember ? "on" : "", + })) + .post("/login"); +``` + +```js React icon="react" +const { transform } = useForm({ ... }) + +transform((data) => ({ + ...data, + remember: data.remember ? 'on' : '', +})) +``` + +```js Svelte icon="s" +$form + .transform((data) => ({ + ...data, + remember: data.remember ? "on" : "", + })) + .post("/login"); +``` + + + +You can use the `processing` property to track if a form is currently being submitted. This can be helpful for preventing double form submissions by disabling the submit button. + + + +```vue Vue icon="vuejs" + +``` + +```jsx React icon="react" +const { processing } = useForm({ ... }) + + +``` + +```svelte Svelte icon="s" + +``` + + + +If your form is uploading files, the current progress event is available via the `progress` property, allowing you to easily display the upload progress. + + + +```vue Vue icon="vuejs" + + {{ form.progress.percentage }}% + +``` + +```jsx React icon="react" +const { progress } = useForm({ ... }) + +{progress && ( + + {progress.percentage}% + +)} +``` + +```svelte Svelte icon="s" +{#if $form.progress} + + {$form.progress.percentage}% + +{/if} +``` + + + +### Form Errors + +If there are form validation errors, they are available via the `errors` property. When building Laravel powered Inertia applications, form errors will automatically be populated when your application throws instances of `ValidationException`, such as when using `{'$request->validate()'}`. + + + +```vue Vue icon="vuejs" +
    {{ form.errors.email }}
    +``` + +```jsx React icon="react" +const { errors } = useForm({ ... }) + +{errors.email &&
    {errors.email}
    } +``` + +```svelte Svelte icon="s" +{#if $form.errors.email} +
    {$form.errors.email}
    +{/if} +``` + +
    + +For a more thorough discussion of form validation and errors, please consult the [validation documentation](/v3/the-basics/validation). + +To determine if a form has any errors, you may use the `hasErrors` property. To clear form errors, use the `clearErrors()` method. + + + +```js Vue icon="vuejs" +// Clear all errors... +form.clearErrors(); + +// Clear errors for specific fields... +form.clearErrors("field", "anotherfield"); +``` + +```js React icon="react" +const { clearErrors } = useForm({ ... }) + +// Clear all errors... +clearErrors() + +// Clear errors for specific fields... +clearErrors('field', 'anotherfield') +``` + +```js Svelte icon="s" +// Clear all errors... +$form.clearErrors(); + +// Clear errors for specific fields... +$form.clearErrors("field", "anotherfield"); +``` + + + +If you're using client-side input validation libraries or do client-side validation manually, you can set your own errors on the form using the `setErrors()` method. + + + +```js Vue icon="vuejs" +// Set a single error... +form.setError("field", "Your error message."); + +// Set multiple errors at once... +form.setError({ + foo: "Your error message for the foo field.", + bar: "Some other error for the bar field.", +}); +``` + +```js React icon="react" +const { setError } = useForm({ ... }) + +// Set a single error... +setError('field', 'Your error message.'); + +// Set multiple errors at once... +setError({ + foo: 'Your error message for the foo field.', + bar: 'Some other error for the bar field.' +}); +``` + +```js Svelte icon="s" +// Set a single error +$form.setError("field", "Your error message."); + +// Set multiple errors at once +$form.setError({ + foo: "Your error message for the foo field.", + bar: "Some other error for the bar field.", +}); +``` + + + +Unlike an actual form submission, the page's props remain unchanged when manually setting errors on a form instance. + +When a form has been successfully submitted, the `wasSuccessful` property will be `true`. In addition to this, forms have a `recentlySuccessful` property, which will be set to `true` for two seconds after a successful form submission. This property can be utilized to show temporary success messages. + +You may customize the duration of the `recentlySuccessful` state by setting the `form.recentlySuccessfulDuration` option in your [application defaults](/v3/installation/client-side-setup#configuring-defaults). The default value is `2000` milliseconds. + +### Resetting the Form + +To reset the form's values back to their default values, you can use the `reset()` method. + + + +```js Vue icon="vuejs" +// Reset the form... +form.reset(); + +// Reset specific fields... +form.reset("field", "anotherfield"); +``` + +```js React icon="react" +const { reset } = useForm({ ... }) + +// Reset the form... +reset() + +// Reset specific fields... +reset('field', 'anotherfield') +``` + +```js Svelte icon="s" +// Reset the form... +$form.reset(); + +// Reset specific fields... +$form.reset("field", "anotherfield"); +``` + + + +Sometimes, you may want to restore your form fields to their default values and clear any validation errors at the same time. Instead of calling `reset()` and `clearErrors()` separately, you can use the `resetAndClearErrors()` method, which combines both actions into a single call. + + + +```js Vue icon="vuejs" +// Reset the form and clear all errors... +form.resetAndClearErrors(); + +// Reset specific fields and clear their errors... +form.resetAndClearErrors("field", "anotherfield"); +``` + +```js React icon="react" +const { resetAndClearErrors } = useForm({ ... }) + +// Reset the form and clear all errors... +resetAndClearErrors() + +// Reset specific fields and clear their errors... +resetAndClearErrors('field', 'anotherfield') +``` + +```js Svelte icon="s" +// Reset the form and clear all errors... +$form.resetAndClearErrors(); + +// Reset specific fields and clear their errors... +$form.resetAndClearErrors("field", "anotherfield"); +``` + + + +### Setting New Default Values + +If your form's default values become outdated, you can use the `defaults()` method to update them. Then, the form will be reset to the correct values the next time the `reset()` method is invoked. + + + +```js Vue icon="vuejs" +// Set the form's current values as the new defaults... +form.defaults(); + +// Update the default value of a single field... +form.defaults("email", "updated-default@example.com"); + +// Update the default value of multiple fields... +form.defaults({ + name: "Updated Example", + email: "updated-default@example.com", +}); +``` + +```js React icon="react" +const { setDefaults } = useForm({ ... }) + +// Set the form's current values as the new defaults... +setDefaults() + +// Update the default value of a single field... +setDefaults('email', 'updated-default@example.com') + +// Update the default value of multiple fields... +setDefaults({ + name: 'Updated Example', + email: 'updated-default@example.com', +}) +``` + +```js Svelte icon="s" +// Set the form's current values as the new defaults... +$form.defaults(); + +// Update the default value of a single field... +$form.defaults("email", "updated-default@example.com"); + +// Change the default value of multiple fields... +$form.defaults({ + name: "Updated Example", + email: "updated-default@example.com", +}); +``` + + + +### Form Field Change Tracking + +To determine if a form has any changes, you may use the `isDirty` property. + + + +```vue Vue icon="vuejs" +
    There are unsaved form changes.
    +``` + +```jsx React icon="react" +const { isDirty } = useForm({ ... }) + +{isDirty &&
    There are unsaved form changes.
    } +``` + +```svelte Svelte icon="s" +{#if $form.isDirty} +
    There are unsaved form changes.
    +{/if} +``` + +
    + +### Canceling Form Submissions + +To cancel a form submission, use the `cancel()` method. + + + +```js Vue icon="vuejs" +form.cancel(); +``` + +```js React icon="react" +const { cancel } = useForm({ ... }) + +cancel() +``` + +```js Svelte icon="s" +$form.cancel(); +``` + + + +### Form Data and History State + +To instruct Inertia to store a form's data and errors in [history state](/v3/data-props/remembering-state), you can provide a unique form key as the first argument when instantiating your form. + + + +```js Vue icon="vuejs" +import { useForm } from '@inertiajs/vue3' + +const form = useForm('CreateUser', data) +const form = useForm(`EditUser:${user.id}\ +``` + +```js React icon="react" +import { useForm } from '@inertiajs/react' + +const form = useForm('CreateUser', data) +const form = useForm(`EditUser:${user.id}\ +``` + +```js Svelte icon="s" +import { useForm } from '@inertiajs/svelte' + +const form = useForm('CreateUser', data) +const form = useForm(`EditUser:${user.id}\ +``` + + + +#### Excluding Fields + +Sometimes you may wish to prevent certain fields from being stored in history state. For example, you may want to exclude password fields for security reasons. + + + +```js Vue icon="vuejs" +import { useForm } from "@inertiajs/vue3"; + +const form = useForm("LoginForm", { + email: "", + password: "", +}).dontRemember("password"); +``` + +```js React icon="react" +import { useForm } from "@inertiajs/react"; + +const form = useForm("LoginForm", { + email: "", + password: "", +}).dontRemember("password"); +``` + +```js Svelte icon="s" +import { useForm } from "@inertiajs/svelte"; + +const form = useForm("LoginForm", { + email: "", + password: "", +}).dontRemember("password"); +``` + + + +Multiple fields may be excluded by passing additional arguments. + +```js +form.dontRemember("password", "password_confirmation"); +``` + + + Some browsers trigger a "save password" prompt whenever password field values + are written to history state, even without form submission. Excluding password + fields avoids this issue. + + +### Wayfinder + +v2.0.6+ + +When using [Wayfinder](https://github.com/laravel/wayfinder) in conjunction with the form helper, you can simply pass the resulting object directly to the `form.submit` method. The form helper will infer the HTTP method and URL from the Wayfinder object. + + + +```js Vue icon="vuejs" +import { useForm } from "@inertiajs/vue3"; +import { store } from "App/Http/Controllers/UserController"; + +const form = useForm({ + name: "John Doe", + email: "john.doe@example.com", +}); + +form.submit(store()); +``` + +```js React icon="react" +import { useForm } from "@inertiajs/react"; +import { store } from "App/Http/Controllers/UserController"; + +const form = useForm({ + name: "John Doe", + email: "john.doe@example.com", +}); + +form.submit(store()); +``` + +```js Svelte icon="s" +import { useForm } from "@inertiajs/svelte"; +import { store } from "App/Http/Controllers/UserController"; + +const form = useForm({ + name: "John Doe", + email: "john.doe@example.com", +}); + +form.submit(store()); +``` + + + +### Precognition + +v2.3+ + +Just like the `
    ` component, the `useForm` helper supports [Precognition](#precognition) for real-time validation. You may enable it by chaining the `withPrecognition()` method with the HTTP method and endpoint for validation requests. + + + Precognition requires server-side support. Laravel users should see the + [Laravel Precognition documentation](https://laravel.com/docs/precognition) + for setup instructions. For other frameworks, see the [protocol + page](/v3/core-concepts/the-protocol#request-headers) for implementation + details. + + + + +```js Vue icon="vuejs" +import { useForm } from "@inertiajs/vue3"; + +const form = useForm({ + name: "", + email: "", +}).withPrecognition("post", "/users"); +``` + +```js React icon="react" +import { useForm } from "@inertiajs/react"; + +const form = useForm({ + name: "", + email: "", +}).withPrecognition("post", "/users"); +``` + +```js Svelte icon="s" +import { useForm } from "@inertiajs/svelte"; + +const form = useForm({ + name: "", + email: "", +}).withPrecognition("post", "/users"); +``` + + + +For backwards compatibility with the `laravel-precognition` packages, you may also pass the method and URL as the first arguments to `useForm()`. + +```js +const form = useForm("post", "/users", { + name: "", + email: "", +}); +``` + + + Since Precognition is now built-in, you may remove the `laravel-precognition` + package and import `useForm` from your Inertia adapter instead. + + +You may also use [Wayfinder](https://github.com/laravel/wayfinder) when enabling Precognition. + +```js +import { store } from "App/Http/Controllers/UserController"; + +const form = useForm({ + name: "", + email: "", +}).withPrecognition(store()); + +// Or passing Wayfinder as the first argument... +const form = useForm(store(), { + name: "", + email: "", +}); +``` + +Once Precognition is enabled, call `validate()` with a field name to trigger validation for that field. The `invalid()` helper checks if a field has validation errors, while `validating` indicates when a request is in progress. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +import { useForm } from "@inertiajs/react"; + +const { data, setData, post, errors, validating, validate, invalid } = useForm( + "post", + "/users", + { + name: "", + email: "", + }, +); + +function submit(e) { + e.preventDefault(); + post("/users"); +} + +return ( + + setData("name", e.target.value)} + onBlur={() => validate("name")} + /> + {invalid("name") &&

    {errors.name}

    } + + setData("email", e.target.value)} + onBlur={() => validate("email")} + /> + {invalid("email") &&

    {errors.email}

    } + + {validating &&

    Validating...

    } + + + +); +``` + +```svelte Svelte 4 icon="s" + + +
    $form.post('/users')}> + $form.validate('name')} /> + {#if $form.invalid('name')} +

    {$form.errors.name}

    + {/if} + + $form.validate('email')} /> + {#if $form.invalid('email')} +

    {$form.errors.email}

    + {/if} + + {#if $form.validating} +

    Validating...

    + {/if} + + +
    +``` + +```svelte Svelte 5 icon="s" + + +
    { e.preventDefault(); $form.post('/users') }}> + $form.validate('name')} /> + {#if $form.invalid('name')} +

    {$form.errors.name}

    + {/if} + + $form.validate('email')} /> + {#if $form.invalid('email')} +

    {$form.errors.email}

    + {/if} + + {#if $form.validating} +

    Validating...

    + {/if} + + +
    +``` + +
    + +You may also use the `valid()` helper to check if a field has passed validation. + +#### Touch and Validate + +The `touch()` method marks fields as "touched" without triggering validation. You may then validate all touched fields by calling `validate()` without arguments. The `touched()` helper checks if a field has been touched. The `reset()` method clears the touched state for reset fields. + + + +```vue Vue icon="vuejs" + + + + + +

    Name has been touched

    +``` + +```jsx React icon="react" + setData('name', e.target.value)} onBlur={() => touch('name')} /> + setData('email', e.target.value)} onBlur={() => touch('email')} /> + + + +{touched('name') &&

    Name has been touched

    } +``` + +```svelte Svelte icon="s" + $form.touch('name')} /> + $form.touch('email')} /> + + + +{#if $form.touched('name')} +

    Name has been touched

    +{/if} +``` + +
    + +#### Options + +Validation requests are automatically debounced. The first request fires immediately, then subsequent changes are debounced (1500ms by default). You may customize this timeout using `setValidationTimeout()`. + + + +```js Vue icon="vuejs" +const form = useForm("post", "/users", { + name: "", +}).setValidationTimeout(500); +``` + +```js React icon="react" +const form = useForm("post", "/users", { + name: "", +}); + +form.setValidationTimeout(500); +``` + +```js Svelte icon="s" +const form = useForm("post", "/users", { + name: "", +}); + +$form.setValidationTimeout(500); +``` + + + +By default, files are excluded from validation requests to avoid unnecessary uploads. You may enable file validation using `validateFiles()`. + + + +```js Vue icon="vuejs" +const form = useForm("post", "/users", { + avatar: null, +}).validateFiles(); +``` + +```js React icon="react" +const form = useForm("post", "/users", { + avatar: null, +}); + +form.validateFiles(); +``` + +```js Svelte icon="s" +const form = useForm("post", "/users", { + avatar: null, +}); + +$form.validateFiles(); +``` + + + +By default, validation errors are simplified to strings (the first error message). You can indicate you would like all errors as arrays using `withAllErrors()`. + + + +```js Vue icon="vuejs" +const form = useForm("post", "/users", { + name: "", +}).withAllErrors(); +``` + +```js React icon="react" +const form = useForm("post", "/users", { + name: "", +}); + +form.withAllErrors(); +``` + +```js Svelte icon="s" +const form = useForm("post", "/users", { + name: "", +}); + +$form.withAllErrors(); +``` + + + +With Precognition enabled, you may call `submit()` without arguments to submit to the configured endpoint. + +## Server-Side Responses + +When using Inertia, you don't typically inspect form responses client-side like you would with traditional XHR/fetch requests. Instead, your server-side route or controller issues a [redirect](/v3/the-basics/redirects) response after processing the form, often redirecting to a success page. + +```php +class UsersController extends Controller +{ + public function index() + { + return Inertia::render('Users/Index', [ + 'users' => User::all(), + ]); + } + + public function store(Request $request) + { + User::create($request->validate([ + 'first_name' => ['required', 'max:50'], + 'last_name' => ['required', 'max:50'], + 'email' => ['required', 'max:50', 'email'], + ])); + + return to_route('users.index'); + } +} +``` + +This redirect-based approach works with all form submission methods: the `
    ` component, `useForm` helper, and manual router submissions. It makes handling Inertia forms feel very similar to classic server-side form submissions. + +## Server-Side Validation + +Both the `` component and `useForm` helper automatically handle server-side validation errors. When your server returns validation errors, they're automatically available in the `errors` object without any additional configuration. + +Unlike traditional XHR/fetch requests where you might check for a `422` status code, Inertia handles validation errors as part of its redirect-based flow, just like classic server-side form submissions, but without the full page reload. + +For a complete guide on validation error handling, including error bags and advanced scenarios, see the [validation documentation](/v3/the-basics/validation). + +## Manual Form Submissions + +It's also possible to submit forms manually using Inertia's `router` methods directly, without using the `` component or `useForm` helper: + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +import { useState } from "react"; +import { router } from "@inertiajs/react"; + +export default function Edit() { + const [values, setValues] = useState({ + first_name: "", + last_name: "", + email: "", + }); + + function handleChange(e) { + const key = e.target.id; + const value = e.target.value; + setValues((values) => ({ + ...values, + [key]: value, + })); + } + + function handleSubmit(e) { + e.preventDefault(); + router.post("/users", values); + } + + return ( + + + + + + + + + + ); +} +``` + +```svelte Svelte 4 icon="s" + + +
    + + + + + + + + + + +
    +``` + +```svelte Svelte 5 icon="s" + + +
    + + + + + + + + + + +
    +``` + +
    + +## File Uploads + +When making requests or form submissions that include files, Inertia will automatically convert the request data into a `FormData` object. This works with the `
    ` component, `useForm`helper, and manual router submissions. + +For more information on file uploads, including progress tracking, see the [file uploads documentation](/v3/the-basics/file-uploads). + +## XHR / Fetch Submissions + +Using Inertia to submit forms works great for the vast majority of situations. However, in the event that you need more control over the form submission, you're free to make plain XHR or `fetch` requests instead, using the library of your choice. diff --git a/v3/the-basics/links.mdx b/v3/the-basics/links.mdx new file mode 100644 index 0000000..6c8d288 --- /dev/null +++ b/v3/the-basics/links.mdx @@ -0,0 +1,422 @@ +--- +title: Links +--- + +To create links to other pages within an Inertia app, you will typically use the Inertia `` component. This component is a light wrapper around a standard anchor `` link that intercepts click events and prevents full page reloads. This is [how Inertia provides a single-page app experience](/v3/core-concepts/how-it-works) once your application has been loaded. + +## Creating Links + +To create an Inertia link, use the Inertia `` component. Any attributes you provide to this component will be proxied to the underlying HTML tag. + + + +```vue Vue icon="vuejs" +import { Link } from '@inertiajs/vue3' + +Home +``` + +```jsx React icon="react" +import { Link } from "@inertiajs/react"; + +Home; +``` + +```svelte Svelte icon="s" +import { inertia, Link } from '@inertiajs/svelte' + +Home + +Home +``` + + + +By default, Inertia renders links as anchor `` elements. However, you can change the tag using the `as` prop. + + + +```vue Vue icon="vuejs" +import { Link } from '@inertiajs/vue3' + +Logout + +// Renders as... + +``` + +```jsx React icon="react" +import { Link } from '@inertiajs/react' + +Logout + +// Renders as... + +``` + +```svelte Svelte icon="s" +import { Link } from '@inertiajs/svelte' + +Logout + +// Renders as... + +``` + + + + + Creating `POST`/`PUT`/`PATCH`/ `DELETE` anchor `` links is discouraged as it causes "Open Link in New Tab / Window" accessibility issues. The component automatically renders a{` `} ` + +Logout +``` + + + +## Wayfinder + +v2.0.6+ + +When using [Wayfinder](https://github.com/laravel/wayfinder) in conjunction with the `Link`component, you can simply pass the resulting object directly to the `href` prop. The `Link` will infer the HTTP method and URL directly from the Wayfinder object. + + + +```vue Vue icon="vuejs" +import { Link } from '@inertiajs/vue3' import { show } from +'App/Http/Controllers/UserController' + +John Doe +``` + +```jsx React icon="react" +import { Link } from "@inertiajs/react"; +import { show } from "App/Http/Controllers/UserController"; + +John Doe; +``` + +```svelte Svelte icon="s" +import { inertia, Link } from '@inertiajs/svelte' +import { show } from 'App/Http/Controllers/UserController' + + + +John Doe +``` + + + +## Data + +When making `POST` or `PUT` requests, you may wish to add additional data to the request. You can accomplish this using the `data` prop. The provided data can be an `object` or `FormData` instance. + + + +```vue Vue icon="vuejs" +import { Link } from '@inertiajs/vue3' + +Save +``` + +```jsx React icon="react" +import { Link } from "@inertiajs/react"; + + + Save +; +``` + +```svelte Svelte icon="s" +import { inertia, Link } from '@inertiajs/svelte' + + + +Save +``` + + + +## Custom Headers + +The `headers` prop allows you to add custom headers to an Inertia link. However, the headers Inertia uses internally to communicate its state to the server take priority and therefore cannot be overwritten. + + + +```vue Vue icon="vuejs" +import { Link } from '@inertiajs/vue3' + +Save +``` + +```jsx React icon="react" +import { Link } from "@inertiajs/react"; + + + Save +; +``` + +```svelte Svelte icon="s" +import { inertia, Link } from '@inertiajs/svelte' + + + +Save +``` + + + +## Browser History + +The `replace` prop allows you to specify the browser's history behavior. By default, page visits push (new) state (`window.history.pushState`) into the history; however, it's also possible to replace state (`window.history.replaceState`) by setting the `replace` prop to `true`. This will cause the visit to replace the current history state instead of adding a new history state to the stack. + + + +```vue Vue icon="vuejs" +import { Link } from '@inertiajs/vue3' + +Home +``` + +```jsx React icon="react" +import { Link } from "@inertiajs/react"; + + + Home +; +``` + +```svelte Svelte icon="s" +import { inertia, Link } from '@inertiajs/svelte' + +Home + +Home +``` + + + +## State Preservation + +You can preserve a page component's local state using the `preserve-state` prop. This will prevent a page component from fully re-rendering. The `preserve-state` prop is especially helpful on pages that contain forms, since you can avoid manually repopulating input fields and can also maintain a focused input. + + + +```vue Vue icon="vuejs" +import { Link } from '@inertiajs/vue3' + + + +Search +``` + +```jsx React icon="react" +import { Link } from '@inertiajs/react' + + + +Search +``` + +```svelte Svelte icon="s" +import { inertia, Link } from '@inertiajs/svelte' + + + + + +Search +``` + + + +## Scroll Preservation + +You can use the `preserveScroll` prop to prevent Inertia from automatically resetting the scroll position when making a page visit. + + + +```vue Vue icon="vuejs" +import { Link } from '@inertiajs/vue3' + +Home +``` + +```jsx React icon="react" +import { Link } from "@inertiajs/react"; + + + Home +; +``` + +```svelte Svelte icon="s" +import { inertia, Link } from '@inertiajs/svelte' + +Home + +Home +``` + + + +For more information on managing scroll position, check out the documentation on [scroll management](/v3/advanced/scroll-management). + +## Partial Reloads + +The `only` prop allows you to specify that only a subset of a page's props (data) should be retrieved from the server on subsequent visits to that page. + + + +```vue Vue icon="vuejs" +import { Link } from '@inertiajs/vue3' + +Show active +``` + +```jsx React icon="react" +import { Link } from "@inertiajs/react"; + + + Show active +; +``` + +```svelte Svelte icon="s" +import { inertia, Link } from '@inertiajs/svelte' + +Show active + +Show active +``` + + + +For more information on this topic, check out the complete documentation on [partial reloads](/v3/data-props/partial-reloads). + +## View Transitions + +You may enable [View transitions](/v3/the-basics/view-transitions) for a link by setting the `viewTransition` prop to `true`. This will use the browser's View Transitions API to animate the page transition. + + + +```vue Vue icon="vuejs" +import { Link } from '@inertiajs/vue3' + +Navigate +``` + +```jsx React icon="react" +import { Link } from "@inertiajs/react"; + + + Navigate +; +``` + +```svelte Svelte icon="s" +import { Link } from '@inertiajs/svelte' + +Navigate +``` + + + +## Active States + +It's common to set an active state for navigation links based on the current page. This can be accomplished when using Inertia by inspecting the `page` object and doing string comparisons against the `page.url` and `page.component` properties. + + + +```vue Vue icon="vuejs" +import { Link } from '@inertiajs/vue3' // URL exact match... +Users + +// Component exact match... +Users + +// URL starts with (/users, /users/create, /users/1, etc.)... +Users + +// Component starts with (Users/Index, Users/Create, Users/Show, etc.)... +Users +``` + +```jsx React icon="react" +import { usePage } from '@inertiajs/react' + +const { url, component } = usePage() + +// URL exact match... +Users + +// Component exact match... +Users + +// URL starts with (/users, /users/create, /users/1, etc.)... +Users + +// Component starts with (Users/Index, Users/Create, Users/Show, etc.)... +Users +``` + +```svelte Svelte icon="s" +import { inertia, Link, page } from '@inertiajs/svelte' + +// URL exact match... +Users + +// Component exact match... +Users + +// URL starts with (/users, /users/create, /users/1, etc.)... +Users + +// Component starts with (Users/Index, Users/Create, Users/Show, etc.)... +Users +``` + + + +You can perform exact match comparisons (`===`), `startsWith()` comparisons (useful for matching a subset of pages), or even more complex comparisons using regular expressions. + +Using this approach, you're not limited to just setting class names. You can use this technique to conditionally render any markup on active state, such as different link text or even an SVG icon that represents the link is active. + +## Data Loading Attribute + +While a link is making an active request, a `data-loading` attribute is added to the link element. This allows you to style the link while it's in a loading state. The attribute is removed once the request is complete. diff --git a/v3/the-basics/manual-visits.mdx b/v3/the-basics/manual-visits.mdx new file mode 100644 index 0000000..a1512dd --- /dev/null +++ b/v3/the-basics/manual-visits.mdx @@ -0,0 +1,1113 @@ +--- +title: Manual Visits +--- + +In addition to [creating links](/v3/the-basics/links), it's also possible to manually make Inertia visits / requests programmatically via JavaScript. This is accomplished via the `router.visit()` method. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.visit(url, { + method: "get", + data: {}, + replace: false, + preserveState: false, + preserveScroll: false, + only: [], + except: [], + headers: {}, + errorBag: null, + forceFormData: false, + queryStringArrayFormat: "brackets", + async: false, + showProgress: true, + fresh: false, + reset: [], + preserveUrl: false, + prefetch: false, + viewTransition: false, + onCancelToken: (cancelToken) => {}, + onCancel: () => {}, + onBefore: (visit) => {}, + onStart: (visit) => {}, + onProgress: (progress) => {}, + onSuccess: (page) => {}, + onError: (errors) => {}, + onFinish: (visit) => {}, + onPrefetching: () => {}, + onPrefetched: () => {}, +}); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.visit(url, { + method: "get", + data: {}, + replace: false, + preserveState: false, + preserveScroll: false, + only: [], + except: [], + headers: {}, + errorBag: null, + forceFormData: false, + queryStringArrayFormat: "brackets", + async: false, + showProgress: true, + fresh: false, + reset: [], + preserveUrl: false, + prefetch: false, + viewTransition: false, + onCancelToken: (cancelToken) => {}, + onCancel: () => {}, + onBefore: (visit) => {}, + onStart: (visit) => {}, + onProgress: (progress) => {}, + onSuccess: (page) => {}, + onError: (errors) => {}, + onFinish: (visit) => {}, + onPrefetching: () => {}, + onPrefetched: () => {}, +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.visit(url, { + method: "get", + data: {}, + replace: false, + preserveState: false, + preserveScroll: false, + only: [], + except: [], + headers: {}, + errorBag: null, + forceFormData: false, + queryStringArrayFormat: "brackets", + async: false, + showProgress: true, + fresh: false, + reset: [], + preserveUrl: false, + prefetch: false, + viewTransition: false, + onCancelToken: (cancelToken) => {}, + onCancel: () => {}, + onBefore: (visit) => {}, + onStart: (visit) => {}, + onProgress: (progress) => {}, + onSuccess: (page) => {}, + onError: (errors) => {}, + onFinish: (visit) => {}, + onPrefetching: () => {}, + onPrefetched: () => {}, +}); +``` + + + +However, it's generally more convenient to use one of Inertia's shortcut request methods. These methods share all the same options as `router.visit()`. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.get(url, data, options); +router.post(url, data, options); +router.put(url, data, options); +router.patch(url, data, options); +router.delete(url, options); +router.reload(options); // Uses the current URL +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.get(url, data, options); +router.post(url, data, options); +router.put(url, data, options); +router.patch(url, data, options); +router.delete(url, options); +router.reload(options); // Uses the current URL +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.get(url, data, options); +router.post(url, data, options); +router.put(url, data, options); +router.patch(url, data, options); +router.delete(url, options); +router.reload(options); // Uses the current URL +``` + + + +The `reload()` method is a convenient, shorthand method that automatically visits the current page with `preserveState` and `preserveScroll` both set to `true`, making it the perfect method to invoke when you just want to reload the current page's data. + +## Method + +When making manual visits, you may use the `method` option to set the request's HTTP method to `get`, `post`, `put`, `patch` or `delete`. The default method is `get`. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.visit(url, { method: "post" }); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.visit(url, { method: "post" }); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.visit(url, { method: "post" }); +``` + + + +Uploading files via `put` or `patch` is not supported in Laravel. Instead, make the request via `post`, including a `_method` field set to `put` or `patch`. This is called [form method spoofing](https://laravel.com/docs/routing#form-method-spoofing). + +## Wayfinder + +v2.1.2+ + +When using [Wayfinder](https://github.com/laravel/wayfinder), you can pass the resulting object directly to any router method. The router will infer the HTTP method and URL from the Wayfinder object. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; +import { show } from "App/Http/Controllers/UserController"; + +router.visit(show(1)); +router.post(store()); +router.delete(destroy(1)); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; +import { show } from "App/Http/Controllers/UserController"; + +router.visit(show(1)); +router.post(store()); +router.delete(destroy(1)); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; +import { show } from "App/Http/Controllers/UserController"; + +router.visit(show(1)); +router.post(store()); +router.delete(destroy(1)); +``` + + + +If you provide both a Wayfinder object and specify the `method` option, the `method`option will take precedence. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; +import { update } from "App/Http/Controllers/UserController"; + +router.visit(update(1), { method: "patch" }); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; +import { update } from "App/Http/Controllers/UserController"; + +router.visit(update(1), { method: "patch" }); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; +import { update } from "App/Http/Controllers/UserController"; + +router.visit(update(1), { method: "patch" }); +``` + + + +## Data + +You may use the `data` option to add data to the request. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.visit("/users", { + method: "post", + data: { + name: "John Doe", + email: "john.doe@example.com", + }, +}); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.visit("/users", { + method: "post", + data: { + name: "John Doe", + email: "john.doe@example.com", + }, +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.visit("/users", { + method: "post", + data: { + name: "John Doe", + email: "john.doe@example.com", + }, +}); +``` + + + +For convenience, the `get()`, `post()`, `put()`, and `patch()`methods all accept `data` as their second argument. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.post("/users", { + name: "John Doe", + email: "john.doe@example.com", +}); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.post("/users", { + name: "John Doe", + email: "john.doe@example.com", +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.post("/users", { + name: "John Doe", + email: "john.doe@example.com", +}); +``` + + + +## Custom Headers + +The `headers` option allows you to add custom headers to a request. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.post("/users", data, { + headers: { + "Custom-Header": "value", + }, +}); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.post("/users", data, { + headers: { + "Custom-Header": "value", + }, +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.post("/users", data, { + headers: { + "Custom-Header": "value", + }, +}); +``` + + + +The headers Inertia uses internally to communicate its state to the server take priority and therefore cannot be overwritten. + +## Global Visit Options + +You may configure a `visitOptions` callback when [initializing your Inertia app](/v3/installation/client-side-setup#configuring-defaults) to modify visit options globally for every request. The callback receives the target URL and the current visit options, and should return an object with any options you want to override. + + + +```js Vue icon="vuejs" +import { createApp, h } from "vue"; +import { createInertiaApp } from "@inertiajs/vue3"; + +createInertiaApp({ + // ... + defaults: { + visitOptions: (href, options) => { + return { + headers: { + ...options.headers, + "X-Custom-Header": "value", + }, + }; + }, + }, +}); +``` + +```jsx React icon="react" +import { createInertiaApp } from "@inertiajs/react"; +import { createRoot } from "react-dom/client"; + +createInertiaApp({ + // ... + defaults: { + visitOptions: (href, options) => { + return { + headers: { + ...options.headers, + "X-Custom-Header": "value", + }, + }; + }, + }, +}); +``` + +```js Svelte icon="s" +import { createInertiaApp } from "@inertiajs/svelte"; + +createInertiaApp({ + // ... + defaults: { + visitOptions: (href, options) => { + return { + headers: { + ...options.headers, + "X-Custom-Header": "value", + }, + }; + }, + }, +}); +``` + + + +## File Uploads + +When making visits / requests that include files, Inertia will automatically convert the request data into a `FormData` object. If you would like the request to always use a `FormData` object, you may use the `forceFormData` option. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.post("/companies", data, { + forceFormData: true, +}); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.post("/companies", data, { + forceFormData: true, +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.post("/companies", data, { + forceFormData: true, +}); +``` + + + +For more information on uploading files, check out the dedicated [file uploads](/v3/the-basics/file-uploads) documentation. + +## Browser History + +When making visits, Inertia automatically adds a new entry into the browser history. However, it's also possible to replace the current history entry by setting the `replace` option to `true`. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.get("/users", { search: "John" }, { replace: true }); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.get("/users", { search: "John" }, { replace: true }); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.get("/users", { search: "John" }, { replace: true }); +``` + + + +Visits made to the same URL automatically set `replace` to `true`. + +## Client Side Visits + +You can use the `router.push` and `router.replace` method to make client-side visits. This method is useful when you want to update the browser's history without making a server request. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.push({ + url: "/users", + component: "Users", + props: { search: "John" }, + clearHistory: false, + encryptHistory: false, + preserveScroll: false, + preserveState: false, + errorBag: null, + onSuccess: (page) => {}, + onError: (errors) => {}, + onFinish: (visit) => {}, +}); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.push({ + url: "/users", + component: "Users", + props: { search: "John" }, + clearHistory: false, + encryptHistory: false, + preserveScroll: false, + preserveState: false, + errorBag: null, + onSuccess: (page) => {}, + onError: (errors) => {}, + onFinish: (visit) => {}, +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.push({ + url: "/users", + component: "Users", + props: { search: "John" }, + clearHistory: false, + encryptHistory: false, + preserveScroll: false, + preserveState: false, + errorBag: null, + onSuccess: (page) => {}, + onError: (errors) => {}, + onFinish: (visit) => {}, +}); +``` + + + +All of the parameters are optional. By default, all passed paramaters (except `errorBag`) will be merged with the current page. This means you are responsible for overriding the current page's URL, component, and props. + +If you need access to the current page's props, you may pass a function to the `props` option. This function receives the current props (including any [once props](/v3/data-props/once-props)) and should return the new props. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.push({ url: "/users", component: "Users" }); + +router.replace({ + props: (currentProps) => ({ ...currentProps, search: "John" }), +}); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.push({ url: "/users", component: "Users" }); + +router.replace({ + props: (currentProps) => ({ ...currentProps, search: "John" }), +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.push({ url: "/users", component: "Users" }); + +router.replace({ + props: (currentProps) => ({ ...currentProps, search: "John" }), +}); +``` + + + +The function also receives [once props](/v3/data-props/once-props) as a second argument. This is useful when you want to replace all regular props while still preserving once props. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.replace({ + props: (currentProps, onceProps) => ({ ...onceProps, search: "John" }), +}); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.replace({ + props: (currentProps, onceProps) => ({ ...onceProps, search: "John" }), +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.replace({ + props: (currentProps, onceProps) => ({ ...onceProps, search: "John" }), +}); +``` + + + +The `errorBag` option allows you to specify which error bag to use when handling validation errors in the `onError` callback. + +Make sure that any route you push on the client side is also defined on the server side. If the user refreshes the page, the server will need to know how to render the page. + + + Some browsers limit the number of `history.pushState()` and + `history.replaceState()` calls allowed within a short time period. Inertia + catches this error and logs it to the console, but the state update will be + lost. Avoid calling `router.push()` or `router.replace()` too frequently, and + consider debouncing or batching updates in high-frequency scenarios. + + +### Prop Helpers + +Inertia provides three helper methods for updating page props without making server requests. These methods are shortcuts to `router.replace()` and automatically set `preserveScroll` and `preserveState` to `true`. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +// Replace a prop value... +router.replaceProp("user.name", "Jane Smith"); + +// Append to an array prop... +router.appendToProp("messages", { id: 4, text: "New message" }); + +// Prepend to an array prop... +router.prependToProp("tags", "urgent"); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +// Replace a prop value... +router.replaceProp("user.name", "Jane Smith"); + +// Append to an array prop... +router.appendToProp("messages", { id: 4, text: "New message" }); + +// Prepend to an array prop... +router.prependToProp("tags", "urgent"); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +// Replace a prop value... +router.replaceProp("user.name", "Jane Smith"); + +// Append to an array prop... +router.appendToProp("messages", { id: 4, text: "New message" }); + +// Prepend to an array prop... +router.prependToProp("tags", "urgent"); +``` + + + +All three methods support dot notation for nested props and can accept a callback function that receives the current value as the first argument and the current page props as the second argument. + + + +```js Vue icon="vuejs" +import { router } from '@inertiajs/vue3' + +router.prependToProp('notifications', (current, props) => { + return { + id: Date.now(), + message: `Hello ${props.user.name}\ +``` + +```js React icon="react" +import { router } from '@inertiajs/react' + +router.prependToProp('notifications', (current, props) => { + return { + id: Date.now(), + message: `Hello ${props.user.name}\ +``` + +```js Svelte icon="s" +import { router } from '@inertiajs/svelte' + +router.prependToProp('notifications', (current, props) => { + return { + id: Date.now(), + message: `Hello ${props.user.name}\ +``` + + + +## State Preservation + +By default, page visits to the same page create a fresh page component instance. This causes any local state, such as form inputs, scroll positions, and focus states to be lost. + +However, in some situations, it's necessary to preserve the page component state. For example, when submitting a form, you need to preserve your form data in the event that form validation fails on the server. + +For this reason, the `post`, `put`, `patch`, `delete`, and `reload` methods all set the `preserveState` option to `true` by default. + +You can instruct Inertia to preserve the component's state when using the `get` method by setting the `preserveState` option to `true`. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.get("/users", { search: "John" }, { preserveState: true }); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.get("/users", { search: "John" }, { preserveState: true }); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.get("/users", { search: "John" }, { preserveState: true }); +``` + + + +If you'd like to only preserve state if the response includes validation errors, set the `preserveState` option to "errors". + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.get("/users", { search: "John" }, { preserveState: "errors" }); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.get("/users", { search: "John" }, { preserveState: "errors" }); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.get("/users", { search: "John" }, { preserveState: "errors" }); +``` + + + +You can also lazily evaluate the `preserveState` option based on the response by providing a callback. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.post("/users", data, { + preserveState: (page) => page.props.someProp === "value", +}); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.post("/users", data, { + preserveState: (page) => page.props.someProp === "value", +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.post("/users", data, { + preserveState: (page) => page.props.someProp === "value", +}); +``` + + + +## Scroll Preservation + +When navigating between pages, Inertia mimics default browser behavior by automatically resetting the scroll position of the document body (as well as any [scroll regions](/v3/advanced/scroll-management#scroll-regions) you've defined) back to the top of the page. + +You can disable this behavior by setting the `preserveScroll` option to `true`. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.visit(url, { preserveScroll: true }); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.visit(url, { preserveScroll: true }); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.visit(url, { preserveScroll: true }); +``` + + + +If you'd like to only preserve the scroll position if the response includes validation errors, set the `preserveScroll` option to "errors". + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.visit(url, { preserveScroll: "errors" }); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.visit(url, { preserveScroll: "errors" }); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.visit(url, { preserveScroll: "errors" }); +``` + + + +You can also lazily evaluate the `preserveScroll` option based on the response by providing a callback. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.post("/users", data, { + preserveScroll: (page) => page.props.someProp === "value", +}); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.post("/users", data, { + preserveScroll: (page) => page.props.someProp === "value", +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.post("/users", data, { + preserveScroll: (page) => page.props.someProp === "value", +}); +``` + + + +For more information regarding this feature, please consult the [scroll management](/v3/advanced/scroll-management) documentation. + +## Partial Reloads + +The `only` option allows you to request a subset of the props (data) from the server on subsequent visits to the same page, thus making your application more efficient since it does not need to retrieve data that the page is not interested in refreshing. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.get("/users", { search: "John" }, { only: ["users"] }); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.get("/users", { search: "John" }, { only: ["users"] }); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.get("/users", { search: "John" }, { only: ["users"] }); +``` + + + +For more information on this feature, please consult the [partial reloads](/v3/data-props/partial-reloads) documentation. + +## View Transitions + +You may enable [View transitions](/v3/the-basics/view-transitions) for a visit by setting the `viewTransition` option to `true`. This will use the browser's View Transitions API to animate the page transition. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.visit("/another-page", { viewTransition: true }); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.visit("/another-page", { viewTransition: true }); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.visit("/another-page", { viewTransition: true }); +``` + + + +## Visit Cancellation + +You can cancel a visit using a cancel token, which Inertia automatically generates and provides via the `onCancelToken()` callback prior to making the visit. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.post("/users", data, { + onCancelToken: (cancelToken) => (this.cancelToken = cancelToken), +}); + +// Cancel the visit... +this.cancelToken.cancel(); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.post("/users", data, { + onCancelToken: (cancelToken) => (this.cancelToken = cancelToken), +}); + +// Cancel the visit... +this.cancelToken.cancel(); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.post("/users", data, { + onCancelToken: (cancelToken) => (this.cancelToken = cancelToken), +}); + +// Cancel the visit... +this.cancelToken.cancel(); +``` + + + +The `onCancel()` and `onFinish()` event callbacks will be executed when a visit is cancelled. + +## Event Callbacks + +In addition to Inertia's [global events](/v3/advanced/events), Inertia also provides a number of per-visit event callbacks. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.post("/users", data, { + onBefore: (visit) => {}, + onStart: (visit) => {}, + onProgress: (progress) => {}, + onSuccess: (page) => {}, + onError: (errors) => {}, + onCancel: () => {}, + onFinish: (visit) => {}, + onPrefetching: () => {}, + onPrefetched: () => {}, +}); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.post("/users", data, { + onBefore: (visit) => {}, + onStart: (visit) => {}, + onProgress: (progress) => {}, + onSuccess: (page) => {}, + onError: (errors) => {}, + onCancel: () => {}, + onFinish: (visit) => {}, + onPrefetching: () => {}, + onPrefetched: () => {}, +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.post("/users", data, { + onBefore: (visit) => {}, + onStart: (visit) => {}, + onProgress: (progress) => {}, + onSuccess: (page) => {}, + onError: (errors) => {}, + onCancel: () => {}, + onFinish: (visit) => {}, + onPrefetching: () => {}, + onPrefetched: () => {}, +}); +``` + + + +Returning `false` from the `onBefore()` callback will cause the visit to be cancelled. + + + +```js Vue icon="vuejs" +import { router } from '@inertiajs/vue3' + +router.delete(`/users/${user.id}\ +``` + +```js React icon="react" +import { router } from '@inertiajs/react' + +router.delete(`/users/${user.id}\ +``` + +```js Svelte icon="s" +import { router } from '@inertiajs/svelte' + +router.delete(`/users/${user.id}\ +``` + + + +It's also possible to return a promise from the `onSuccess()` and `onError()` callbacks. When doing so, the "finish" event will be delayed until the promise has resolved. + + + +```js Vue icon="vuejs" +import { router } from '@inertiajs/vue3' + +router.post(url, { + onSuccess: () => { + return Promise.all([ + this.firstTask(), + this.secondTask() + ]) + } + onFinish: visit => { + // Not called until firstTask() and secondTask() have finished + }, +}) +``` + +```js React icon="react" +import { router } from '@inertiajs/react' + +router.post(url, { + onSuccess: () => { + return Promise.all([ + this.firstTask(), + this.secondTask() + ]) + } + onFinish: visit => { + // Not called until firstTask() and secondTask() have finished + }, +}) +``` + +```js Svelte icon="s" +import { router } from '@inertiajs/svelte' + +router.post(url, { + onSuccess: () => { + return Promise.all([ + this.firstTask(), + this.secondTask() + ]) + } + onFinish: visit => { + // Not called until firstTask() and secondTask() have finished + }, +}) +``` + + diff --git a/v3/the-basics/pages.mdx b/v3/the-basics/pages.mdx new file mode 100644 index 0000000..90398bc --- /dev/null +++ b/v3/the-basics/pages.mdx @@ -0,0 +1,490 @@ +--- +title: Pages +--- + +import { ClientSpecific } from "/snippets/client-specific.jsx"; +import { ReactSpecific } from "/snippets/react-specific.jsx"; +import { SvelteSpecific } from "/snippets/svelte-specific.jsx"; +import { VueSpecific } from "/snippets/vue-specific.jsx"; + +When building applications using Inertia, each page in your application typically has its own controller / route and a corresponding JavaScript component. This allows you to retrieve just the data necessary for that page - no API required. + +In addition, all of the data needed for the page can be retrieved before the page is ever rendered by the browser, eliminating the need for displaying "loading" states when users visit your application. + +## Creating Pages + +Inertia pages are simply JavaScript components. If you have ever written a Vue, React, or Svelte component, you will feel right at home. As you can see in the example below, pages receive data from your application's controllers as props. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +import Layout from "./Layout"; +import { Head } from "@inertiajs/react"; + +export default function Welcome({ user }) { + return ( + + +

    Welcome

    +

    Hello {user.name}, welcome to your first Inertia app!

    +
    + ); +} +``` + +```svelte Svelte 4 icon="s" + + + + Welcome + + + +

    Welcome

    +

    Hello {user.name}, welcome to your first Inertia app!

    +
    +``` + +```svelte Svelte 5 icon="s" + + + + Welcome + + + +

    Welcome

    +

    Hello {user.name}, welcome to your first Inertia app!

    +
    +``` + +
    + + + Given the page above, you can render the page by returning an [Inertia + response](/v3/the-basics/responses) from a controller or route. In this + example, let's assume this page is stored at{" "} + `resources/js/Pages/User/Show.vue` + `resources/js/Pages/User/Show.jsx` + `resources/js/Pages/User/Show.svelte`within a + Laravel application. + + +```php +use Inertia\Inertia; + +class UserController extends Controller +{ + public function show(User $user) + { + return Inertia::render('User/Show', [ + 'user' => $user + ]); + } +} +``` + +If you attempt to render a page that does not exist, the response will typically be a blank screen. To prevent this, you may set the `inertia.ensure_pages_exist` configuration option to `true`. The Laravel adapter will then throw an `Inertia\ComponentNotFoundException` when a page cannot be found. + +## Creating Layouts + +While not required, for most projects it makes sense to create a layout component that all of your pages can use. You may have noticed in our page example above that we're wrapping the page content within a `` component. Here's an example of such a component: + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +import { Link } from "@inertiajs/react"; + +export default function Layout({ children }) { + return ( +
    +
    + Home + About + Contact +
    +
    {children}
    +
    + ); +} +``` + +```svelte Svelte 4 icon="s" + + +
    +
    + Home + About + Contact +
    +
    + +
    +
    +``` + +```svelte Svelte 5 icon="s" + + +
    +
    + Home + About + Contact +
    +
    + {@render children()} +
    +
    +``` + +
    + + + As you can see, there is nothing Inertia specific within this template. This + is just a typical Vue + React + Svelte component. + + +## Persistent Layouts + +While it's simple to implement layouts as children of page components, it forces the layout instance to be destroyed and recreated between visits. This means you cannot have persistent layout state when navigating between pages. + +For example, maybe you have an audio player on a podcast website that you want to continue playing as users navigate the site. Or, maybe you simply want to maintain the scroll position in your sidebar navigation between page visits. In these situations, the solution is to leverage Inertia's persistent layouts. + + + +```vue Vue icon="vuejs" + + + + + +``` + +```jsx React icon="react" +import Layout from './Layout' + +const Home = ({ user }) => { + return ( + +

    Welcome

    +

    Hello {user.name}, welcome to your first Inertia app!

    + + ) +} + +Home.layout = page => + +export default Home +``` + +```svelte Svelte 4 icon="s" + + + + +

    Welcome

    +

    Hello {user.name}, welcome to your first Inertia app!

    +``` + +```svelte Svelte 5 icon="s" + + + + +

    Welcome

    +

    Hello {user.name}, welcome to your first Inertia app!

    +``` + +
    + +You can also create more complex layout arrangements using nested layouts. + + + +```vue Vue icon="vuejs" + + + + + +``` + +```jsx React icon="react" +import SiteLayout from './SiteLayout' +import NestedLayout from './NestedLayout' + +const Home = ({ user }) => { + return ( + +

    Welcome

    +

    Hello {user.name}, welcome to your first Inertia app!

    + + ) +} + +Home.layout = page => ( + + + +) + +export default Home +``` + +```svelte Svelte 4 icon="s" + + + + +

    Welcome

    +

    Hello {user.name}, welcome to your first Inertia app!

    +``` + +```svelte Svelte 5 icon="s" + + + + +

    Welcome

    +

    Hello {user.name}, welcome to your first Inertia app!

    +``` + +
    + + + +If you're using Vue 3.3+, you can alternatively use [defineOptions](https://vuejs.org/api/sfc-script-setup.html#defineoptions) to define a layout within ` +``` + + + +## Default Layouts + +If you're using persistent layouts, you may find it convenient to define the default page layout in the `resolve()` callback of your application's main JavaScript file. + + + +```js Vue icon="vuejs" +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; + }, + // ... +}); +``` + +```js React icon="react" +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) => ); + return page; + }, + // ... +}); +``` + +```js Svelte icon="s" +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 }; + }, + // ... +}); +``` + + + +This will automatically set the page layout to `Layout` if a layout has not already been set for that page. + +You can even go a step further and conditionally set the default page layout based on the page `name`, which is available to the `resolve()` callback. For example, maybe you don't want the default layout to be applied to your public pages. + + + +```js Vue icon="vuejs" +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 = name.startsWith("Public/") ? undefined : Layout; + return page; + }, + // ... +}); +``` + +```js React icon="react" +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 = name.startsWith("Public/") + ? undefined + : (page) => ; + return page; + }, + // ... +}); +``` + +```js Svelte icon="s" +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: name.startsWith("Public/") ? undefined : Layout, + }; + }, + // ... +}); +``` + + diff --git a/v3/the-basics/redirects.mdx b/v3/the-basics/redirects.mdx new file mode 100644 index 0000000..e10f67c --- /dev/null +++ b/v3/the-basics/redirects.mdx @@ -0,0 +1,47 @@ +--- +title: Redirects +--- + +When making a non-GET Inertia request manually or via a `` element, you should ensure that you always respond with a proper Inertia redirect response. + +For example, if your controller is creating a new user, your "store" endpoint should return a redirect back to a standard `GET` endpoint, such as your user "index" page. Inertia will automatically follow this redirect and update the page accordingly. + +```php +class UsersController extends Controller +{ + public function index() + { + return Inertia::render('Users/Index', [ + 'users' => User::all(), + ]); + } + + public function store(Request $request) + { + User::create( + $request->validate([ + 'name' => ['required', 'max:50'], + 'email' => ['required', 'max:50', 'email'], + ]) + ); + + return to_route('users.index'); + } +} +``` + +## 303 Response Code + +When redirecting after a `PUT`, `PATCH`, or `DELETE` request, you must use a `303` response code, otherwise the subsequent request will not be treated as a `GET`request. A `303` redirect is very similar to a `302` redirect; however, the follow-up request is explicitly changed to a `GET` request. + +If you're using one of our official server-side adapters, all redirects will automatically be converted to `303` redirects. + +## External Redirects + +Sometimes it's necessary to redirect to an external website, or even another non-Inertia endpoint in your app while handling an Inertia request. This can be accomplished using a server-side initiated `window.location` visit via the `Inertia::location()` method. + +```php +return Inertia::location($url); +``` + +The `Inertia::location()` method will generate a `409 Conflict` response and include the destination URL in the `X-Inertia-Location` header. When this response is received client-side, Inertia will automatically perform a `window.location = url` visit. diff --git a/v3/the-basics/responses.mdx b/v3/the-basics/responses.mdx new file mode 100644 index 0000000..1848fbe --- /dev/null +++ b/v3/the-basics/responses.mdx @@ -0,0 +1,239 @@ +--- +title: Responses +--- + +## Creating Responses + +Creating an Inertia response is simple. To get started, invoke the `Inertia::render()` method within your controller or route, providing both the name of the [JavaScript page component](/v3/the-basics/pages) that you wish to render, as well as any properties (data) for the page. + +In the example below, we will pass a single property (`event`) which contains four attributes (`id`, `title`, `start_date` and `description`) to the `Event/Show` page component. + +```php +use Inertia\Inertia; + +class EventsController extends Controller +{ + public function show(Event $event) + { + return Inertia::render('Event/Show', [ + 'event' => $event->only( + 'id', + 'title', + 'start_date', + 'description' + ), + ]); + + // Alternatively, you can use the inertia() helper... + return inertia('Event/Show', [ + 'event' => $event->only( + 'id', + 'title', + 'start_date', + 'description' + ), + ]); + } +} +``` + + + To ensure that pages load quickly, only return the minimum data required for + the page. + + + + Be aware that all data returned from the controllers will be visible + client-side, so be sure to omit sensitive information. + + +## Properties + +To pass data from the server to your page components, you can use properties. You can pass various types of values as props, including primitive types, arrays, objects, and several Laravel-specific types that are automatically resolved: + +```php +use App\Models\User; +use Illuminate\Http\Resources\Json\JsonResource; + +Inertia::render('Dashboard', [ + // Primitive values + 'title' => 'Dashboard', + 'count' => 42, + 'active' => true, + + // Arrays and objects + 'settings' => ['theme' => 'dark', 'notifications' => true], + + // Arrayable objects (Collections, Models, etc.) + 'user' => auth()->user(), // Eloquent model + 'users' => User::all(), // Eloquent collection + + // API Resources + 'profile' => new UserResource(auth()->user()), + + // Responsable objects + 'data' => new JsonResponse(['key' => 'value']), + + // Closures + 'timestamp' => fn() => now()->timestamp, +]); +``` + +Arrayable objects like Eloquent models and collections are automatically converted using their `toArray()` method. Responsable objects like API resources and JSON responses are resolved through their `toResponse()` method. + +## `ProvidesInertiaProperty` Interface + +When passing props to your components, you may want to create custom classes that can transform themselves into the appropriate data format. While Laravel's `Arrayable` interface simply converts objects to arrays, Inertia offers the more powerful `ProvidesInertiaProperty` interface for context-aware transformations. + +This interface requires a `toInertiaProperty` method that receives a `PropertyContext`object containing the property key (`$context->key`), all props for the page (`$context->props`), and the request instance (`$context->request`). + +```php +use Inertia\PropertyContext; +use Inertia\ProvidesInertiaProperty; + +class UserAvatar implements ProvidesInertiaProperty +{ + public function __construct(protected User $user, protected int $size = 64) + { + // + } + + public function toInertiaProperty(PropertyContext $context): mixed + { + return $this->user->avatar + ? Storage::url($this->user->avatar) + : "https://ui-avatars.com/api/?name={$this->user->name}&size={$this->size}"; + } +} +``` + +Once defined, you can use this class directly as a prop value. + +```php +Inertia::render('Profile', [ + 'user' => $user, + 'avatar' => new UserAvatar($user, 128), +]); +``` + +The `PropertyContext` gives you access to the property key, which enables powerful patterns like merging with shared data. + +```php +use Inertia\Inertia; +use Inertia\PropertyContext; +use Inertia\ProvidesInertiaProperty; + +class MergeWithShared implements ProvidesInertiaProperty +{ + public function __construct(protected array $items = []) + { + // + } + + public function toInertiaProperty(PropertyContext $context): mixed + { + // Access the property key to get shared data + $shared = Inertia::getShared($context->key, []); + + // Merge with the new items + return array_merge($shared, $this->items); + } +} + +// Usage +Inertia::share('notifications', ['Welcome back!']); + +return Inertia::render('Dashboard', [ + 'notifications' => new MergeWithShared(['New message received']), + // Result: ['Welcome back!', 'New message received'] +]); +``` + +## `ProvidesInertiaProperties` Interface + +In some situations you may want to group related props together for reusability across different pages. You can accomplish this by implementing the `ProvidesInertiaProperties` interface. + +This interface requires a `toInertiaProperties` method that returns an array of key-value pairs. The method receives a `RenderContext` object containing the component name (`$context->component`) and request instance (`$context->request`). + +```php +use App\Models\User; +use Illuminate\Container\Attributes\CurrentUser; +use Inertia\RenderContext; +use Inertia\ProvidesInertiaProperties; + +class UserPermissions implements ProvidesInertiaProperties +{ + public function __construct(#[CurrentUser] protected User $user) + { + // + } + + public function toInertiaProperties(RenderContext $context): array + { + return [ + 'canEdit' => $this->user->can('edit'), + 'canDelete' => $this->user->can('delete'), + 'canPublish' => $this->user->can('publish'), + 'isAdmin' => $this->user->hasRole('admin'), + ]; + } +} +``` + +You can use these prop classes directly in the `render()` and `with()` methods. + +```php +public function index(UserPermissions $permissions) +{ + return Inertia::render('UserProfile', $permissions); + + // or... + + return Inertia::render('UserProfile')->with($permissions); +} +``` + +You can also combine multiple prop classes with other props in an array: + +```php +public function index(UserPermissions $permissions) +{ + return Inertia::render('UserProfile', [ + 'user' => auth()->user(), + $permissions, + ]); + + // or using method chaining... + + return Inertia::render('UserProfile') + ->with('user', auth()->user()) + ->with($permissions); +} +``` + +## Root Template Data + +There are situations where you may want to access your prop data in your application's root Blade template. For example, you may want to add a meta description tag, Twitter card meta tags, or Facebook Open Graph meta tags. You can access this data via the `$page` variable. + +```blade + +``` + +Sometimes you may even want to provide data to the root template that will not be sent to your JavaScript page component. This can be accomplished by invoking the `withViewData` method. + +```php +return Inertia::render('Event', ['event' => $event]) + ->withViewData(['meta' => $event->meta]); +``` + +After invoking the `withViewData` method, you can access the defined data as you would typically access a Blade template variable. + +```blade + +``` + +## Maximum Response Size + +To enable client-side history navigation, all Inertia server responses are stored in the browser's history state. However, keep in mind that some browsers impose a size limit on how much data can be saved within the history state. + +For example, [Firefox](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState) has a size limit of 16 MiB and throws a `NS_ERROR_ILLEGAL_VALUE` error if you exceed this limit. Typically, this is much more data than you'll ever practically need when building applications. diff --git a/v3/the-basics/routing.mdx b/v3/the-basics/routing.mdx new file mode 100644 index 0000000..eccc0df --- /dev/null +++ b/v3/the-basics/routing.mdx @@ -0,0 +1,88 @@ +--- +title: Routing +--- + +## Defining Routes + +When using Inertia, all of your application's routes are defined server-side. This means that you don't need Vue Router or React Router. Instead, you can simply define Laravel routes and return [Inertia responses](/v3/the-basics/responses) from those routes. + +## Shorthand Routes + +If you have a [page](/v3/the-basics/pages) that doesn't need a corresponding controller method, like an "FAQ" or "about" page, you can route directly to a component via the `Route::inertia()` method. + +```php +Route::inertia('/about', 'About'); +``` + +## Generating URLs + +Some server-side frameworks allow you to generate URLs from named routes. However, you will not have access to those helpers client-side. Here are a couple ways to still use named routes with Inertia. + +The first option is to generate URLs server-side and include them as props. Notice in this example how we're passing the `edit_url` and `create_url` to the `Users/Index` component. + +```php +class UsersController extends Controller +{ + public function index() + { + return Inertia::render('Users/Index', [ + 'users' => User::all()->map(fn ($user) => [ + 'id' => $user->id, + 'name' => $user->name, + 'email' => $user->email, + 'edit_url' => route('users.edit', $user), + ]), + 'create_url' => route('users.create'), + ]); + } +} +``` + +When using Laravel, you have several options to make your server-side routes available to your client-side code: + +### Wayfinder + +When using [Wayfinder](https://github.com/laravel/wayfinder), you can pass the generated TypeScript method directly to the [Link component](/v3/the-basics/links#wayfinder), [form helpers](/v3/the-basics/forms#wayfinder), or [router methods](/v3/the-basics/manual-visits#wayfinder) and Inertia understand how to handle it. In fact, if you are developing an application using one of Laravel's [starter kits](https://laravel.com/docs/starter-kits), Wayfinder is already configured for you. + +### Ziggy + +The [Ziggy](https://github.com/tighten/ziggy) library can make your named, server-side routes available to you via a global `route()` function. If you're using the Vue plugin included with Ziggy, you may use the `route()` function directly in your templates. + +```vue +Create User +``` + +When [server-side rendering](/v3/advanced/server-side-rendering) is enabled, you may pass an options object to the Ziggy plugin in your `ssr.js` file. This should include the route definitions and current location. + +```js +.use(ZiggyVue, { + ...page.props.ziggy, + location: new URL(page.props.ziggy.location), +}); +``` + +## Customizing the Page URL + +The [page object](/v3/core-concepts/the-protocol#the-page-object) includes a `url` that represents the current page's URL. By default, the Laravel adapter resolves this using the `fullUrl()` method on the `Request` instance, but strips the scheme and host so the result is a relative URL. + +If you need to customize how the URL is resolved, you may provide a resolver within the `urlResolver`method of the Inertia `HandleInertiaRequests` middleware. + +```php +class HandleInertiaRequests extends Middleware +{ + public function urlResolver() + { + return function (Request $request) { + // Return the URL for the request... + }; + } +} +``` + +Alternatively, you may define the resolver using the `Inertia::resolveUrlUsing()` method. + +```php +Inertia::resolveUrlUsing(function (Request $request) { + // Return the URL for the request... +}); +``` diff --git a/v3/the-basics/title-and-meta.mdx b/v3/the-basics/title-and-meta.mdx new file mode 100644 index 0000000..1315e73 --- /dev/null +++ b/v3/the-basics/title-and-meta.mdx @@ -0,0 +1,285 @@ +--- +title: Title & Meta +--- + +Since Inertia powered JavaScript apps are rendered within the document ``, they are unable to render markup to the document ``, as it's outside of their scope. To help with this, Inertia ships with a `` component which can be used to set the page ``, `<meta>` tags, and other `<head>` elements. + +The `<Head>` component will only replace `<head>` elements that are not in your server-side root template. + +The `<Head>` component is not available in the Svelte adapter, as Svelte already ships with its own `<svelte:head>` component. + +## Head Component + +To add `<head>` elements to your page, use the `<Head>` component. Within this component, you can include the elements that you wish to add to the document `<head>`. + +<CodeGroup> + +```vue Vue icon="vuejs" +import { Head } from '@inertiajs/vue3' + +<Head> + <title>Your page title + + +``` + +```jsx React icon="react" +import { Head } from "@inertiajs/react"; + + + Your page title + +; +``` + +```svelte Svelte icon="s" + + Your page title + + +``` + + + +## Title Shorthand + +If you only need to add a `` to the document `<head>`, you may simply pass the title as a prop to the `<Head>` component. + +<CodeGroup> + +```vue Vue icon="vuejs" +import { Head } from '@inertiajs/vue3' + +<Head title="Your page title" /> +``` + +```jsx React icon="react" +import { Head } from "@inertiajs/react"; + +<Head title="Your page title" />; +``` + +```js Svelte icon="s" +// Not supported +``` + +</CodeGroup> + +## Title Callback + +You can globally modify the page `<title>` using the `title` callback in the `createInertiaApp` setup method. Typically, this method is invoked in your application's main JavaScript file. A common use case for the title callback is automatically adding an app name before or after each page title. + +```js +createInertiaApp({ + title: (title) => `${title} - My App`, + // ... +}); +``` + +After defining the `title` callback, the callback will automatically be invoked when you set a title using the `<Head>` component. + +<CodeGroup> + +```vue Vue icon="vuejs" +import { Head } from '@inertiajs/vue3' + +<Head title="Home"> +``` + +```jsx React icon="react" +import { Head } from '@inertiajs/react' + +<Head title="Home"> +``` + +```js Svelte icon="s" +// Not supported +``` + +</CodeGroup> + +Which, in this example, will result in the following `<title>` tag. + +```html +<title>Home - My App +``` + +The `title` callback will also be invoked when you set the title using a `` tag within your `<Head>` component. + +<CodeGroup> + +```vue Vue icon="vuejs" +import { Head } from '@inertiajs/vue3' + +<Head> + <title>Home + +``` + +```jsx React icon="react" +import { Head } from "@inertiajs/react"; + + + Home +; +``` + +```js Svelte icon="s" +// Not supported +``` + + + +## Multiple Head Instances + +It's possible to have multiple instances of the `` component throughout your application. For example, your layout can set some default `` elements, and then your individual pages can override those defaults. + + + +```vue Vue icon="vuejs" +// Layout.vue import { Head } from '@inertiajs/vue3' + + + My app + + + + +// About.vue import { Head } from '@inertiajs/vue3' + + + About - My app + + +``` + +```jsx React icon="react" +// Layout.js + +import { Head } from "@inertiajs/react"; + + + My app + + +; + +// About.js + +import { Head } from "@inertiajs/react"; + + + About - My app + +; +``` + +```js Svelte icon="s" +// Not supported +``` + + + +Inertia will only ever render one `` tag; however, all other tags will be stacked since it's valid to have multiple instances of them. To avoid duplicate tags in your `<head>`, you can use the `head-key` property, which will make sure the tag is only rendered once. This is illustrated in the example above for the `<meta name="description">` tag. + +The code example above will render the following HTML. + +```html +<head> + <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> + <title>About - My app + + +``` + +## Head Extension + +When building a real application, it can sometimes be helpful to create a custom head component that extends Inertia's `` component. This gives you a place to set app-wide defaults, such as appending the app name to the page title. + + + +```vue Vue icon="vuejs" + + + + + +``` + +```jsx React icon="react" +// AppHead.js + +import { Head } from "@inertiajs/react"; + +const Site = ({ title, children }) => { + return ( + + {title ? `${title} - My App` : "My App"} + {children} + + ); +}; + +export default Site; +``` + +```js Svelte icon="s" +// Not supported +``` + + + +Once you have created the custom component, you can just start using it in your pages. + + + +```vue Vue icon="vuejs" +import AppHead from './AppHead' + + +``` + +```jsx React icon="react" +import AppHead from './AppHead' + + +``` + +```js Svelte icon="s" +// Not supported +``` + + + +## Inertia Attribute on Elements + +Inertia has historically used the `inertia` attribute to track and manage elements in the document ``. However, you can now opt-in to using the more standards-compliant `data-inertia` attribute instead. According to the HTML specification, custom attributes should be prefixed with `data-` to avoid conflicts with future HTML standards. + +To enable this, configure the `future.useDataInertiaHeadAttribute` option in your [application defaults](/v3/installation/client-side-setup#configuring-defaults). + +```js +createInertiaApp({ + // resolve, setup, etc. + defaults: { + future: { + useDataInertiaHeadAttribute: true, + }, + }, +}); +``` diff --git a/v3/the-basics/validation.mdx b/v3/the-basics/validation.mdx new file mode 100644 index 0000000..906a708 --- /dev/null +++ b/v3/the-basics/validation.mdx @@ -0,0 +1,236 @@ +--- +title: Validation +--- + +## How It Works + +Handling server-side validation errors in Inertia works differently than a classic XHR-driven form that requires you to catch the validation errors from `422` responses and manually update the form's error state - because Inertia never receives `422` responses. Instead, Inertia operates much more like a standard full page form submission. Here's how: + +First, you [submit your form using Inertia](/v3/the-basics/forms). If there are server-side validation errors, you don't return those errors as a `422` JSON response. Instead, you redirect (server-side) the user back to the form page they were previously on, flashing the validation errors in the session. Some frameworks, such as Laravel, do this automatically. + +Next, any time these validation errors are present in the session, they automatically get shared with Inertia, making them available client-side as page props which you can display in your form. Since props are reactive, they are automatically shown when the form submission completes. + +Finally, since Inertia apps never generate `422` responses, Inertia needs another way to determine if a response includes validation errors. To do this, Inertia checks the `page.props.errors` object for the existence of any errors. In the event that errors are present, the request's `onError()` callback will be called instead of the `onSuccess()` callback. + +## Sharing Errors + +In order for your server-side validation errors to be available client-side, your server-side framework must share them via the `errors` prop. Inertia's first-party adapters, such as the Laravel adapter, do this automatically. For other frameworks, you may need to do this manually. Please refer to your specific server-side adapter documentation for more information. + +## Displaying Errors + +Since validation errors are made available client-side as page component props, you can conditionally display them based on their existence. Remember, when using our first-party server adapters (such as the Laravel adapter), the `errors` prop will automatically be available to your page. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +import { useState } from "react"; +import { router, usePage } from "@inertiajs/react"; + +export default function Edit() { + const { errors } = usePage().props; + + const [values, setValues] = useState({ + first_name: null, + last_name: null, + email: null, + }); + + function handleChange(e) { + setValues((values) => ({ + ...values, + [e.target.id]: e.target.value, + })); + } + + function handleSubmit(e) { + e.preventDefault(); + router.post("/users", values); + } + + return ( + + + + {errors.first_name &&
    {errors.first_name}
    } + + + {errors.last_name &&
    {errors.last_name}
    } + + + {errors.email &&
    {errors.email}
    } + + + ); +} +``` + +```svelte Svelte icon="s" + + +
    + + + {#if errors.first_name}
    {errors.first_name}
    {/if} + + + + {#if errors.last_name}
    {errors.last_name}
    {/if} + + + + {#if errors.email}
    {errors.email}
    {/if} + + +
    +``` + +
    + +When using the Vue adapters, you may also access the errors via the `$page.props.errors` object. + +### Multiple Errors Per Field + +By default, Inertia's Laravel adapter returns only the first validation error for each field. You may opt-in to receiving all errors by setting the `$withAllErrors` property to `true` in your middleware. + +```php +class HandleInertiaRequests extends Middleware +{ + protected $withAllErrors = true; + + // ... +} +``` + +When enabled, each field will contain an array of error strings instead of a single string. + + + +```vue Vue icon="vuejs" +

    {{ error }}

    +``` + +```jsx React icon="react" +{ + errors.email.map((error, index) =>

    {error}

    ); +} +``` + +```svelte Svelte icon="s" +{#each errors.email as error} +

    {error}

    +{/each} +``` + +
    + +You may [configure TypeScript](/v3/advanced/typescript#error-values) to expect arrays instead of strings. + +## Repopulating Input + +While handling errors in Inertia is similar to full page form submissions, Inertia offers even more benefits. In fact, you don't even need to manually repopulate old form input data. + +When validation errors occur, the user is typically redirected back to the form page they were previously on. And, by default, Inertia automatically preserves the [component state](/v3/the-basics/manual-visits#state-preservation) for `post`, `put`, `patch`, and `delete` requests. Therefore, all the old form input data remains exactly as it was when the user submitted the form. + +So, the only work remaining is to display any validation errors using the `errors` prop. + +## Error Bags + +If you're using the [form helper](/v3/the-basics/forms#form-helper), it's not necessary to use error bags since validation errors are automatically scoped to the form object making the request. + +For pages that have more than one form, it's possible to encounter conflicts when displaying validation errors if two forms share the same field names. For example, imagine a "create company" form and a "create user" form that both have a `name` field. Since both forms will be displaying the `page.props.errors.name` validation error, generating a validation error for the `name`field in either form will cause the error to appear in both forms. + +To solve this issue, you can use "error bags". Error bags scope the validation errors returned from the server within a unique key specific to that form. Continuing with our example above, you might have a `createCompany` error bag for the first form and a `createUser` error bag for the second form. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.post("/companies", data, { + errorBag: "createCompany", +}); + +router.post("/users", data, { + errorBag: "createUser", +}); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.post("/companies", data, { + errorBag: "createCompany", +}); + +router.post("/users", data, { + errorBag: "createUser", +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.post("/companies", data, { + errorBag: "createCompany", +}); + +router.post("/users", data, { + errorBag: "createUser", +}); +``` + + + +Specifying an error bag will cause the validation errors to come back from the server within `page.props.errors.createCompany` and `page.props.errors.createUser`. diff --git a/v3/the-basics/view-transitions.mdx b/v3/the-basics/view-transitions.mdx new file mode 100644 index 0000000..e2f9b7d --- /dev/null +++ b/v3/the-basics/view-transitions.mdx @@ -0,0 +1,320 @@ +--- +title: View Transitions +--- + +v2.2.13+ + +Inertia supports the [View Transitions API](https://developer.chrome.com/docs/web-platform/view-transitions), allowing you to animate page transitions. + +The View Transitions API is a [relatively new browser feature](https://caniuse.com/view-transitions). Inertia gracefully falls back to standard page transitions in browsers that don't support the API. + +## Enabling Transitions + +You may enable view transitions for a visit by setting the `viewTransition` option to `true`. By default, this will apply a cross-fade transition between pages. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.visit("/another-page", { viewTransition: true }); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.visit("/another-page", { viewTransition: true }); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.visit("/another-page", { viewTransition: true }); +``` + + + +## Transition Callbacks + +You may also pass a callback to the `viewTransition` option, which will receive the standard [`ViewTransition`](https://developer.mozilla.org/en-US/docs/Web/API/ViewTransition) instance provided by the browser. This allows you to hook into the various promises provided by the API. + + + +```js Vue icon="vuejs" +import { router } from "@inertiajs/vue3"; + +router.visit("/another-page", { + viewTransition: (transition) => { + transition.ready.then(() => console.log("Transition ready")); + transition.updateCallbackDone.then(() => console.log("DOM updated")); + transition.finished.then(() => console.log("Transition finished")); + }, +}); +``` + +```js React icon="react" +import { router } from "@inertiajs/react"; + +router.visit("/another-page", { + viewTransition: (transition) => { + transition.ready.then(() => console.log("Transition ready")); + transition.updateCallbackDone.then(() => console.log("DOM updated")); + transition.finished.then(() => console.log("Transition finished")); + }, +}); +``` + +```js Svelte icon="s" +import { router } from "@inertiajs/svelte"; + +router.visit("/another-page", { + viewTransition: (transition) => { + transition.ready.then(() => console.log("Transition ready")); + transition.updateCallbackDone.then(() => console.log("DOM updated")); + transition.finished.then(() => console.log("Transition finished")); + }, +}); +``` + + + +## Links + +The `viewTransition` option is also available on the `Link` component. + + + +```vue Vue icon="vuejs" +import { Link } from '@inertiajs/vue3' + +Navigate +``` + +```jsx React icon="react" +import { Link } from "@inertiajs/react"; + + + Navigate +; +``` + +```svelte Svelte icon="s" +import { Link } from '@inertiajs/svelte' + +Navigate +``` + + + +You may also pass a callback to access the `ViewTransition` instance. + + + +```vue Vue icon="vuejs" +import { Link } from '@inertiajs/vue3' + + + Navigate + +``` + +```jsx React icon="react" +import { Link } from "@inertiajs/react"; + + + transition.finished.then(() => console.log("Done")) + } +> + Navigate +; +``` + +```svelte Svelte icon="s" +import { Link } from '@inertiajs/svelte' + + transition.finished.then(() => console.log('Done'))} +> + Navigate + +``` + + + +## Global Configuration + +You may enable view transitions globally for all visits by configuring the `visitOptions` callback when [initializing your Inertia app](/v3/installation/client-side-setup#configuring-defaults). + + + +```js Vue icon="vuejs" +import { createInertiaApp } from "@inertiajs/vue3"; + +createInertiaApp({ + // ... + defaults: { + visitOptions: (href, options) => { + return { viewTransition: true }; + }, + }, +}); +``` + +```jsx React icon="react" +import { createInertiaApp } from "@inertiajs/react"; + +createInertiaApp({ + // ... + defaults: { + visitOptions: (href, options) => { + return { viewTransition: true }; + }, + }, +}); +``` + +```js Svelte icon="s" +import { createInertiaApp } from "@inertiajs/svelte"; + +createInertiaApp({ + // ... + defaults: { + visitOptions: (href, options) => { + return { viewTransition: true }; + }, + }, +}); +``` + + + +## Customizing Transitions + +You may customize the transition animations using CSS. The View Transitions API uses several pseudo-elements that you can target with CSS to create custom animations. The following examples are taken from the [Chrome documentation ](https://developer.chrome.com/docs/web-platform/view-transitions/same-document#customize_the_transition). + +```css +@keyframes fade-in { + from { + opacity: 0; + } +} +@keyframes fade-out { + to { + opacity: 0; + } +} +@keyframes slide-from-right { + from { + transform: translateX(30px); + } +} +@keyframes slide-to-left { + to { + transform: translateX(-30px); + } +} +::view-transition-old(root) { + animation: + 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out, + 300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left; +} +::view-transition-new(root) { + animation: + 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in, + 300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right; +} +``` + +You may also animate individual elements between pages by assigning them a unique `view-transition-name`. For example, you may animate an avatar from a large size on a profile page to a small size on a dashboard. + + + +```vue Vue icon="vuejs" + + + + + + + + + +``` + +```jsx React icon="react" +// Profile.jsx +export default function Profile() { + return User +} + +// CSS +.avatar-large { + view-transition-name: user-avatar; + width: auto; + height: 200px; +} + +// Dashboard.jsx +export default function Dashboard() { + return User +} + +// CSS +.avatar-small { + view-transition-name: user-avatar; + width: auto; + height: 40px; +} +``` + +```svelte Svelte icon="s" + +User + + + + +User + + +``` + + + +You may customize view transitions to your liking using any CSS animations you wish. For more information, please consult the [View Transitions API documentation ](https://developer.chrome.com/docs/web-platform/view-transitions/same-document#customize_the_transition). From 10602bd11d738b734500bb5215a27f267897a840 Mon Sep 17 00:00:00 2001 From: Pascal Baljet Date: Wed, 11 Feb 2026 15:02:25 +0100 Subject: [PATCH 02/27] wip --- docs.json | 19 + v3/advanced/error-handling.mdx | 117 ++--- v3/advanced/events.mdx | 74 ++- v3/advanced/server-side-rendering.mdx | 402 ++++++++++------ v3/advanced/typescript.mdx | 105 ++++ v3/data-props/deferred-props.mdx | 87 ++-- v3/data-props/flash-data.mdx | 4 +- v3/data-props/load-when-visible.mdx | 157 ++---- v3/data-props/partial-reloads.mdx | 35 ++ v3/getting-started/index.mdx | 11 + v3/getting-started/upgrade-guide.mdx | 268 ++++++++--- v3/installation/client-side-setup.mdx | 279 +++++++---- v3/the-basics/file-uploads.mdx | 28 +- v3/the-basics/forms.mdx | 269 +---------- v3/the-basics/http-requests.mdx | 509 ++++++++++++++++++++ v3/the-basics/layouts.mdx | 659 ++++++++++++++++++++++++++ v3/the-basics/links.mdx | 2 - v3/the-basics/manual-visits.mdx | 51 +- v3/the-basics/optimistic-updates.mdx | 302 ++++++++++++ v3/the-basics/pages.mdx | 397 +--------------- v3/the-basics/responses.mdx | 16 +- v3/the-basics/view-transitions.mdx | 2 - 22 files changed, 2533 insertions(+), 1260 deletions(-) create mode 100644 v3/the-basics/http-requests.mdx create mode 100644 v3/the-basics/layouts.mdx create mode 100644 v3/the-basics/optimistic-updates.mdx diff --git a/docs.json b/docs.json index 82b8e23..6ad953b 100644 --- a/docs.json +++ b/docs.json @@ -192,8 +192,11 @@ "v3/the-basics/links", "v3/the-basics/manual-visits", "v3/the-basics/forms", + "v3/the-basics/http-requests", + "v3/the-basics/optimistic-updates", "v3/the-basics/file-uploads", "v3/the-basics/validation", + "v3/the-basics/layouts", "v3/the-basics/view-transitions" ] }, @@ -340,6 +343,22 @@ "source": "/view-transitions", "destination": "/v3/the-basics/view-transitions" }, + { + "source": "/http-requests", + "destination": "/v3/the-basics/http-requests" + }, + { + "source": "/optimistic-updates", + "destination": "/v3/the-basics/optimistic-updates" + }, + { + "source": "/layout-props", + "destination": "/v3/the-basics/layouts" + }, + { + "source": "/v3/the-basics/layout-props", + "destination": "/v3/the-basics/layouts" + }, { "source": "/shared-data", "destination": "/v3/data-props/shared-data" diff --git a/v3/advanced/error-handling.mdx b/v3/advanced/error-handling.mdx index 421fb77..095dfe8 100644 --- a/v3/advanced/error-handling.mdx +++ b/v3/advanced/error-handling.mdx @@ -10,54 +10,42 @@ The challenge is, if you're making an XHR request (which Inertia does) and you h Inertia solves this issue by showing all non-Inertia responses in a modal. This means you get the same beautiful error-reporting you're accustomed to, even though you've made that request over XHR. -## Dialog Element - -By default, Inertia displays error modals using a custom `
    ` overlay. However, you can opt-in to using the native HTML `` element instead, which provides built-in modal functionality including backdrop handling. - -To enable this, configure the `future.useDialogForErrorModal` option in your [application defaults](/v3/installation/client-side-setup#configuring-defaults). - -```js -createInertiaApp({ - // resolve, setup, etc. - defaults: { - future: { - useDialogForErrorModal: true, - }, - }, -}); -``` - ## Production -In production you will want to return a proper Inertia error response instead of relying on the modal-driven error reporting that is present during development. To accomplish this, you'll need to update your framework's default exception handler to return a custom error page. - -When building Laravel applications, you can accomplish this by using the `respond` exception method in your application's `bootstrap/app.php` file. +In production you will want to return a proper Inertia error response instead of relying on the modal-driven error reporting that is present during development. To accomplish this, you may use the `Inertia::handleExceptionsUsing()` method in your application's service provider. ```php -use Illuminate\Http\Request; -use Symfony\Component\HttpFoundation\Response; +// app/Providers/AppServiceProvider.php use Inertia\Inertia; - -->withExceptions(function (Exceptions $exceptions) { - $exceptions->respond(function (Response $response, Throwable $exception, Request $request) { - if (! app()->environment(['local', 'testing']) && in_array($response->getStatusCode(), [500, 503, 404, 403])) { - return Inertia::render('ErrorPage', ['status' => $response->getStatusCode()]) - ->toResponse($request) - ->setStatusCode($response->getStatusCode()); +use Inertia\ExceptionResponse; + +public function boot(): void +{ + Inertia::handleExceptionsUsing(function (ExceptionResponse $response) { + if (in_array($response->statusCode(), [403, 404, 500, 503])) { + return $response->render('ErrorPage', [ + 'status' => $response->statusCode(), + ])->withSharedData(); } - - if ($response->getStatusCode() === 419) { - return back()->with([ - 'message' => 'The page expired, please try again.', - ]); - } - - return $response; }); -}) +} ``` -You may have noticed we're returning an `ErrorPage` page component in the example above. You'll need to actually create this component, which will serve as the generic error page for your application. Here's an example error component you can use as a starting point. +Since exceptions like 404s occur outside of the Inertia middleware (the request never reaches your routes), the error response won't have access to shared data or the root view by default. Calling `withSharedData()` explicitly resolves the Inertia middleware and includes your shared props in the error page. + +The `ExceptionResponse` instance provides the `exception`, `request`, and `response` as public readonly properties, along with the following methods: + +- `render($component, $props)` - Render an Inertia page component with the given props +- `withSharedData()` - Include shared data from the Inertia middleware +- `usingMiddleware($class)` - Specify which Inertia middleware to use for shared data and root view resolution +- `rootView($view)` - Set a custom root view for the error response +- `statusCode()` - Get the HTTP status code of the original response + +The Inertia middleware is auto-resolved from the matched route or the kernel's middleware groups, so `withSharedData()` typically works without specifying a middleware class. Returning `null` from the callback falls through to Laravel's default exception rendering. + +### Error Page Example + +You'll need to create the error page components referenced above. Here's an example you may use as a starting point. @@ -112,39 +100,14 @@ export default function ErrorPage({ status }) { return (
    -

    {title}

    +

    {title}

    {description}
    ); } ``` -```svelte Svelte 4 icon="s" - - -
    -

    {title}

    -
    {description}
    -
    -``` - -```svelte Svelte 5 icon="s" +```svelte Svelte icon="s" ``` ```jsx React icon="react" @@ -128,18 +135,7 @@ useEffect(() => { }, []); ``` -```js Svelte 4 icon="s" -import { router } from "@inertiajs/svelte"; -import { onMount } from "svelte"; - -onMount(() => { - return router.on("start", (event) => { - console.log(`Starting a visit to ${event.detail.visit.url}`); - }); -}); -``` - -```js Svelte 5 icon="s" +```js Svelte icon="s" import { router } from "@inertiajs/svelte"; $effect(() => { @@ -198,7 +194,7 @@ document.removeEventListener("inertia:start", startEventListener); ## Cancelling Events -Some events, such as `before`, `exception`, and `invalid`, support cancellation, allowing you to prevent Inertia's default behavior. Just like native events, the event will be cancelled if only one event listener calls `event.preventDefault()`. +Some events, such as `before`, `networkError`, and `httpException`, support cancellation, allowing you to prevent Inertia's default behavior. Just like native events, the event will be cancelled if only one event listener calls `event.preventDefault()`. @@ -432,8 +428,6 @@ The `success` event is not cancelable. ## Flash -v2.3.3+ - The `flash` event fires when [flash data](/v3/data-props/flash-data) is received from the server. This is useful for displaying toast notifications or handling temporary data in a central location. @@ -506,18 +500,18 @@ router.on("error", (errors) => { The `error` event is not cancelable. -## Invalid +## HTTP Exception -The `invalid` event fires when a non-Inertia response is received from the server, such as an HTML or vanilla JSON response. A valid Inertia response is a response that has the `X-Inertia` header set to `true` with a `json` payload containing [the page object](/v3/core-concepts/the-protocol#the-page-object). +The `httpException` event fires when a non-Inertia response is received from the server, such as an HTML or vanilla JSON response. A valid Inertia response is a response that has the `X-Inertia` header set to `true` with a `json` payload containing [the page object](/v3/core-concepts/the-protocol#the-page-object). -This event is fired for all response types, including `200`, `400`, and `500`response codes. +This event is fired for all response types, including `200`, `400`, and `500` response codes. ```js Vue icon="vuejs" import { router } from "@inertiajs/vue3"; -router.on("invalid", (event) => { +router.on("httpException", (event) => { console.log(`An invalid Inertia response was received.`); console.log(event.detail.response); }); @@ -526,7 +520,7 @@ router.on("invalid", (event) => { ```jsx React icon="react" import { router } from "@inertiajs/react"; -router.on("invalid", (event) => { +router.on("httpException", (event) => { console.log(`An invalid Inertia response was received.`); console.log(event.detail.response); }); @@ -535,7 +529,7 @@ router.on("invalid", (event) => { ```js Svelte icon="s" import { router } from "@inertiajs/svelte"; -router.on("invalid", (event) => { +router.on("httpException", (event) => { console.log(`An invalid Inertia response was received.`); console.log(event.detail.response); }); @@ -543,14 +537,14 @@ router.on("invalid", (event) => { -You may cancel the `invalid` event to prevent Inertia from showing the non-Inertia response modal. +You may cancel the `httpException` event to prevent Inertia from showing the non-Inertia response modal. ```js Vue icon="vuejs" import { router } from "@inertiajs/vue3"; -router.on("invalid", (event) => { +router.on("httpException", (event) => { event.preventDefault(); // Handle the invalid response yourself... @@ -560,7 +554,7 @@ router.on("invalid", (event) => { ```jsx React icon="react" import { router } from "@inertiajs/react"; -router.on("invalid", (event) => { +router.on("httpException", (event) => { event.preventDefault(); // Handle the invalid response yourself... @@ -570,7 +564,7 @@ router.on("invalid", (event) => { ```js Svelte icon="s" import { router } from "@inertiajs/svelte"; -router.on("invalid", (event) => { +router.on("httpException", (event) => { event.preventDefault(); // Handle the invalid response yourself... @@ -579,49 +573,49 @@ router.on("invalid", (event) => { -## Exception +## Network Error -The `exception` event fires on unexpected XHR errors such as network interruptions. In addition, this event fires for errors generated when resolving page components. +The `networkError` event fires on unexpected XHR errors such as network interruptions. In addition, this event fires for errors generated when resolving page components. ```js Vue icon="vuejs" import { router } from "@inertiajs/vue3"; -router.on("exception", (event) => { +router.on("networkError", (event) => { console.log(`An unexpected error occurred during an Inertia visit.`); - console.log(event.detail.error); + console.log(event.detail.exception); }); ``` ```jsx React icon="react" import { router } from "@inertiajs/react"; -router.on("exception", (event) => { +router.on("networkError", (event) => { console.log(`An unexpected error occurred during an Inertia visit.`); - console.log(event.detail.error); + console.log(event.detail.exception); }); ``` ```js Svelte icon="s" import { router } from "@inertiajs/svelte"; -router.on("exception", (event) => { +router.on("networkError", (event) => { console.log(`An unexpected error occurred during an Inertia visit.`); - console.log(event.detail.error); + console.log(event.detail.exception); }); ``` -You may cancel the `exception` event to prevent the error from being thrown. +You may cancel the `networkError` event to prevent the error from being thrown. ```js Vue icon="vuejs" import { router } from "@inertiajs/vue3"; -router.on("exception", (event) => { +router.on("networkError", (event) => { event.preventDefault(); // Handle the error yourself }); @@ -630,7 +624,7 @@ router.on("exception", (event) => { ```jsx React icon="react" import { router } from "@inertiajs/react"; -router.on("exception", (event) => { +router.on("networkError", (event) => { event.preventDefault(); // Handle the error yourself }); @@ -639,7 +633,7 @@ router.on("exception", (event) => { ```js Svelte icon="s" import { router } from "@inertiajs/svelte"; -router.on("exception", (event) => { +router.on("networkError", (event) => { event.preventDefault(); // Handle the error yourself }); diff --git a/v3/advanced/server-side-rendering.mdx b/v3/advanced/server-side-rendering.mdx index ac1ebda..9a1caef 100644 --- a/v3/advanced/server-side-rendering.mdx +++ b/v3/advanced/server-side-rendering.mdx @@ -5,8 +5,6 @@ title: Server-Side Rendering (SSR) import { ClientSpecific } from "/snippets/client-specific.jsx" import { ReactSpecific } from "/snippets/react-specific.jsx" import { SvelteSpecific } from "/snippets/svelte-specific.jsx" -import { Svelte4Specific } from "/snippets/svelte4-specific.jsx" -import { Svelte5Specific } from "/snippets/svelte5-specific.jsx" import { VueSpecific } from "/snippets/vue-specific.jsx" Server-side rendering pre-renders your JavaScript pages on the server, allowing your visitors to receive fully rendered HTML when they visit your application. Since fully rendered HTML is served by your application, it's also easier for search engines to index your site. @@ -21,43 +19,90 @@ If you are using [Laravel Starter Kits](https://laravel.com/docs/starter-kits), npm run build:ssr ``` -## Install Dependencies +## Vite Plugin Setup + +The recommended way to configure SSR is with the `@inertiajs/vite` plugin. This approach handles SSR configuration automatically, including development mode SSR without a separate Node.js server. + + + + ```bash + npm install @inertiajs/vite + ``` + + + + Add the Inertia plugin to your `vite.config.js` file. The plugin will automatically detect your SSR entry point. + + ```js vite.config.js + import inertia from '@inertiajs/vite' + import laravel from 'laravel-vite-plugin' + import { defineConfig } from 'vite' + + export default defineConfig({ + plugins: [ + laravel({ + input: ['resources/css/app.css', 'resources/js/app.js'], + refresh: true, + }), + inertia(), + ], + }) + ``` + + You may also configure SSR options explicitly. + + ```js vite.config.js + inertia({ + ssr: { + entry: 'resources/js/ssr.js', + port: 13714, + cluster: true, + }, + }) + ``` + + + + Update the `build` script in your `package.json` to build both bundles. + + ```json package.json + "scripts": { + "dev": "vite", + "build": "vite build" // [!code --] + "build": "vite build && vite build --ssr" // [!code ++] + }, + ``` + + -If you are not using a Laravel starter kit and would like to manually configure SSR, we'll first install the additional dependencies required for server-side rendering. This is only necessary for the Vue adapters, so you can skip this step if you're using React or Svelte. +### Development Mode - +The Vite plugin handles SSR automatically during development without running a separate Node.js server. It exposes a server endpoint that Laravel uses for rendering, complete with HMR support. -```bash Vue icon="vuejs" -npm install @vue/server-renderer -``` +Simply run `npm run dev` as usual. SSR is handled by the Vite dev server. -```js React icon="react" -// No additional dependencies required -``` +### Production -```js Svelte icon="s" -// No additional dependencies required -``` - - - -## Add Server Entry-Point - -Next, we'll create a `resources/js/ssr.js` file within our Laravel project that will serve as our SSR entry point. +For production, build both bundles and start the SSR server. ```bash -touch resources/js/ssr.js +npm run build +php artisan inertia:start-ssr ``` -This file is going to look very similar to your `resources/js/app.js` file, except it's not going to run in the browser, but rather in Node.js. Here's a complete example. +### Custom SSR Entry Point + +The Vite plugin reuses your `app.js` entry point for both browser and SSR rendering by default, so no separate file is needed. The plugin detects the `data-server-rendered` attribute to decide whether to hydrate or mount, and the `setup` and `resolve` callbacks are optional. + +If you need custom SSR logic (such as Vue plugins that should only run on the server), you may create a separate `resources/js/ssr.js` file. ```js Vue icon="vuejs" import { createInertiaApp } from '@inertiajs/vue3' import createServer from '@inertiajs/vue3/server' -import { renderToString } from '@vue/server-renderer' import { createSSRApp, h } from 'vue' +import { renderToString } from 'vue/server-renderer' createServer(page => createInertiaApp({ @@ -94,25 +139,7 @@ createServer(page => ) ``` -```js Svelte 4 icon="s" -import { createInertiaApp } from '@inertiajs/svelte' -import createServer from '@inertiajs/svelte/server' - -createServer(page => - createInertiaApp({ - page, - resolve: name => { - const pages = import.meta.glob('./Pages/**/*.svelte', { eager: true }) - return pages[`./Pages/${name}.svelte`] - }, - setup({ App, props }) { - return App.render(props) - }, - }), -) -``` - -```js Svelte 5 icon="s" +```js Svelte icon="s" import { createInertiaApp } from '@inertiajs/svelte' import createServer from '@inertiajs/svelte/server' import { render } from 'svelte/server' @@ -133,22 +160,128 @@ createServer(page => -When creating this file, be sure to add anything that's missing from your `app.js` file that makes sense to run in SSR mode, such as plugins or custom mixins. +Be sure to add anything that's missing from your `app.js` file that makes sense to run in SSR mode, such as plugins or custom mixins. + +## Manual Setup + +If you prefer not to use the Vite plugin, you may configure SSR manually. + + + + Create a `resources/js/ssr.js` file within your Laravel project. + + + + ```js Vue icon="vuejs" + import { createInertiaApp } from '@inertiajs/vue3' + import createServer from '@inertiajs/vue3/server' + import { renderToString } from 'vue/server-renderer' + import { createSSRApp, h } from 'vue' + + createServer(page => + createInertiaApp({ + page, + render: renderToString, + resolve: name => { + const pages = import.meta.glob('./Pages/**/*.vue', { eager: true }) + return pages[`./Pages/${name}.vue`] + }, + setup({ App, props, plugin }) { + return createSSRApp({ + render: () => h(App, props), + }).use(plugin) + }, + }), + ) + ``` + + ```jsx React icon="react" + import { createInertiaApp } from '@inertiajs/react' + import createServer from '@inertiajs/react/server' + import ReactDOMServer from 'react-dom/server' + + createServer(page => + createInertiaApp({ + page, + render: ReactDOMServer.renderToString, + resolve: name => { + const pages = import.meta.glob('./Pages/**/*.jsx', { eager: true }) + return pages[`./Pages/${name}.jsx`] + }, + setup: ({ App, props }) => , + }), + ) + ``` + + ```js Svelte icon="s" + import { createInertiaApp } from '@inertiajs/svelte' + import createServer from '@inertiajs/svelte/server' + import { render } from 'svelte/server' + + createServer(page => + createInertiaApp({ + page, + resolve: name => { + const pages = import.meta.glob('./Pages/**/*.svelte', { eager: true }) + return pages[`./Pages/${name}.svelte`] + }, + setup({ App, props }) { + return render(App, { props }) + }, + }), + ) + ``` + + + + + + Add the `ssr` property to the Laravel Vite plugin configuration. + + ```js vite.config.js + export default defineConfig({ + plugins: [ + laravel({ + input: ['resources/css/app.css', 'resources/js/app.js'], + ssr: 'resources/js/ssr.js', // [!code ++] + refresh: true, + }), + // ... + ], + }) + ``` + + + + Update the `build` script in your `package.json` to build both bundles. + + ```json package.json + "scripts": { + "dev": "vite", + "build": "vite build" // [!code --] + "build": "vite build && vite build --ssr" // [!code ++] + }, + ``` + + ### Clustering -By default, the SSR server will run on a single thread. Clustering starts multiple Node servers on the same port, requests are then handled by each thread in a round-robin way. +By default, the SSR server runs on a single thread. You may enable clustering to start multiple Node servers on the same port, with requests handled by each thread in a round-robin fashion. -You can enable clustering by passing a second argument of options to `createServer`. +```js vite.config.js +inertia({ + ssr: { + cluster: true, + }, +}) +``` + +When using a [custom SSR entry point](#custom-ssr-entry-point) or [manual setup](#manual-setup), you may pass the `cluster` option to `createServer` instead. ```js Vue icon="vuejs" -import { createInertiaApp } from '@inertiajs/vue3' -import createServer from '@inertiajs/vue3/server' -import { renderToString } from '@vue/server-renderer' -import { createSSRApp, h } from 'vue' - createServer(page => createInertiaApp({ // ... @@ -158,10 +291,6 @@ createServer(page => ``` ```jsx React icon="react" -import { createInertiaApp } from '@inertiajs/react' -import createServer from '@inertiajs/react/server' -import ReactDOMServer from 'react-dom/server' - createServer(page => createInertiaApp({ // ... @@ -170,23 +299,7 @@ createServer(page => ) ``` -```js Svelte 4 icon="s" -import { createInertiaApp } from '@inertiajs/svelte' -import createServer from '@inertiajs/svelte/server' - -createServer(page => - createInertiaApp({ - // ... - }), - { cluster: true }, -) -``` - -```js Svelte 5 icon="s" -import { createInertiaApp } from '@inertiajs/svelte' -import createServer from '@inertiajs/svelte/server' -import { render } from 'svelte/server' - +```js Svelte icon="s" createServer(page => createInertiaApp({ // ... @@ -197,44 +310,9 @@ createServer(page => -## Setup Vite - -Next, we need to update our Vite configuration to build our new `ssr.js` file. We can do this by adding a `ssr` property to Laravel's Vite plugin configuration in our `vite.config.js`file. - -```js vite.config.js -export default defineConfig({ - plugins: [ - laravel({ - input: ['resources/css/app.css', 'resources/js/app.js'], - ssr: 'resources/js/ssr.js', // [!code ++] - refresh: true, - }), - // ... - ], -}) -``` - -## Update NPM Script - -Next, let's update the `build` script in our `package.json` file to also build our new `ssr.js` file. - -```json package.json -"scripts": { - "dev": "vite", - "build": "vite build" // [!code --] - "build": "vite build && vite build --ssr" // [!code ++] -}, -``` - -Now you can build both your client-side and server-side bundles. - -```bash -npm run build -``` - ## Running the SSR Server -Now that you have built both your client-side and server-side bundles, you should be able run the Node-based Inertia SSR server using the following command. +Once you have built both your client-side and server-side bundles, you may start the SSR server using the following Artisan command. ```bash php artisan inertia:start-ssr @@ -254,13 +332,11 @@ With the server running, you should be able to access your app within the browse Since your website is now being server-side rendered, you can instruct VueReactSvelte to "hydrate" the static markup and make it interactive instead of re-rendering all the HTML that we just generated. -To enable client-side hydration in a Vue app, update your `ssr.js` file to use `createSSRApp` instead of `createApp`. +To enable client-side hydration in a Vue app, update your `app.js` file to use `createSSRApp` instead of `createApp`. -To enable client-side hydration in a React app, update your `ssr.js` file to use `hydrateRoot` instead of `createRoot`. +To enable client-side hydration in a React app, update your `app.js` file to use `hydrateRoot` instead of `createRoot`. -To enable client-side hydration in a Svelte 4 app, set the `hydrate` option to `true` in your `ssr.js` file. - -To enable client-side hydration in a Svelte 5 app, update your `ssr.js` file to use `hydrate` instead of `mount` when server rendering. +To enable client-side hydration in a Svelte app, update your `app.js` file to use `hydrate` instead of `mount` when server rendering. @@ -300,22 +376,7 @@ createInertiaApp({ }) ``` -```js Svelte 4 icon="s" -import { createInertiaApp } from '@inertiajs/svelte' - -createInertiaApp({ - resolve: name => { - const pages = import.meta.glob('./Pages/**/*.svelte', { eager: true }) - return pages[`./Pages/${name}.svelte`] - }, - setup({ el, App, props }) { - new App({ target: el, props }) // [!code --] - new App({ target: el, props, hydrate: true }) // [!code ++] - }, -}) -``` - -```js Svelte 5 icon="s" +```js Svelte icon="s" import { createInertiaApp } from '@inertiajs/svelte' import { mount } from 'svelte' // [!code --] import { hydrate, mount } from 'svelte' // [!code ++] @@ -338,36 +399,73 @@ createInertiaApp({ - +## Error Handling -You will also need to set the `hydratable` compiler option to `true` in your `vite.config.js` file: +When SSR rendering fails, Inertia gracefully falls back to client-side rendering and dispatches an `SsrRenderFailed` event. You may listen for this event to log failures or send them to an error tracking service. -```js vite.config.js -import { svelte } from '@sveltejs/vite-plugin-svelte' -import laravel from 'laravel-vite-plugin' -import { defineConfig } from 'vite' - -export default defineConfig({ - plugins: [ - laravel.default({ - input: ['resources/css/app.css', 'resources/js/app.js'], - ssr: 'resources/js/ssr.js', - refresh: true, - }), - svelte(), // [!code --] - svelte({ // [!code ++:5] - compilerOptions: { - hydratable: true, - }, - }), - ], -}) +```php +use Illuminate\Support\Facades\Log; +use Inertia\Ssr\SsrRenderFailed; + +Event::listen(SsrRenderFailed::class, function (SsrRenderFailed $event) { + Log::warning('SSR failed', $event->toArray()); +}); +``` + +### Throwing on Error + +For CI or E2E testing, you may prefer SSR failures to throw an exception instead of falling back silently. Set the `throw_on_error` option in your `config/inertia.php` file. + +```php +'ssr' => [ + 'throw_on_error' => (bool) env('INERTIA_SSR_THROW_ON_ERROR', false), +], +``` + +You may set the environment variable in your `phpunit.xml` to catch SSR errors during testing. + +```xml + ``` - ## Disabling SSR -Sometimes you may wish to disable server-side rendering for certain pages or routes in your application. You may do so by setting the `inertia.ssr.enabled` configuration value to `false` for the current request, typically in a service provider or middleware. +Sometimes you may wish to disable server-side rendering for certain pages or routes in your application. + +### Per-Route via Middleware + +You may use the `$withoutSsr` property on your Inertia middleware to disable SSR for specific route patterns. + +```php +use Inertia\Middleware; + +class HandleInertiaRequests extends Middleware +{ + /** + * Defines the routes that should not use SSR. + * + * @var array + */ + protected $withoutSsr = [ + 'admin/*', + 'dashboard', + ]; +} +``` + +### Via Facade + +You may also disable SSR for specific routes using the `Inertia::withoutSsr()` method, typically called from a service provider. + +```php +use Inertia\Inertia; + +Inertia::withoutSsr(['admin/*', 'dashboard']); +``` + +### Per-Request + +You may disable SSR for the current request by setting the `inertia.ssr.enabled` configuration value to `false`. ```php if (request()->is('admin/*')) { diff --git a/v3/advanced/typescript.mdx b/v3/advanced/typescript.mdx index 7423c0e..84b8554 100644 --- a/v3/advanced/typescript.mdx +++ b/v3/advanced/typescript.mdx @@ -254,6 +254,111 @@ const form = useForm<{ }); ``` +## Form Component + +The `
    ` component accepts a generic type parameter for type-safe slot props. In React, you may pass the generic directly. In Vue and Svelte, you may use the `createForm` helper to create a typed form component. + + + +```tsx React icon="react" +import { Form } from "@inertiajs/react"; + +interface UserForm { + name: string; + email: string; +} + +export default function CreateUser() { + return ( + action="/users" method="post"> + {({ errors }) => ( + <> + + {errors.name &&
    {errors.name}
    } + + + )} + + ); +} +``` + +```vue Vue icon="vuejs" + + + +``` + +```svelte Svelte icon="s" + + + + {#snippet children({ errors })} + + {#if errors.name}
    {errors.name}
    {/if} + + {/snippet} +
    +``` + +
    + +The generic provides autocomplete and type checking for the `errors` object, `setError`, `clearErrors`, and other slot props that reference form fields. + +### useFormContext + +The `useFormContext()` function also accepts a generic type parameter, providing type-safe access to the form context from child components. + + + +```tsx React icon="react" +import { useFormContext } from "@inertiajs/react"; + +const form = useFormContext(); +``` + +```vue Vue icon="vuejs" + +``` + +```svelte Svelte icon="s" + +``` + + + ## Remembering State The `useRemember` hook accepts a generic type parameter for type-safe local state persistence, providing autocomplete and ensuring values match the expected types. diff --git a/v3/data-props/deferred-props.mdx b/v3/data-props/deferred-props.mdx index 5788ff8..091f17d 100644 --- a/v3/data-props/deferred-props.mdx +++ b/v3/data-props/deferred-props.mdx @@ -71,25 +71,7 @@ export default () => ( ); ``` -```svelte Svelte 4 icon="s" - - - - -
    Loading...
    -
    - - {#each permissions as permission} - - {/each} -
    -``` - -```svelte Svelte 5 icon="s" +```svelte Svelte icon="s" - + {#snippet fallback()}
    Loading...
    -
    + {/snippet}
    ``` -```svelte Svelte 5 icon="s" +
    + +## Reloading Indicator + +When deferred props are being reloaded via a partial reload, the `Deferred` component exposes a `reloading` boolean through its slot. This allows you to show a loading indicator while still displaying the previously loaded data. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +import { Deferred } from "@inertiajs/react"; + +export default () => ( + Loading...
    }> + {({ reloading }) => ( +
    + +
    + )} + +); +``` + +```svelte Svelte icon="s" - + {#snippet fallback()}
    Loading...
    {/snippet} - + {#snippet children({ reloading })} +
    + {#each permissions as permission} + + {/each} +
    + {/snippet}
    ``` +The `reloading` prop is `false` on the initial load and becomes `true` whenever a partial reload is in progress for the deferred keys. It returns to `false` once the reload completes. + ## Combining with Once Props You may chain the `once()` modifier onto a deferred prop to ensure the data is resolved only once and remembered by the client across subsequent navigations. diff --git a/v3/data-props/flash-data.mdx b/v3/data-props/flash-data.mdx index b841231..3325701 100644 --- a/v3/data-props/flash-data.mdx +++ b/v3/data-props/flash-data.mdx @@ -2,9 +2,7 @@ title: Flash Data --- -v2.3.3+ - -Sometimes you may wish to send one-time data to your frontend that shouldn't reappear when users navigate through browser history. Unlike regular props, flash data isn't persisted in history state, making it ideal for success messages, newly created IDs, or other temporary values. +Flash data lets you send one-time data to your frontend that won't reappear when users navigate through browser history. Unlike regular props, flash data isn't persisted in history state, making it ideal for success messages, newly created IDs, or other temporary values. ## Flashing Data diff --git a/v3/data-props/load-when-visible.mdx b/v3/data-props/load-when-visible.mdx index 03093eb..88eb1a1 100644 --- a/v3/data-props/load-when-visible.mdx +++ b/v3/data-props/load-when-visible.mdx @@ -36,25 +36,7 @@ export default () => ( ); ``` -```svelte Svelte 4 icon="s" - - - - -
    Loading...
    -
    - - {#each permissions as permission} - - {/each} -
    -``` - -```svelte Svelte 5 icon="s" +```svelte Svelte icon="s" - - - -
    Loading...
    -
    - - -
    -``` - -```svelte Svelte 5 icon="s" +```svelte Svelte icon="s" - - - -
    Loading...
    -
    - - {#each permissions as permission} - - {/each} -
    -``` - -```svelte Svelte 5 icon="s" +```svelte Svelte icon="s" - - - - -``` - -```svelte Svelte 5 icon="s" +```svelte Svelte icon="s" - - - - -``` - -```svelte Svelte 5 icon="s" +```svelte Svelte icon="s" @@ -380,28 +303,35 @@ export default () => (
    Refreshing...
    {/if} - + {#snippet fallback()}
    Loading...
    -
    + {/snippet}
    ``` -```svelte Svelte 5 icon="s" - +## Preserving Errors - - - {#if fetching} -
    Refreshing...
    - {/if} +The `WhenVisible` component sets `preserveErrors: true` by default, ensuring that validation errors are not cleared when it triggers a reload. You may override this via the `params` prop. - {#snippet fallback()} -
    Loading...
    - {/snippet} + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" + + {/* ... */} + +``` + +```svelte Svelte icon="s" + + ``` @@ -466,32 +396,7 @@ export default function CreateUser() { } ``` -```svelte Svelte 4 icon="s" - - -
    - -
    - - - - -``` - -```svelte Svelte 5 icon="s" +```svelte Svelte icon="s" - -
    - - $form.avatar = e.target.files[0]} /> - {#if $form.progress} - - {$form.progress.percentage}% - - {/if} - -
    -``` - -```svelte Svelte 5 icon="s" +```svelte Svelte icon="s" - -
    - - {#if $form.errors.email} -
    {$form.errors.email}
    - {/if} - - {#if $form.errors.password} -
    {$form.errors.password}
    - {/if} - Remember Me - -
    -``` - -```svelte Svelte 5 icon="s" +```svelte Svelte icon="s" - -
    $form.post('/users')}> - $form.validate('name')} /> - {#if $form.invalid('name')} -

    {$form.errors.name}

    - {/if} - - $form.validate('email')} /> - {#if $form.invalid('email')} -

    {$form.errors.email}

    - {/if} - - {#if $form.validating} -

    Validating...

    - {/if} - - -
    -``` - -```svelte Svelte 5 icon="s" +```svelte Svelte icon="s" - -
    - - - - - - - - - - -
    -``` - -```svelte Svelte 5 icon="s" +```svelte Svelte icon="s" + + +``` + +```jsx React icon="react" +import { useHttp } from '@inertiajs/react' + +export default function Search() { + const { data, setData, get, processing } = useHttp({ + query: '', + }) + + function search(e) { + setData('query', e.target.value) + get('/api/search', { + onSuccess: (response) => { + console.log(response) + }, + }) + } + + return ( + <> + + {processing &&
    Searching...
    } + + ) +} +``` + +```svelte Svelte icon="s" + + + +{#if $form.processing} +
    Searching...
    +{/if} +``` + +
    + +Unlike router visits, `useHttp` requests do not trigger page navigation or interact with Inertia's page lifecycle. They are plain HTTP requests that return JSON responses. + +## Submitting Data + +The hook provides `get`, `post`, `put`, `patch`, and `delete` convenience methods. A generic `submit` method is also available for dynamic HTTP methods. + + + +```js Vue icon="vuejs" +form.get(url, options) +form.post(url, options) +form.put(url, options) +form.patch(url, options) +form.delete(url, options) +form.submit(method, url, options) +``` + +```js React icon="react" +const { get, post, put, patch, delete: destroy, submit } = useHttp({ ... }) + +get(url, options) +post(url, options) +put(url, options) +patch(url, options) +destroy(url, options) +submit(method, url, options) +``` + +```js Svelte icon="s" +$form.get(url, options) +$form.post(url, options) +$form.put(url, options) +$form.patch(url, options) +$form.delete(url, options) +$form.submit(method, url, options) +``` + + + +Each method returns a `Promise` that resolves with the parsed JSON response data. + + + +```js Vue icon="vuejs" +const response = await form.post('/api/comments', { + onError: (errors) => { + console.log(errors) + }, +}) +``` + +```js React icon="react" +const response = await post('/api/comments', { + onError: (errors) => { + console.log(errors) + }, +}) +``` + +```js Svelte icon="s" +const response = await $form.post('/api/comments', { + onError: (errors) => { + console.log(errors) + }, +}) +``` + + + +## Reactive State + +The `useHttp` hook exposes the same reactive properties as `useForm`: + +| Property | Type | Description | +|---|---|---| +| `data` | `TForm` | The current form data | +| `errors` | `object` | Validation errors keyed by field name | +| `hasErrors` | `boolean` | Whether validation errors exist | +| `processing` | `boolean` | Whether a request is in progress | +| `progress` | `object \| null` | Upload progress with `percentage` and `total` | +| `wasSuccessful` | `boolean` | Whether the last request was successful | +| `recentlySuccessful` | `boolean` | `true` for two seconds after a successful request | +| `isDirty` | `boolean` | Whether the data differs from its defaults | +| `response` | `TResponse \| null` | The last successful response data | + +## Validation Errors + +When a request returns a `422` status code, the hook automatically parses validation errors and makes them available through the `errors` property. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +import { useHttp } from '@inertiajs/react' + +export default function CreateUser() { + const { data, setData, post, errors, processing } = useHttp({ + name: '', + email: '', + }) + + function save(e) { + e.preventDefault() + post('/api/users') + } + + return ( +
    + setData('name', e.target.value)} /> + {errors.name &&
    {errors.name}
    } + + setData('email', e.target.value)} /> + {errors.email &&
    {errors.email}
    } + + +
    + ) +} +``` + +```svelte Svelte icon="s" + + + +{#if $form.errors.name} +
    {$form.errors.name}
    +{/if} + + +{#if $form.errors.email} +
    {$form.errors.email}
    +{/if} + + +``` + +
    + +## Displaying All Errors + +By default, validation errors are simplified to the first error message for each field. You may chain `withAllErrors()` to receive all error messages as arrays, which is useful for fields with multiple validation rules. + + + +```js Vue icon="vuejs" +const form = useHttp({ + name: '', + email: '', +}).withAllErrors() + +// form.errors.name === ['Name is required.', 'Name must be at least 3 characters.'] +``` + +```js React icon="react" +const form = useHttp({ + name: '', + email: '', +}).withAllErrors() + +// form.errors.name === ['Name is required.', 'Name must be at least 3 characters.'] +``` + +```js Svelte icon="s" +const form = useHttp({ + name: '', + email: '', +}).withAllErrors() + +// $form.errors.name === ['Name is required.', 'Name must be at least 3 characters.'] +``` + + + +The same method is available on the [`useForm`](/v3/the-basics/forms#options-2) helper and the [`
    `](/v3/the-basics/forms#options) component. + +## File Uploads + +When the form data includes files, the hook automatically sends the request as `multipart/form-data`. Upload progress is available through the `progress` property. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +import { useHttp } from '@inertiajs/react' + +export default function Upload() { + const { setData, post, progress, processing } = useHttp({ + file: null, + }) + + return ( + <> + setData('file', e.target.files[0])} /> + {progress && } + + + ) +} +``` + +```svelte Svelte icon="s" + + + $form.file = e.target.files[0]} /> +{#if $form.progress} + +{/if} + +``` + + + +## Cancelling Requests + +You may cancel an in-progress request using the `cancel()` method. + + + +```js Vue icon="vuejs" +form.cancel() +``` + +```js React icon="react" +const { cancel } = useHttp({ ... }) + +cancel() +``` + +```js Svelte icon="s" +$form.cancel() +``` + + + +## Optimistic Updates + +The `useHttp` hook supports [optimistic updates](/v3/the-basics/optimistic-updates) via the `optimistic()` method. The callback receives the current form data and should return a partial update to apply immediately. + + + +```js Vue icon="vuejs" +form.optimistic((data) => ({ + likes: data.likes + 1, +})).post('/api/likes') +``` + +```js React icon="react" +const { optimistic, post } = useHttp({ likes: 0 }) + +optimistic((data) => ({ + likes: data.likes + 1, +})) +post('/api/likes') +``` + +```js Svelte icon="s" +$form.optimistic((data) => ({ + likes: data.likes + 1, +})).post('/api/likes') +``` + + + +The update is applied synchronously. If the request fails, the data is rolled back to its previous state. + +## Event Callbacks + +Each submit method accepts an options object with lifecycle callbacks: + +```js +form.post('/api/users', { + onBefore: () => { ... }, + onStart: () => { ... }, + onProgress: (progress) => { ... }, + onSuccess: (response) => { ... }, + onError: (errors) => { ... }, + onCancel: () => { ... }, + onFinish: () => { ... }, +}) +``` + +You may return `false` from `onBefore` to cancel the request. + +## Precognition + +The `useHttp` hook supports [Laravel Precognition](https://laravel.com/docs/precognition) for real-time validation. Enable it by chaining `withPrecognition()` with the HTTP method and validation endpoint. + + + +```js Vue icon="vuejs" +import { useHttp } from '@inertiajs/vue3' + +const form = useHttp({ + name: '', + email: '', +}).withPrecognition('post', '/api/users') +``` + +```js React icon="react" +import { useHttp } from '@inertiajs/react' + +const form = useHttp({ + name: '', + email: '', +}).withPrecognition('post', '/api/users') +``` + +```js Svelte icon="s" +import { useHttp } from '@inertiajs/svelte' + +const form = useHttp({ + name: '', + email: '', +}).withPrecognition('post', '/api/users') +``` + + + +Once enabled, the `validate()`, `touch()`, `touched()`, `valid()`, and `invalid()` methods become available, working identically to [form precognition](/v3/the-basics/forms#precognition-1). + +## History State + +You may persist form data and errors in browser history state by providing a remember key as the first argument. + + + +```js Vue icon="vuejs" +const form = useHttp('SearchForm', { + query: '', +}) +``` + +```js React icon="react" +const form = useHttp('SearchForm', { + query: '', +}) +``` + +```js Svelte icon="s" +const form = useHttp('SearchForm', { + query: '', +}) +``` + + + +You may exclude sensitive fields from being stored in history state using the `dontRemember()` method. + +```js +const form = useHttp('LoginForm', { + email: '', + token: '', +}).dontRemember('token') +``` diff --git a/v3/the-basics/layouts.mdx b/v3/the-basics/layouts.mdx new file mode 100644 index 0000000..7edc185 --- /dev/null +++ b/v3/the-basics/layouts.mdx @@ -0,0 +1,659 @@ +--- +title: Layouts +--- + +import { ClientSpecific } from "/snippets/client-specific.jsx" +import { ReactSpecific } from "/snippets/react-specific.jsx" +import { SvelteSpecific } from "/snippets/svelte-specific.jsx" +import { VueSpecific } from "/snippets/vue-specific.jsx" + +Most applications share common UI elements across pages, such as a primary navigation bar, sidebar, or footer. Layout components let you define this shared UI once and wrap your pages with it automatically. + +## Creating Layouts + +A layout is a standard component that accepts child content. There is nothing Inertia-specific about it. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +import { Link } from "@inertiajs/react" + +export default function Layout({ children }) { + return ( +
    +
    + Home + About + Contact +
    +
    {children}
    +
    + ) +} +``` + +```svelte Svelte icon="s" + + +
    +
    + Home + About + Contact +
    +
    + {@render children()} +
    +
    +``` + +
    + +You may use a layout by wrapping your page content with it directly. However, this approach forces the layout instance to be destroyed and recreated between visits. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +import Layout from './Layout' + +export default function Welcome({ user }) { + return ( + +

    Welcome

    +

    Hello {user.name}, welcome to your first Inertia app!

    +
    + ) +} +``` + +```svelte Svelte icon="s" + + + +

    Welcome

    +

    Hello {user.name}, welcome to your first Inertia app!

    +
    +``` + +
    + +## Persistent Layouts + +Wrapping a page with a layout as a child component works, but it means the layout is destroyed and recreated on every visit. This prevents maintaining layout state across navigations, such as an audio player that should keep playing or a sidebar that should retain its scroll position. + +Persistent layouts solve this by telling Inertia which layout to use for a page. Inertia then manages the layout instance separately, keeping it alive between visits. + + + +```vue Vue icon="vuejs" + + + + + +``` + +```jsx React icon="react" +import Layout from './Layout' + +const Welcome = ({ user }) => { + return ( + <> +

    Welcome

    +

    Hello {user.name}, welcome to your first Inertia app!

    + + ) +} + +Welcome.layout = Layout + +export default Welcome +``` + +```svelte Svelte icon="s" + + + + +

    Welcome

    +

    Hello {user.name}, welcome to your first Inertia app!

    +``` + +
    + + + +Vue 3.3+ users may alternatively use [defineOptions](https://vuejs.org/api/sfc-script-setup.html#defineoptions) to define a layout within ` +``` + + + +### Nested Layouts + +You may create more complex layout arrangements using nested layouts. Pass an array of layout components to wrap the page in multiple layers. + + + +```vue Vue icon="vuejs" + + + + + +``` + +```jsx React icon="react" +import SiteLayout from './SiteLayout' +import NestedLayout from './NestedLayout' + +const Welcome = ({ user }) => { + return ( + <> +

    Welcome

    +

    Hello {user.name}, welcome to your first Inertia app!

    + + ) +} + +Welcome.layout = [SiteLayout, NestedLayout] + +export default Welcome +``` + +```svelte Svelte icon="s" + + + + +

    Welcome

    +

    Hello {user.name}, welcome to your first Inertia app!

    +``` + +
    + +## Default Layouts + +The `layout` option in `createInertiaApp` lets you define a default layout for all pages, saving you from defining it on every page individually. Per-page layouts always take precedence over the default. + +```js +import Layout from './Layout' + +createInertiaApp({ + layout: () => Layout, + // ... +}) +``` + +You may also conditionally return a layout based on the page name. For example, you may wish to exclude public pages from the default layout. + +```js +import Layout from './Layout' + +createInertiaApp({ + layout: (name) => { + if (name.startsWith('Public/')) { + return null + } + + return Layout + }, + // ... +}) +``` + +The full page object is also available as the second argument, giving you access to the page's URL, props, and other metadata. + +The `layout` callback supports all layout formats, including arrays for [nested layouts](#nested-layouts), named objects for [named layouts](#targeting-named-layouts), and tuples for [static props](#static-props). + +### Using the Resolve Callback + +You may also set a default layout inside the `resolve` callback by mutating the resolved page component. This approach is useful when you need access to the resolved component itself. + + + +```js Vue icon="vuejs" +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 + }, + // ... +}) +``` + +```js React icon="react" +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 || Layout + return page + }, + // ... +}) +``` + +```js Svelte icon="s" +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 } + }, + // ... +}) +``` + + + +## Layout Props + +Persistent layouts often need dynamic data from the current page, such as a page title, the active navigation item, or a sidebar toggle. Layout props provide a way to define defaults in your layout and override them from any page. + +### Defining Defaults + +Use the `useLayoutProps` hook in your layout component to declare which props the layout accepts and their default values. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +import { useLayoutProps } from '@inertiajs/react' + +export default function Layout({ children }) { + const { title, showSidebar } = useLayoutProps({ + title: 'My App', + showSidebar: true, + }) + + return ( + <> +
    {title}
    + {showSidebar && } +
    {children}
    + + ) +} +``` + +```svelte Svelte icon="s" + + +
    {$layout.title}
    +{#if $layout.showSidebar} + +{/if} +
    + {@render children()} +
    +``` + +
    + +The defaults object defines which keys the layout will respond to. Only keys declared in the defaults are included in the merged result. Any extra keys set from pages are ignored. + + + +In Vue, `useLayoutProps` returns a `ComputedRef`, so access its properties directly (e.g., `layout.title`). The values update reactively when pages set new layout props. + + + + + +In Svelte, `useLayoutProps` returns a readable store. Use the `$` prefix to access its values (e.g., `$layout.title`). + + + +### Setting Props From Pages + +Use the `setLayoutProps` function from any page component to update the layout's props dynamically. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +import { setLayoutProps } from '@inertiajs/react' + +export default function Dashboard() { + setLayoutProps({ + title: 'Dashboard', + showSidebar: false, + }) + + return

    Dashboard

    +} +``` + +```svelte Svelte icon="s" + + +

    Dashboard

    +``` + +
    + +The layout will re-render with the merged values: `{ title: 'Dashboard', showSidebar: false }`. + +### Targeting Named Layouts + +You may also define your persistent layouts as a named object, allowing you to target specific layouts with props. + + + +```vue Vue icon="vuejs" + +``` + +```jsx React icon="react" +import AppLayout from './AppLayout' +import ContentLayout from './ContentLayout' + +Dashboard.layout = { + app: AppLayout, + content: ContentLayout, +} +``` + +```svelte Svelte icon="s" + +``` + + + +Use `setLayoutPropsFor` to set props for a specific named layout. + + + +```js Vue icon="vuejs" +import { setLayoutPropsFor } from '@inertiajs/vue3' + +setLayoutPropsFor('sidebar', { + collapsed: true, +}) +``` + +```js React icon="react" +import { setLayoutPropsFor } from '@inertiajs/react' + +setLayoutPropsFor('sidebar', { + collapsed: true, +}) +``` + +```js Svelte icon="s" +import { setLayoutPropsFor } from '@inertiajs/svelte' + +setLayoutPropsFor('sidebar', { + collapsed: true, +}) +``` + + + +Named layout props are merged with shared layout props, with named props taking priority. + +### Static Props + +You may also pass static props directly in your persistent layout definition using a tuple. Static props are set once when the layout is defined and don't change between page navigations. + + + +```vue Vue icon="vuejs" + + + + + +``` + +```jsx React icon="react" +import Layout from './Layout' + +const Dashboard = ({ user }) => { + return

    Dashboard

    +} + +Dashboard.layout = [Layout, { title: 'Dashboard' }] + +export default Dashboard +``` + +```svelte Svelte icon="s" + + + + +

    Dashboard

    +``` + +
    + +Named layouts may also include static props using the same tuple syntax. + +```js +Dashboard.layout = { + app: [AppLayout, { theme: 'dark' }], + content: [ContentLayout, { padding: 'sm' }], +} +``` + +For unnamed nested layouts with static props, use an array of tuples. + +```js +Dashboard.layout = [ + [AppLayout, { title: 'Dashboard' }], + [ContentLayout, { padding: 'sm' }], +] +``` + +### Merge Priority + +Layout props are resolved from three sources with the following priority (highest to lowest): + +1. **Dynamic props** - set via `setLayoutProps()` or `setLayoutPropsFor()` +2. **Static props** - defined in the persistent layout definition +3. **Defaults** - declared in `useLayoutProps()` + +Only keys present in the defaults are included in the final result. + +### Auto-Reset on Navigation + +Dynamic layout props are automatically reset to their defaults when navigating to a new page (unless `preserveState` is enabled). This ensures each page starts with a clean slate and only the layout props explicitly set by that page are applied. + +### Resetting Props + +You may also manually reset all dynamic layout props back to their defaults using `resetLayoutProps`. + + + +```js Vue icon="vuejs" +import { resetLayoutProps } from '@inertiajs/vue3' + +resetLayoutProps() +``` + +```js React icon="react" +import { resetLayoutProps } from '@inertiajs/react' + +resetLayoutProps() +``` + +```js Svelte icon="s" +import { resetLayoutProps } from '@inertiajs/svelte' + +resetLayoutProps() +``` + + diff --git a/v3/the-basics/links.mdx b/v3/the-basics/links.mdx index 6c8d288..69fa3ea 100644 --- a/v3/the-basics/links.mdx +++ b/v3/the-basics/links.mdx @@ -101,8 +101,6 @@ import { inertia, Link } from '@inertiajs/svelte' ## Wayfinder -v2.0.6+ - When using [Wayfinder](https://github.com/laravel/wayfinder) in conjunction with the `Link`component, you can simply pass the resulting object directly to the `href` prop. The `Link` will infer the HTTP method and URL directly from the Wayfinder object. diff --git a/v3/the-basics/manual-visits.mdx b/v3/the-basics/manual-visits.mdx index a1512dd..492f6f3 100644 --- a/v3/the-basics/manual-visits.mdx +++ b/v3/the-basics/manual-visits.mdx @@ -27,6 +27,7 @@ router.visit(url, { reset: [], preserveUrl: false, prefetch: false, + preserveErrors: false, viewTransition: false, onCancelToken: (cancelToken) => {}, onCancel: () => {}, @@ -35,6 +36,8 @@ router.visit(url, { onProgress: (progress) => {}, onSuccess: (page) => {}, onError: (errors) => {}, + onHttpException: (response) => {}, + onNetworkError: (error) => {}, onFinish: (visit) => {}, onPrefetching: () => {}, onPrefetched: () => {}, @@ -62,6 +65,7 @@ router.visit(url, { reset: [], preserveUrl: false, prefetch: false, + preserveErrors: false, viewTransition: false, onCancelToken: (cancelToken) => {}, onCancel: () => {}, @@ -70,6 +74,8 @@ router.visit(url, { onProgress: (progress) => {}, onSuccess: (page) => {}, onError: (errors) => {}, + onHttpException: (response) => {}, + onNetworkError: (error) => {}, onFinish: (visit) => {}, onPrefetching: () => {}, onPrefetched: () => {}, @@ -97,6 +103,7 @@ router.visit(url, { reset: [], preserveUrl: false, prefetch: false, + preserveErrors: false, viewTransition: false, onCancelToken: (cancelToken) => {}, onCancel: () => {}, @@ -105,6 +112,8 @@ router.visit(url, { onProgress: (progress) => {}, onSuccess: (page) => {}, onError: (errors) => {}, + onHttpException: (response) => {}, + onNetworkError: (error) => {}, onFinish: (visit) => {}, onPrefetching: () => {}, onPrefetched: () => {}, @@ -184,8 +193,6 @@ Uploading files via `put` or `patch` is not supported in Laravel. Instead, make ## Wayfinder -v2.1.2+ - When using [Wayfinder](https://github.com/laravel/wayfinder), you can pass the resulting object directly to any router method. The router will infer the HTTP method and URL from the Wayfinder object. @@ -219,7 +226,7 @@ router.delete(destroy(1)); -If you provide both a Wayfinder object and specify the `method` option, the `method`option will take precedence. +If you provide both a Wayfinder object and specify the `method` option, the `method` option will take precedence. @@ -688,7 +695,9 @@ import { router } from '@inertiajs/vue3' router.prependToProp('notifications', (current, props) => { return { id: Date.now(), - message: `Hello ${props.user.name}\ + message: `Hello ${props.user.name}`, + } +}) ``` ```js React icon="react" @@ -697,7 +706,9 @@ import { router } from '@inertiajs/react' router.prependToProp('notifications', (current, props) => { return { id: Date.now(), - message: `Hello ${props.user.name}\ + message: `Hello ${props.user.name}`, + } +}) ``` ```js Svelte icon="s" @@ -706,7 +717,9 @@ import { router } from '@inertiajs/svelte' router.prependToProp('notifications', (current, props) => { return { id: Date.now(), - message: `Hello ${props.user.name}\ + message: `Hello ${props.user.name}`, + } +}) ``` @@ -993,6 +1006,8 @@ router.post("/users", data, { onProgress: (progress) => {}, onSuccess: (page) => {}, onError: (errors) => {}, + onHttpException: (response) => {}, + onNetworkError: (error) => {}, onCancel: () => {}, onFinish: (visit) => {}, onPrefetching: () => {}, @@ -1009,6 +1024,8 @@ router.post("/users", data, { onProgress: (progress) => {}, onSuccess: (page) => {}, onError: (errors) => {}, + onHttpException: (response) => {}, + onNetworkError: (error) => {}, onCancel: () => {}, onFinish: (visit) => {}, onPrefetching: () => {}, @@ -1025,6 +1042,8 @@ router.post("/users", data, { onProgress: (progress) => {}, onSuccess: (page) => {}, onError: (errors) => {}, + onHttpException: (response) => {}, + onNetworkError: (error) => {}, onCancel: () => {}, onFinish: (visit) => {}, onPrefetching: () => {}, @@ -1034,26 +1053,32 @@ router.post("/users", data, { -Returning `false` from the `onBefore()` callback will cause the visit to be cancelled. +Returning `false` from the `onBefore()` callback will cause the visit to be cancelled. Returning `false` from `onHttpException()` or `onNetworkError()` will prevent the corresponding [global event](/v3/advanced/events) from being fired and its default behavior. ```js Vue icon="vuejs" import { router } from '@inertiajs/vue3' -router.delete(`/users/${user.id}\ +router.delete(`/users/${user.id}`, { + onBefore: () => confirm('Are you sure you want to delete this user?'), +}) ``` ```js React icon="react" import { router } from '@inertiajs/react' -router.delete(`/users/${user.id}\ +router.delete(`/users/${user.id}`, { + onBefore: () => confirm('Are you sure you want to delete this user?'), +}) ``` ```js Svelte icon="s" import { router } from '@inertiajs/svelte' -router.delete(`/users/${user.id}\ +router.delete(`/users/${user.id}`, { + onBefore: () => confirm('Are you sure you want to delete this user?'), +}) ``` @@ -1071,7 +1096,7 @@ router.post(url, { this.firstTask(), this.secondTask() ]) - } + }, onFinish: visit => { // Not called until firstTask() and secondTask() have finished }, @@ -1087,7 +1112,7 @@ router.post(url, { this.firstTask(), this.secondTask() ]) - } + }, onFinish: visit => { // Not called until firstTask() and secondTask() have finished }, @@ -1103,7 +1128,7 @@ router.post(url, { this.firstTask(), this.secondTask() ]) - } + }, onFinish: visit => { // Not called until firstTask() and secondTask() have finished }, diff --git a/v3/the-basics/optimistic-updates.mdx b/v3/the-basics/optimistic-updates.mdx new file mode 100644 index 0000000..1c25ef2 --- /dev/null +++ b/v3/the-basics/optimistic-updates.mdx @@ -0,0 +1,302 @@ +--- +title: Optimistic Updates +--- + +import { ClientSpecific } from "/snippets/client-specific.jsx" +import { ReactSpecific } from "/snippets/react-specific.jsx" +import { SvelteSpecific } from "/snippets/svelte-specific.jsx" +import { VueSpecific } from "/snippets/vue-specific.jsx" + +Inertia allows you to update the UI immediately without waiting for the server to respond, such as incrementing a like counter, toggling a bookmark, or adding an item to a list. Optimistic updates apply changes instantly while the request is in flight, automatically rolling back if the request fails. + +## Router Visits + +You may chain the `optimistic()` method before any router visit. The callback receives the current page props and should return a partial update to apply immediately. + + + +```js Vue icon="vuejs" +import { router } from '@inertiajs/vue3' + +router.optimistic((props) => ({ + ...props, + post: { + ...props.post, + likes: props.post.likes + 1, + }, +})).post(`/posts/${post.id}/like`) +``` + +```js React icon="react" +import { router } from '@inertiajs/react' + +router.optimistic((props) => ({ + ...props, + post: { + ...props.post, + likes: props.post.likes + 1, + }, +})).post(`/posts/${post.id}/like`) +``` + +```js Svelte icon="s" +import { router } from '@inertiajs/svelte' + +router.optimistic((props) => ({ + ...props, + post: { + ...props.post, + likes: props.post.likes + 1, + }, +})).post(`/posts/${post.id}/like`) +``` + + + +The optimistic update is applied synchronously to the current page's props, so your component re-renders immediately with the new values. When the server responds, Inertia replaces the optimistic data with the server's response. If the request fails, the props are automatically reverted to their original values. + +## Form Component + +The `` component supports optimistic updates via the `optimistic` prop. The callback receives the current page props and the form data being submitted. + + + +```vue Vue icon="vuejs" + +``` + +```jsx React icon="react" + ({ + todos: [...props.todos, { id: Date.now(), name: data.name, done: false }], + })} +> + + + +``` + +```svelte Svelte icon="s" +
    ({ + todos: [...props.todos, { id: Date.now(), name: data.name, done: false }], + })} +> + + +
    +``` + +
    + +The `
    ` component manages input data internally, so the form data is provided as a second callback argument. The callback should return a partial props update, just like router visits and `useForm`. + +## Form Helper + +The `useForm` helper also supports optimistic updates via the same `optimistic()` method. + + + +```vue Vue icon="vuejs" + + + +``` + +```jsx React icon="react" +import { useForm } from '@inertiajs/react' + +export default function Posts({ posts }) { + const { data, setData, optimistic, post, processing } = useForm({ + title: '', + }) + + function save(e) { + e.preventDefault() + optimistic((props) => ({ + ...props, + posts: [...props.posts, { title: data.title }], + })) + post('/posts') + } + + return ( + + setData('title', e.target.value)} /> + + + ) +} +``` + +```svelte Svelte icon="s" + + + + +``` + +
    + +The `useForm` optimistic callback operates on page props, just like router visits. The callback receives the current page props and the rollback restores those props to their pre-request state. + +## HTTP Requests + +The [`useHttp`](/v3/the-basics/http-requests) hook supports optimistic updates in the same way. + + + +```js Vue icon="vuejs" +import { useHttp } from '@inertiajs/vue3' + +const form = useHttp({ + likes: 0, +}) + +form.optimistic((data) => ({ + likes: data.likes + 1, +})).post('/api/likes') +``` + +```js React icon="react" +import { useHttp } from '@inertiajs/react' + +const { optimistic, post } = useHttp({ + likes: 0, +}) + +optimistic((data) => ({ + likes: data.likes + 1, +})) +post('/api/likes') +``` + +```js Svelte icon="s" +import { useHttp } from '@inertiajs/svelte' + +const form = useHttp({ + likes: 0, +}) + +$form.optimistic((data) => ({ + likes: data.likes + 1, +})).post('/api/likes') +``` + + + +## How It Works + +When an optimistic update is applied: + +1. The returned props are compared against the current page props, and only the keys that actually changed are snapshotted +2. The callback's return value is merged into the current data +3. The request is sent to the server +4. On success, the server's response replaces the optimistic data +5. On failure, only the snapshotted keys are reverted, rolling back the optimistic changes + +The callback should return a **partial** object containing only the keys you wish to update. The returned values are shallow-merged with the current data. + +### Automatic Rollback + +Optimistic state is automatically reverted in several scenarios: + +- **Validation errors (422)**: The optimistic state is reverted and the validation errors are preserved +- **Server errors**: When the request fails for any other reason, the original state is restored +- **Interrupted visits**: When a new visit interrupts an in-flight request, the previous optimistic state is restored before the new optimistic update is applied + +## Inline Option + +You may also pass the optimistic callback directly in the visit options instead of chaining. + + + +```js Vue icon="vuejs" +import { router } from '@inertiajs/vue3' + +router.post(`/posts/${post.id}/like`, {}, { + optimistic: (props) => ({ + post: { ...props.post, likes: props.post.likes + 1 }, + }), +}) +``` + +```js React icon="react" +import { router } from '@inertiajs/react' + +router.post(`/posts/${post.id}/like`, {}, { + optimistic: (props) => ({ + post: { ...props.post, likes: props.post.likes + 1 }, + }), +}) +``` + +```js Svelte icon="s" +import { router } from '@inertiajs/svelte' + +router.post(`/posts/${post.id}/like`, {}, { + optimistic: (props) => ({ + post: { ...props.post, likes: props.post.likes + 1 }, + }), +}) +``` + + + +The inline option works with `useHttp` submit methods as well. + +```js +form.post('/api/likes', { + optimistic: (data) => ({ + likes: data.likes + 1, + }), +}) +``` diff --git a/v3/the-basics/pages.mdx b/v3/the-basics/pages.mdx index 90398bc..43fb3b6 100644 --- a/v3/the-basics/pages.mdx +++ b/v3/the-basics/pages.mdx @@ -49,24 +49,7 @@ export default function Welcome({ user }) { } ``` -```svelte Svelte 4 icon="s" - - - - Welcome - - - -

    Welcome

    -

    Hello {user.name}, welcome to your first Inertia app!

    -
    -``` - -```svelte Svelte 5 icon="s" +```svelte Svelte icon="s" - - -``` - -```jsx React icon="react" -import { Link } from "@inertiajs/react"; - -export default function Layout({ children }) { - return ( -
    -
    - Home - About - Contact -
    -
    {children}
    -
    - ); -} -``` - -```svelte Svelte 4 icon="s" - - -
    -
    - Home - About - Contact -
    -
    - -
    -
    -``` - -```svelte Svelte 5 icon="s" - - -
    -
    - Home - About - Contact -
    -
    - {@render children()} -
    -
    -``` - - - - - As you can see, there is nothing Inertia specific within this template. This - is just a typical Vue - React - Svelte component. - - -## Persistent Layouts - -While it's simple to implement layouts as children of page components, it forces the layout instance to be destroyed and recreated between visits. This means you cannot have persistent layout state when navigating between pages. - -For example, maybe you have an audio player on a podcast website that you want to continue playing as users navigate the site. Or, maybe you simply want to maintain the scroll position in your sidebar navigation between page visits. In these situations, the solution is to leverage Inertia's persistent layouts. - - - -```vue Vue icon="vuejs" - - - - - -``` - -```jsx React icon="react" -import Layout from './Layout' +## Layouts -const Home = ({ user }) => { - return ( - -

    Welcome

    -

    Hello {user.name}, welcome to your first Inertia app!

    - - ) -} - -Home.layout = page => - -export default Home -``` - -```svelte Svelte 4 icon="s" - - - - -

    Welcome

    -

    Hello {user.name}, welcome to your first Inertia app!

    -``` - -```svelte Svelte 5 icon="s" - - - - -

    Welcome

    -

    Hello {user.name}, welcome to your first Inertia app!

    -``` - -
    - -You can also create more complex layout arrangements using nested layouts. - - - -```vue Vue icon="vuejs" - - - - - -``` - -```jsx React icon="react" -import SiteLayout from './SiteLayout' -import NestedLayout from './NestedLayout' - -const Home = ({ user }) => { - return ( - -

    Welcome

    -

    Hello {user.name}, welcome to your first Inertia app!

    - - ) -} - -Home.layout = page => ( - - - -) - -export default Home -``` - -```svelte Svelte 4 icon="s" - - - - -

    Welcome

    -

    Hello {user.name}, welcome to your first Inertia app!

    -``` - -```svelte Svelte 5 icon="s" - - - - -

    Welcome

    -

    Hello {user.name}, welcome to your first Inertia app!

    -``` - -
    - - - -If you're using Vue 3.3+, you can alternatively use [defineOptions](https://vuejs.org/api/sfc-script-setup.html#defineoptions) to define a layout within ` -``` - - - -## Default Layouts - -If you're using persistent layouts, you may find it convenient to define the default page layout in the `resolve()` callback of your application's main JavaScript file. - - - -```js Vue icon="vuejs" -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; - }, - // ... -}); -``` - -```js React icon="react" -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) => ); - return page; - }, - // ... -}); -``` - -```js Svelte icon="s" -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 }; - }, - // ... -}); -``` - - - -This will automatically set the page layout to `Layout` if a layout has not already been set for that page. - -You can even go a step further and conditionally set the default page layout based on the page `name`, which is available to the `resolve()` callback. For example, maybe you don't want the default layout to be applied to your public pages. - - - -```js Vue icon="vuejs" -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 = name.startsWith("Public/") ? undefined : Layout; - return page; - }, - // ... -}); -``` - -```js React icon="react" -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 = name.startsWith("Public/") - ? undefined - : (page) => ; - return page; - }, - // ... -}); -``` - -```js Svelte icon="s" -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: name.startsWith("Public/") ? undefined : Layout, - }; - }, - // ... -}); -``` - - +Most applications share common UI elements across pages. Inertia provides persistent layouts that survive page navigations, along with layout props for passing dynamic data between pages and their layouts. Visit the [layouts documentation](/v3/the-basics/layouts) to learn more. diff --git a/v3/the-basics/responses.mdx b/v3/the-basics/responses.mdx index 1848fbe..4f09886 100644 --- a/v3/the-basics/responses.mdx +++ b/v3/the-basics/responses.mdx @@ -37,6 +37,20 @@ class EventsController extends Controller } ``` +The component name may also be a Backed Enum, which is useful for organizing page components with type-safe references. + +```php +enum Page: string +{ + case EventShow = 'Event/Show'; + case EventIndex = 'Event/Index'; +} + +return Inertia::render(Page::EventShow, [ + 'event' => $event, +]); +``` + To ensure that pages load quickly, only return the minimum data required for the page. @@ -85,7 +99,7 @@ Arrayable objects like Eloquent models and collections are automatically convert When passing props to your components, you may want to create custom classes that can transform themselves into the appropriate data format. While Laravel's `Arrayable` interface simply converts objects to arrays, Inertia offers the more powerful `ProvidesInertiaProperty` interface for context-aware transformations. -This interface requires a `toInertiaProperty` method that receives a `PropertyContext`object containing the property key (`$context->key`), all props for the page (`$context->props`), and the request instance (`$context->request`). +This interface requires a `toInertiaProperty` method that receives a `PropertyContext` object containing the property key (`$context->key`), all props for the page (`$context->props`), and the request instance (`$context->request`). ```php use Inertia\PropertyContext; diff --git a/v3/the-basics/view-transitions.mdx b/v3/the-basics/view-transitions.mdx index e2f9b7d..b128953 100644 --- a/v3/the-basics/view-transitions.mdx +++ b/v3/the-basics/view-transitions.mdx @@ -2,8 +2,6 @@ title: View Transitions --- -v2.2.13+ - Inertia supports the [View Transitions API](https://developer.chrome.com/docs/web-platform/view-transitions), allowing you to animate page transitions. The View Transitions API is a [relatively new browser feature](https://caniuse.com/view-transitions). Inertia gracefully falls back to standard page transitions in browsers that don't support the API. From 923a5d832bfc4ce1daa985904aa38eb7ab328531 Mon Sep 17 00:00:00 2001 From: Pascal Baljet Date: Wed, 11 Feb 2026 15:58:40 +0100 Subject: [PATCH 03/27] Update http-requests.mdx --- v3/the-basics/http-requests.mdx | 144 ++++++++++++++++---------------- 1 file changed, 71 insertions(+), 73 deletions(-) diff --git a/v3/the-basics/http-requests.mdx b/v3/the-basics/http-requests.mdx index 17733f6..f890ded 100644 --- a/v3/the-basics/http-requests.mdx +++ b/v3/the-basics/http-requests.mdx @@ -11,7 +11,7 @@ Not every request needs to trigger an Inertia page visit. For calls to an extern ## Basic Usage -The `useHttp` hook accepts initial form data and returns reactive state along with methods for making HTTP requests. +The `useHttp` hook accepts initial data and returns reactive state along with methods for making HTTP requests. @@ -19,12 +19,12 @@ The `useHttp` hook accepts initial form data and returns reactive state along wi ``` @@ -68,12 +68,12 @@ export default function Search() { - -{#if $form.processing} + +{#if $http.processing}
    Searching...
    {/if} ``` @@ -98,12 +98,12 @@ The hook provides `get`, `post`, `put`, `patch`, and `delete` convenience method ```js Vue icon="vuejs" -form.get(url, options) -form.post(url, options) -form.put(url, options) -form.patch(url, options) -form.delete(url, options) -form.submit(method, url, options) +http.get(url, options) +http.post(url, options) +http.put(url, options) +http.patch(url, options) +http.delete(url, options) +http.submit(method, url, options) ``` ```js React icon="react" @@ -118,12 +118,12 @@ submit(method, url, options) ``` ```js Svelte icon="s" -$form.get(url, options) -$form.post(url, options) -$form.put(url, options) -$form.patch(url, options) -$form.delete(url, options) -$form.submit(method, url, options) +$http.get(url, options) +$http.post(url, options) +$http.put(url, options) +$http.patch(url, options) +$http.delete(url, options) +$http.submit(method, url, options) ``` @@ -133,7 +133,7 @@ Each method returns a `Promise` that resolves with the parsed JSON response data ```js Vue icon="vuejs" -const response = await form.post('/api/comments', { +const response = await http.post('/api/comments', { onError: (errors) => { console.log(errors) }, @@ -149,7 +149,7 @@ const response = await post('/api/comments', { ``` ```js Svelte icon="s" -const response = await $form.post('/api/comments', { +const response = await $http.post('/api/comments', { onError: (errors) => { console.log(errors) }, @@ -164,7 +164,6 @@ The `useHttp` hook exposes the same reactive properties as `useForm`: | Property | Type | Description | |---|---|---| -| `data` | `TForm` | The current form data | | `errors` | `object` | Validation errors keyed by field name | | `hasErrors` | `boolean` | Whether validation errors exist | | `processing` | `boolean` | Whether a request is in progress | @@ -172,7 +171,6 @@ The `useHttp` hook exposes the same reactive properties as `useForm`: | `wasSuccessful` | `boolean` | Whether the last request was successful | | `recentlySuccessful` | `boolean` | `true` for two seconds after a successful request | | `isDirty` | `boolean` | Whether the data differs from its defaults | -| `response` | `TResponse \| null` | The last successful response data | ## Validation Errors @@ -184,24 +182,24 @@ When a request returns a `422` status code, the hook automatically parses valida ``` @@ -237,27 +235,27 @@ export default function CreateUser() { - -{#if $form.errors.name} -
    {$form.errors.name}
    + +{#if $http.errors.name} +
    {$http.errors.name}
    {/if} - -{#if $form.errors.email} -
    {$form.errors.email}
    + +{#if $http.errors.email} +
    {$http.errors.email}
    {/if} - + ```
    @@ -269,30 +267,30 @@ By default, validation errors are simplified to the first error message for each ```js Vue icon="vuejs" -const form = useHttp({ +const http = useHttp({ name: '', email: '', }).withAllErrors() -// form.errors.name === ['Name is required.', 'Name must be at least 3 characters.'] +// http.errors.name === ['Name is required.', 'Name must be at least 3 characters.'] ``` ```js React icon="react" -const form = useHttp({ +const http = useHttp({ name: '', email: '', }).withAllErrors() -// form.errors.name === ['Name is required.', 'Name must be at least 3 characters.'] +// http.errors.name === ['Name is required.', 'Name must be at least 3 characters.'] ``` ```js Svelte icon="s" -const form = useHttp({ +const http = useHttp({ name: '', email: '', }).withAllErrors() -// $form.errors.name === ['Name is required.', 'Name must be at least 3 characters.'] +// $http.errors.name === ['Name is required.', 'Name must be at least 3 characters.'] ``` @@ -301,7 +299,7 @@ The same method is available on the [`useForm`](/v3/the-basics/forms#options-2) ## File Uploads -When the form data includes files, the hook automatically sends the request as `multipart/form-data`. Upload progress is available through the `progress` property. +When the data includes files, the hook automatically sends the request as `multipart/form-data`. Upload progress is available through the `progress` property. @@ -309,19 +307,19 @@ When the form data includes files, the hook automatically sends the request as ` ``` @@ -347,20 +345,20 @@ export default function Upload() { - $form.file = e.target.files[0]} /> -{#if $form.progress} - + $http.file = e.target.files[0]} /> +{#if $http.progress} + {/if} - + ``` @@ -372,7 +370,7 @@ You may cancel an in-progress request using the `cancel()` method. ```js Vue icon="vuejs" -form.cancel() +http.cancel() ``` ```js React icon="react" @@ -382,19 +380,19 @@ cancel() ``` ```js Svelte icon="s" -$form.cancel() +$http.cancel() ``` ## Optimistic Updates -The `useHttp` hook supports [optimistic updates](/v3/the-basics/optimistic-updates) via the `optimistic()` method. The callback receives the current form data and should return a partial update to apply immediately. +The `useHttp` hook supports [optimistic updates](/v3/the-basics/optimistic-updates) via the `optimistic()` method. The callback receives the current data and should return a partial update to apply immediately. ```js Vue icon="vuejs" -form.optimistic((data) => ({ +http.optimistic((data) => ({ likes: data.likes + 1, })).post('/api/likes') ``` @@ -409,7 +407,7 @@ post('/api/likes') ``` ```js Svelte icon="s" -$form.optimistic((data) => ({ +$http.optimistic((data) => ({ likes: data.likes + 1, })).post('/api/likes') ``` @@ -423,7 +421,7 @@ The update is applied synchronously. If the request fails, the data is rolled ba Each submit method accepts an options object with lifecycle callbacks: ```js -form.post('/api/users', { +http.post('/api/users', { onBefore: () => { ... }, onStart: () => { ... }, onProgress: (progress) => { ... }, @@ -445,7 +443,7 @@ The `useHttp` hook supports [Laravel Precognition](https://laravel.com/docs/prec ```js Vue icon="vuejs" import { useHttp } from '@inertiajs/vue3' -const form = useHttp({ +const http = useHttp({ name: '', email: '', }).withPrecognition('post', '/api/users') @@ -454,7 +452,7 @@ const form = useHttp({ ```js React icon="react" import { useHttp } from '@inertiajs/react' -const form = useHttp({ +const http = useHttp({ name: '', email: '', }).withPrecognition('post', '/api/users') @@ -463,7 +461,7 @@ const form = useHttp({ ```js Svelte icon="s" import { useHttp } from '@inertiajs/svelte' -const form = useHttp({ +const http = useHttp({ name: '', email: '', }).withPrecognition('post', '/api/users') @@ -475,24 +473,24 @@ Once enabled, the `validate()`, `touch()`, `touched()`, `valid()`, and `invalid( ## History State -You may persist form data and errors in browser history state by providing a remember key as the first argument. +You may persist data and errors in browser history state by providing a remember key as the first argument. ```js Vue icon="vuejs" -const form = useHttp('SearchForm', { +const http = useHttp('SearchData', { query: '', }) ``` ```js React icon="react" -const form = useHttp('SearchForm', { +const http = useHttp('SearchData', { query: '', }) ``` ```js Svelte icon="s" -const form = useHttp('SearchForm', { +const http = useHttp('SearchData', { query: '', }) ``` @@ -502,7 +500,7 @@ const form = useHttp('SearchForm', { You may exclude sensitive fields from being stored in history state using the `dontRemember()` method. ```js -const form = useHttp('LoginForm', { +const http = useHttp('Login', { email: '', token: '', }).dontRemember('token') From eb13bb1fb1d5a4c8207a55f945d32ab1795b2a0a Mon Sep 17 00:00:00 2001 From: Pascal Baljet Date: Wed, 11 Feb 2026 16:13:43 +0100 Subject: [PATCH 04/27] Svelte 5 syntax --- v3/data-props/infinite-scroll.mdx | 38 ++++----- v3/data-props/load-when-visible.mdx | 12 +-- v3/data-props/prefetching.mdx | 2 +- v3/the-basics/file-uploads.mdx | 12 +-- v3/the-basics/forms.mdx | 114 +++++++++++++-------------- v3/the-basics/http-requests.mdx | 52 ++++++------ v3/the-basics/optimistic-updates.mdx | 10 +-- v3/the-basics/validation.mdx | 4 +- 8 files changed, 124 insertions(+), 120 deletions(-) diff --git a/v3/data-props/infinite-scroll.mdx b/v3/data-props/infinite-scroll.mdx index 6b9665e..5489a5f 100644 --- a/v3/data-props/infinite-scroll.mdx +++ b/v3/data-props/infinite-scroll.mdx @@ -57,7 +57,7 @@ export default function Users({ users }) { ```svelte Svelte icon="s" @@ -192,7 +192,7 @@ export default function Users({ users }) { ```svelte Svelte icon="s" - - + + {#each users.data as user (user.id)} @@ -392,29 +392,29 @@ export default ({ users }) => ( ```svelte Svelte icon="s" -
    + {#snippet previous({ hasMore, fetch, loading })} {#if hasMore} - {/if} -
    + {/snippet} {#each users.data as user (user.id)}
    {user.name}
    {/each} -
    + {#snippet next({ hasMore, fetch, loading })} {#if hasMore} - {/if} -
    + {/snippet}
    ``` @@ -472,8 +472,10 @@ The main content area where you render your data items. This slot receives loadi ``` ```svelte Svelte icon="s" - - + + {#snippet children({ loading, loadingPrevious, loadingNext })} + + {/snippet} ``` @@ -505,9 +507,9 @@ The loading slot is used as a fallback when loading content and no custom `befor -
    + {#snippet loading()} Loading more users... -
    + {/snippet}
    ``` @@ -821,7 +823,7 @@ export default ({ users }) => { ```svelte Svelte icon="s" - + {#each users.data as user (user.id)} diff --git a/v3/data-props/load-when-visible.mdx b/v3/data-props/load-when-visible.mdx index 88eb1a1..94d065b 100644 --- a/v3/data-props/load-when-visible.mdx +++ b/v3/data-props/load-when-visible.mdx @@ -297,11 +297,13 @@ export default () => ( let { permissions } = $props() - - - {#if fetching} -
    Refreshing...
    - {/if} + + {#snippet children({ fetching })} + + {#if fetching} +
    Refreshing...
    + {/if} + {/snippet} {#snippet fallback()}
    Loading...
    diff --git a/v3/data-props/prefetching.mdx b/v3/data-props/prefetching.mdx index 2dc8f95..9541efb 100644 --- a/v3/data-props/prefetching.mdx +++ b/v3/data-props/prefetching.mdx @@ -406,7 +406,7 @@ const form = useForm({ }); function submit() { - $form.post("/users", { + form.post("/users", { invalidateCacheTags: ["users", "dashboard"], }); } diff --git a/v3/the-basics/file-uploads.mdx b/v3/the-basics/file-uploads.mdx index 5f4cc01..bc00ec4 100644 --- a/v3/the-basics/file-uploads.mdx +++ b/v3/the-basics/file-uploads.mdx @@ -112,16 +112,16 @@ return ( function submit(e) { e.preventDefault() - $form.post('/users') + form.post('/users') }
    - - $form.avatar = e.target.files[0]} /> - {#if $form.progress} - - {$form.progress.percentage}% + + form.avatar = e.target.files[0]} /> + {#if form.progress} + + {form.progress.percentage}% {/if} diff --git a/v3/the-basics/forms.mdx b/v3/the-basics/forms.mdx index f6c63b9..c247cbd 100644 --- a/v3/the-basics/forms.mdx +++ b/v3/the-basics/forms.mdx @@ -905,7 +905,7 @@ function handleSubmit() { - + ```
    @@ -967,11 +967,11 @@ import { useFormContext } from '@inertiajs/svelte' const form = useFormContext() -{#if $form} - {#if $form.isDirty}Unsaved changes{/if} - {#if $form.errors.name}{$form.errors.name}{/if} - - +{#if form} + {#if form.isDirty}Unsaved changes{/if} + {#if form.errors.name}{form.errors.name}{/if} + + {/if} ``` @@ -1429,21 +1429,21 @@ const form = useForm({ function submit(e) { e.preventDefault() - $form.post('/login') + form.post('/login') }
    - - {#if $form.errors.email} -
    {$form.errors.email}
    + + {#if form.errors.email} +
    {form.errors.email}
    {/if} - - {#if $form.errors.password} -
    {$form.errors.password}
    + + {#if form.errors.password} +
    {form.errors.password}
    {/if} - Remember Me - + Remember Me +
    ``` @@ -1474,12 +1474,12 @@ destroy(url, options) ``` ```js Svelte icon="s" -$form.submit(method, url, options); -$form.get(url, options); -$form.post(url, options); -$form.put(url, options); -$form.patch(url, options); -$form.delete(url, options); +form.submit(method, url, options); +form.get(url, options); +form.post(url, options); +form.put(url, options); +form.patch(url, options); +form.delete(url, options); ```
    @@ -1505,9 +1505,9 @@ post('/profile', { ``` ```js Svelte icon="s" -$form.post("/profile", { +form.post("/profile", { preserveScroll: true, - onSuccess: () => $form.reset("password"), + onSuccess: () => form.reset("password"), }); ``` @@ -1561,7 +1561,7 @@ const { processing } = useForm({ ... }) ``` ```svelte Svelte icon="s" - + ```
    @@ -1587,9 +1587,9 @@ const { progress } = useForm({ ... }) ``` ```svelte Svelte icon="s" -{#if $form.progress} - - {$form.progress.percentage}% +{#if form.progress} + + {form.progress.percentage}% {/if} ``` @@ -1613,8 +1613,8 @@ const { errors } = useForm({ ... }) ``` ```svelte Svelte icon="s" -{#if $form.errors.email} -
    {$form.errors.email}
    +{#if form.errors.email} +
    {form.errors.email}
    {/if} ``` @@ -1646,10 +1646,10 @@ clearErrors('field', 'anotherfield') ```js Svelte icon="s" // Clear all errors... -$form.clearErrors(); +form.clearErrors(); // Clear errors for specific fields... -$form.clearErrors("field", "anotherfield"); +form.clearErrors("field", "anotherfield"); ``` @@ -1684,10 +1684,10 @@ setError({ ```js Svelte icon="s" // Set a single error -$form.setError("field", "Your error message."); +form.setError("field", "Your error message."); // Set multiple errors at once -$form.setError({ +form.setError({ foo: "Your error message for the foo field.", bar: "Some other error for the bar field.", }); @@ -1727,10 +1727,10 @@ reset('field', 'anotherfield') ```js Svelte icon="s" // Reset the form... -$form.reset(); +form.reset(); // Reset specific fields... -$form.reset("field", "anotherfield"); +form.reset("field", "anotherfield"); ``` @@ -1759,10 +1759,10 @@ resetAndClearErrors('field', 'anotherfield') ```js Svelte icon="s" // Reset the form and clear all errors... -$form.resetAndClearErrors(); +form.resetAndClearErrors(); // Reset specific fields and clear their errors... -$form.resetAndClearErrors("field", "anotherfield"); +form.resetAndClearErrors("field", "anotherfield"); ``` @@ -1805,13 +1805,13 @@ setDefaults({ ```js Svelte icon="s" // Set the form's current values as the new defaults... -$form.defaults(); +form.defaults(); // Update the default value of a single field... -$form.defaults("email", "updated-default@example.com"); +form.defaults("email", "updated-default@example.com"); // Change the default value of multiple fields... -$form.defaults({ +form.defaults({ name: "Updated Example", email: "updated-default@example.com", }); @@ -1836,7 +1836,7 @@ const { isDirty } = useForm({ ... }) ``` ```svelte Svelte icon="s" -{#if $form.isDirty} +{#if form.isDirty}
    There are unsaved form changes.
    {/if} ``` @@ -1860,7 +1860,7 @@ cancel() ``` ```js Svelte icon="s" -$form.cancel(); +form.cancel(); ``` @@ -2138,18 +2138,18 @@ const form = useForm('post', '/users', { }) -
    { e.preventDefault(); $form.post('/users') }}> - $form.validate('name')} /> - {#if $form.invalid('name')} -

    {$form.errors.name}

    + { e.preventDefault(); form.post('/users') }}> + form.validate('name')} /> + {#if form.invalid('name')} +

    {form.errors.name}

    {/if} - $form.validate('email')} /> - {#if $form.invalid('email')} -

    {$form.errors.email}

    + form.validate('email')} /> + {#if form.invalid('email')} +

    {form.errors.email}

    {/if} - {#if $form.validating} + {#if form.validating}

    Validating...

    {/if} @@ -2186,12 +2186,12 @@ The `touch()` method marks fields as "touched" without triggering validation. Yo ``` ```svelte Svelte icon="s" - $form.touch('name')} /> - $form.touch('email')} /> + form.touch('name')} /> + form.touch('email')} /> - + -{#if $form.touched('name')} +{#if form.touched('name')}

    Name has been touched

    {/if} ``` @@ -2223,7 +2223,7 @@ const form = useForm("post", "/users", { name: "", }); -$form.setValidationTimeout(500); +form.setValidationTimeout(500); ``` @@ -2251,7 +2251,7 @@ const form = useForm("post", "/users", { avatar: null, }); -$form.validateFiles(); +form.validateFiles(); ``` @@ -2279,7 +2279,7 @@ const form = useForm("post", "/users", { name: "", }); -$form.withAllErrors(); +form.withAllErrors(); ``` diff --git a/v3/the-basics/http-requests.mdx b/v3/the-basics/http-requests.mdx index f890ded..83804da 100644 --- a/v3/the-basics/http-requests.mdx +++ b/v3/the-basics/http-requests.mdx @@ -73,7 +73,7 @@ const http = useHttp({ }) function search() { - $http.get('/api/search', { + http.get('/api/search', { onSuccess: (response) => { console.log(response) }, @@ -81,8 +81,8 @@ function search() { } - -{#if $http.processing} + +{#if http.processing}
    Searching...
    {/if} ``` @@ -118,12 +118,12 @@ submit(method, url, options) ``` ```js Svelte icon="s" -$http.get(url, options) -$http.post(url, options) -$http.put(url, options) -$http.patch(url, options) -$http.delete(url, options) -$http.submit(method, url, options) +http.get(url, options) +http.post(url, options) +http.put(url, options) +http.patch(url, options) +http.delete(url, options) +http.submit(method, url, options) ``` @@ -149,7 +149,7 @@ const response = await post('/api/comments', { ``` ```js Svelte icon="s" -const response = await $http.post('/api/comments', { +const response = await http.post('/api/comments', { onError: (errors) => { console.log(errors) }, @@ -241,21 +241,21 @@ const http = useHttp({ }) function save() { - $http.post('/api/users') + http.post('/api/users') } - -{#if $http.errors.name} -
    {$http.errors.name}
    + +{#if http.errors.name} +
    {http.errors.name}
    {/if} - -{#if $http.errors.email} -
    {$http.errors.email}
    + +{#if http.errors.email} +
    {http.errors.email}
    {/if} - + ``` @@ -290,7 +290,7 @@ const http = useHttp({ email: '', }).withAllErrors() -// $http.errors.name === ['Name is required.', 'Name must be at least 3 characters.'] +// http.errors.name === ['Name is required.', 'Name must be at least 3 characters.'] ``` @@ -350,15 +350,15 @@ const http = useHttp({ }) function upload() { - $http.post('/api/uploads') + http.post('/api/uploads') } - $http.file = e.target.files[0]} /> -{#if $http.progress} - + http.file = e.target.files[0]} /> +{#if http.progress} + {/if} - + ``` @@ -380,7 +380,7 @@ cancel() ``` ```js Svelte icon="s" -$http.cancel() +http.cancel() ``` @@ -407,7 +407,7 @@ post('/api/likes') ``` ```js Svelte icon="s" -$http.optimistic((data) => ({ +http.optimistic((data) => ({ likes: data.likes + 1, })).post('/api/likes') ``` diff --git a/v3/the-basics/optimistic-updates.mdx b/v3/the-basics/optimistic-updates.mdx index 1c25ef2..3b516ef 100644 --- a/v3/the-basics/optimistic-updates.mdx +++ b/v3/the-basics/optimistic-updates.mdx @@ -173,15 +173,15 @@ const form = useForm({ }) function save() { - $form.optimistic((props) => ({ + form.optimistic((props) => ({ ...props, - posts: [...props.posts, { title: $form.title }], + posts: [...props.posts, { title: form.title }], })).post('/posts') } - - + + ``` @@ -226,7 +226,7 @@ const form = useHttp({ likes: 0, }) -$form.optimistic((data) => ({ +form.optimistic((data) => ({ likes: data.likes + 1, })).post('/api/likes') ``` diff --git a/v3/the-basics/validation.mdx b/v3/the-basics/validation.mdx index 906a708..f13ee1c 100644 --- a/v3/the-basics/validation.mdx +++ b/v3/the-basics/validation.mdx @@ -106,7 +106,7 @@ export default function Edit() { - + { e.preventDefault(); handleSubmit() }}> {#if errors.first_name}
    {errors.first_name}
    {/if} From c10a7ffa178fa8692a4a8b9bdd9502da8d5d6ee5 Mon Sep 17 00:00:00 2001 From: Pascal Baljet Date: Wed, 11 Feb 2026 16:24:27 +0100 Subject: [PATCH 05/27] wip --- v3/advanced/code-splitting.mdx | 2 +- v3/data-props/partial-reloads.mdx | 2 +- v3/data-props/prefetching.mdx | 2 +- v3/the-basics/forms.mdx | 14 +++++++------- v3/the-basics/links.mdx | 2 +- v3/the-basics/redirects.mdx | 2 +- v3/the-basics/routing.mdx | 2 +- v3/the-basics/validation.mdx | 2 +- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/v3/advanced/code-splitting.mdx b/v3/advanced/code-splitting.mdx index 0c2592a..4749137 100644 --- a/v3/advanced/code-splitting.mdx +++ b/v3/advanced/code-splitting.mdx @@ -61,7 +61,7 @@ Next, create a `.babelrc` file in your project with the following configuration: If you're using Laravel Mix, the dynamic imports Babel plugin is already installed and configured, and you can skip these steps. We recommend using Laravel Mix 6 or above, as there are known issues with older versions. -Finally, update the `resolve` callback in your app's initialization code to use `import`instead of `require`. +Finally, update the `resolve` callback in your app's initialization code to use `import` instead of `require`. diff --git a/v3/data-props/partial-reloads.mdx b/v3/data-props/partial-reloads.mdx index cc65a76..cbab1a5 100644 --- a/v3/data-props/partial-reloads.mdx +++ b/v3/data-props/partial-reloads.mdx @@ -4,7 +4,7 @@ title: Partial Reloads When making visits to the same page you are already on, it's not always necessary to re-fetch all of the page's data from the server. In fact, selecting only a subset of the data can be a helpful performance optimization if it's acceptable that some page data becomes stale. Inertia makes this possible via its "partial reload" feature. -As an example, consider a "user index" page that includes a list of users, as well as an option to filter the users by their company. On the first request to the page, both the `users` and `companies`props are passed to the page component. However, on subsequent visits to the same page (maybe to filter the users), you can request only the `users` data from the server without requesting the `companies` data. Inertia will then automatically merge the partial data returned from the server with the data it already has in memory client-side. +As an example, consider a "user index" page that includes a list of users, as well as an option to filter the users by their company. On the first request to the page, both the `users` and `companies` props are passed to the page component. However, on subsequent visits to the same page (maybe to filter the users), you can request only the `users` data from the server without requesting the `companies` data. Inertia will then automatically merge the partial data returned from the server with the data it already has in memory client-side. Partial reloads only work for visits made to the same page component. diff --git a/v3/data-props/prefetching.mdx b/v3/data-props/prefetching.mdx index 9541efb..d125981 100644 --- a/v3/data-props/prefetching.mdx +++ b/v3/data-props/prefetching.mdx @@ -432,7 +432,7 @@ router.post("/posts", postData, { ## Stale While Revalidate -By default, Inertia will fetch a fresh copy of the data when the user visits the page if the cached data is older than the cache duration. You can customize this behavior by passing a tuple to the `cacheFor`prop. +By default, Inertia will fetch a fresh copy of the data when the user visits the page if the cached data is older than the cache duration. You can customize this behavior by passing a tuple to the `cacheFor` prop. The first value in the array represents the number of seconds the cache is considered fresh, while the second value defines how long it can be served as stale data before fetching data from the server is necessary. diff --git a/v3/the-basics/forms.mdx b/v3/the-basics/forms.mdx index c247cbd..83de83e 100644 --- a/v3/the-basics/forms.mdx +++ b/v3/the-basics/forms.mdx @@ -502,8 +502,8 @@ Some props are intentionally grouped under `options` instead of being top-level When setting the `disableWhileProcessing` `disableWhileProcessing` - `disable-while-processing`prop, the `Form` - component will add the `inert` attribute to the HTML `form`tag while the form + `disable-while-processing` prop, the `Form` + component will add the `inert` attribute to the HTML `form` tag while the form is processing to prevent user interaction. @@ -1449,7 +1449,7 @@ function submit(e) { -To submit the form, you may use the `get`, `post`, `put`, `patch`and `delete` methods. +To submit the form, you may use the `get`, `post`, `put`, `patch` and `delete` methods. @@ -1875,21 +1875,21 @@ To instruct Inertia to store a form's data and errors in [history state](/v3/dat import { useForm } from '@inertiajs/vue3' const form = useForm('CreateUser', data) -const form = useForm(`EditUser:${user.id}\ +const form = useForm(`EditUser:${user.id}`, data) ``` ```js React icon="react" import { useForm } from '@inertiajs/react' const form = useForm('CreateUser', data) -const form = useForm(`EditUser:${user.id}\ +const form = useForm(`EditUser:${user.id}`, data) ``` ```js Svelte icon="s" import { useForm } from '@inertiajs/svelte' const form = useForm('CreateUser', data) -const form = useForm(`EditUser:${user.id}\ +const form = useForm(`EditUser:${user.id}`, data) ``` @@ -2435,7 +2435,7 @@ function submit(e) { ## File Uploads -When making requests or form submissions that include files, Inertia will automatically convert the request data into a `FormData` object. This works with the `` component, `useForm`helper, and manual router submissions. +When making requests or form submissions that include files, Inertia will automatically convert the request data into a `FormData` object. This works with the `` component, `useForm` helper, and manual router submissions. For more information on file uploads, including progress tracking, see the [file uploads documentation](/v3/the-basics/file-uploads). diff --git a/v3/the-basics/links.mdx b/v3/the-basics/links.mdx index 69fa3ea..0d06767 100644 --- a/v3/the-basics/links.mdx +++ b/v3/the-basics/links.mdx @@ -101,7 +101,7 @@ import { inertia, Link } from '@inertiajs/svelte' ## Wayfinder -When using [Wayfinder](https://github.com/laravel/wayfinder) in conjunction with the `Link`component, you can simply pass the resulting object directly to the `href` prop. The `Link` will infer the HTTP method and URL directly from the Wayfinder object. +When using [Wayfinder](https://github.com/laravel/wayfinder) in conjunction with the `Link` component, you can simply pass the resulting object directly to the `href` prop. The `Link` will infer the HTTP method and URL directly from the Wayfinder object. diff --git a/v3/the-basics/redirects.mdx b/v3/the-basics/redirects.mdx index e10f67c..3d0749c 100644 --- a/v3/the-basics/redirects.mdx +++ b/v3/the-basics/redirects.mdx @@ -32,7 +32,7 @@ class UsersController extends Controller ## 303 Response Code -When redirecting after a `PUT`, `PATCH`, or `DELETE` request, you must use a `303` response code, otherwise the subsequent request will not be treated as a `GET`request. A `303` redirect is very similar to a `302` redirect; however, the follow-up request is explicitly changed to a `GET` request. +When redirecting after a `PUT`, `PATCH`, or `DELETE` request, you must use a `303` response code, otherwise the subsequent request will not be treated as a `GET` request. A `303` redirect is very similar to a `302` redirect; however, the follow-up request is explicitly changed to a `GET` request. If you're using one of our official server-side adapters, all redirects will automatically be converted to `303` redirects. diff --git a/v3/the-basics/routing.mdx b/v3/the-basics/routing.mdx index eccc0df..e320bf5 100644 --- a/v3/the-basics/routing.mdx +++ b/v3/the-basics/routing.mdx @@ -65,7 +65,7 @@ When [server-side rendering](/v3/advanced/server-side-rendering) is enabled, you The [page object](/v3/core-concepts/the-protocol#the-page-object) includes a `url` that represents the current page's URL. By default, the Laravel adapter resolves this using the `fullUrl()` method on the `Request` instance, but strips the scheme and host so the result is a relative URL. -If you need to customize how the URL is resolved, you may provide a resolver within the `urlResolver`method of the Inertia `HandleInertiaRequests` middleware. +If you need to customize how the URL is resolved, you may provide a resolver within the `urlResolver` method of the Inertia `HandleInertiaRequests` middleware. ```php class HandleInertiaRequests extends Middleware diff --git a/v3/the-basics/validation.mdx b/v3/the-basics/validation.mdx index f13ee1c..03f8a54 100644 --- a/v3/the-basics/validation.mdx +++ b/v3/the-basics/validation.mdx @@ -189,7 +189,7 @@ So, the only work remaining is to display any validation errors using the `error If you're using the [form helper](/v3/the-basics/forms#form-helper), it's not necessary to use error bags since validation errors are automatically scoped to the form object making the request. -For pages that have more than one form, it's possible to encounter conflicts when displaying validation errors if two forms share the same field names. For example, imagine a "create company" form and a "create user" form that both have a `name` field. Since both forms will be displaying the `page.props.errors.name` validation error, generating a validation error for the `name`field in either form will cause the error to appear in both forms. +For pages that have more than one form, it's possible to encounter conflicts when displaying validation errors if two forms share the same field names. For example, imagine a "create company" form and a "create user" form that both have a `name` field. Since both forms will be displaying the `page.props.errors.name` validation error, generating a validation error for the `name` field in either form will cause the error to appear in both forms. To solve this issue, you can use "error bags". Error bags scope the validation errors returned from the server within a unique key specific to that form. Continuing with our example above, you might have a `createCompany` error bag for the first form and a `createUser` error bag for the second form. From 14e55469d39b6298b279ee625575b1ac21aefd84 Mon Sep 17 00:00:00 2001 From: Pascal Baljet Date: Wed, 11 Feb 2026 17:32:40 +0100 Subject: [PATCH 06/27] wip --- v3/getting-started/upgrade-guide.mdx | 21 ++++----------------- v3/installation/client-side-setup.mdx | 17 ----------------- 2 files changed, 4 insertions(+), 34 deletions(-) diff --git a/v3/getting-started/upgrade-guide.mdx b/v3/getting-started/upgrade-guide.mdx index 2275144..90c9ce8 100644 --- a/v3/getting-started/upgrade-guide.mdx +++ b/v3/getting-started/upgrade-guide.mdx @@ -121,11 +121,12 @@ router.post('/users', data, { ### Future Options Removed -The `future` configuration namespace has been replaced by `legacy`, with inverted logic. Three future options have been removed entirely as their behavior is now always enabled: +The `future` configuration namespace has been removed. All four future options from v2 are now always enabled and no longer configurable: - `future.preserveEqualProps` - `future.useDataInertiaHeadAttribute` - `future.useDialogForErrorModal` +- `future.useScriptElementForInitialPage` ```js // Before (v2) @@ -135,6 +136,7 @@ createInertiaApp({ preserveEqualProps: true, useDataInertiaHeadAttribute: true, useDialogForErrorModal: true, + useScriptElementForInitialPage: true, }, }, }) @@ -145,19 +147,7 @@ createInertiaApp({ }) ``` -The fourth option, `future.useScriptElementForInitialPage`, has been flipped to `legacy.useDataAttributeForInitialPage`. The script element is now the default, so you only need this option if you want to revert to the old data attribute behavior: - -```js -createInertiaApp({ - defaults: { - legacy: { - useDataAttributeForInitialPage: true, - }, - }, -}) -``` - -Laravel users should also set the `pages.use_data_attribute_for_initial_page` option to `true` in their `config/inertia.php` file so the `@inertia` Blade directive outputs a data attribute instead of a script element. +Initial page data is now always passed via a ` -
    + +
    ``` @@ -265,7 +266,7 @@ Inertia shares data between the server and client via a page object. This object milliseconds). -On standard full page visits, the page object is JSON encoded into the `data-page` attribute in the root `
    `. On Inertia visits (as indicated by the presence of the `X-Inertia` header), the page object is returned as the JSON payload. +On standard full page visits, the page object is JSON encoded into a `