Loading...
Loading...
Page components, persistent layouts, Link/router navigation, Head, Deferred, WhenVisible, InfiniteScroll, and URL-driven state for Inertia Rails. React examples inline; Vue and Svelte equivalents in references. Use when building pages, adding navigation, implementing persistent layouts, infinite scroll, lazy-loaded sections, or working with client-side Inertia APIs (router.reload, router.replaceProp, prefetching).
npx skill4agent add inertia-rails/skills inertia-rails-pagesPage.layout = ...defineOptions({ layout })paramsuseStateuseEffectrouter.getrouter.reload({ only: [...] })useEffectfetchrouter.replacePropwindow.location.searchuseSearchParamsuseStateuseEffect<Deferred>{(data) => ...}usePage()usePage().props.flashusePage().flashPage.layout = ...type Props = { ... }interfacedefineProps<T>()let { ... } = $props()type Props = {
posts: Post[]
}
export default function Index({ posts }: Props) {
return <PostList posts={posts} />
}import { AppLayout } from '@/layouts/app-layout'
export default function Show({ course }: Props) {
return <CourseContent course={course} />
}
// Single layout
Show.layout = (page: React.ReactNode) => <AppLayout>{page}</AppLayout>// app/frontend/entrypoints/inertia.tsx
resolve: async (name) => {
const page = await pages[`../pages/${name}.tsx`]()
page.default.layout ??= (page: React.ReactNode) => <AppLayout>{page}</AppLayout> // default if not set
return page
}<Link>router<Link href="..."><a>router.get/post/patch/delete// Prefetching — preloads page data on hover
<Link href="/users" prefetch>Users</Link>
<Link href="/users" prefetch cacheFor="30s">Users</Link>
// Prefetch with cache tags — invalidate after mutations
<Link href="/users" prefetch cacheTags="users">Users</Link>
// Programmatic prefetch (e.g., likely next destination)
router.prefetch('/settings', {}, { cacheFor: '1m' })
// Partial reload — refresh specific props without navigation
router.reload({ only: ['users'] })routerreferences/navigation.md// Replace a single prop (dot notation supported)
router.replaceProp('show_modal', false)
router.replaceProp('user.name', 'Jane Smith')
// With callback (receives current value + all props)
router.replaceProp('count', (current) => current + 1)
// Append/prepend to array props
router.appendToProp('messages', { id: 4, text: 'New' })
router.prependToProp('notifications', (current, props) => ({
id: Date.now(),
message: `Hello ${props.auth.user.name}`,
}))router.replace()preserveScrollpreserveStatetruerouter.replaceProprouter.reloadrouter.replaceProprouter.reloadparamsuseStateuseEffectrouter.getuseStateuseEffect# Step 1: Controller reads params, passes as prop
def index
render inertia: {
users: User.all,
selected_user_id: params[:user_id]&.to_i
}
end// Step 2+3: Derive state from props, router.get to update URL
type Props = {
users: User[]
selected_user_id: number | null // from controller
}
export default function Index({ users, selected_user_id }: Props) {
// Derive — no useState, no useEffect, no window.location parsing
const selectedUser = selected_user_id
? users.find(u => u.id === selected_user_id)
: null
const openDialog = (id: number) =>
router.get('/users', { user_id: id }, {
preserveState: true,
preserveScroll: true,
})
const closeDialog = () =>
router.get('/users', {}, {
preserveState: true,
preserveScroll: true,
})
return (
<Dialog open={!!selectedUser} onOpenChange={(open) => !open && closeDialog()}>
<DialogContent>{/* ... */}</DialogContent>
</Dialog>
)
}router.get('/users', { user_id: 5 })params[:user_id] = 5selected_user_id: 5window.locationinertia-rails-typescripttype Props = {
users: User[] // page-specific only
// auth is NOT here — typed globally via InertiaConfig
}
export default function Index({ users }: Props) {
const { props, flash } = usePage()
// props.auth typed via InertiaConfig, flash.notice typed via InertiaConfig
return <UserList users={users} />
}inertia-rails-controllersshadcn-inertia// BAD: usePage().props.flash ← WRONG, flash is not in props
// GOOD: usePage().flash ← flash.notice, flash.alert<Deferred>ReactNode() => ReactNodeusePage()import { Deferred } from '@inertiajs/react'
export default function Dashboard({ basic_stats }: Props) {
return (
<>
<QuickStats data={basic_stats} />
<Deferred data="detailed_stats" fallback={<Spinner />}>
<DetailedStats />
</Deferred>
</>
)
}
// Also valid — render function (no args, child still reads from usePage):
// <Deferred data="stats" fallback={<Spinner />}>
// {() => <Stats />}
// </Deferred>
// BAD — render function does NOT receive data as argument:
// <Deferred data="stats">{(data) => <Stats data={data} />}</Deferred><InfiniteScroll>InertiaRails.scrollinertia-rails-controllersimport { InfiniteScroll } from '@inertiajs/react'
export default function Index({ posts }: Props) {
return (
<InfiniteScroll data="posts" loading={() => <Spinner />}>
{posts.map(post => <PostCard key={post.id} post={post} />)}
</InfiniteScroll>
)
}dataloadingmanualmanualAfter={3}preserveUrl<WhenVisible><InfiniteScroll>import { WhenVisible } from '@inertiajs/react'
<WhenVisible data="comments" fallback={<Spinner />}>
<CommentsList />
</WhenVisible>| Symptom | Cause | Fix |
|---|---|---|
| Layout remounts on every navigation | Wrapping layout in JSX return instead of | Use persistent layout |
| Render function expects args | Render function receives NO arguments — use |
Flash is | Accessing | Flash is top-level: |
| URL state lost on navigation | Parsing | Derive from props — controller reads |
| Element not in viewport or prop name wrong | |
Component state resets on | Missing | Add |
| Scroll jumps to top after form submit | Missing | Add |
inertia-rails-controllersshadcn-inertiainertia-rails-typescriptinertia-rails-controllersshadcn-inertiareferences/vue.mddefinePropsusePage()<Deferred><WhenVisible><InfiniteScroll>defineOptions({ layout })references/svelte.md$props()$page{#snippet}<Deferred><WhenVisible><InfiniteScroll><svelte:head><Head>onBeforeonStartonProgressonFinishonCancelreferences/navigation.mdrouter.flash()references/layouts.md<Link>router.visit