inertia-rails-pages

安装量: 59
排名: #12544

安装

npx skills add https://github.com/inertia-rails/skills --skill inertia-rails-pages

Inertia Rails Pages Page components, layouts, navigation, and client-side APIs. Before building a page, ask: Does this page need a layout? → Use persistent layout (React: Page.layout = ... ; Vue: defineOptions({ layout }) ; Svelte: module script export) — wrapping in JSX/template remounts on every navigation, losing scroll position, audio playback, and component state Does UI state come from the URL? → Change BOTH controller (read params , pass as prop) AND component (derive from prop, no useState / useEffect ) — use router.get to update URL Need to refresh data without navigation? → router.reload({ only: [...] }) — never useEffect + fetch Need to update a prop without server round-trip? → router.replaceProp — no fetch, no reload NEVER: Parse window.location.search or use useSearchParams — derive URL state from controller props Use useState / useEffect to sync URL ↔ React state — the controller passes URL-derived data as props; the component just reads them Pass arguments to render function — {(data) => ...} does NOT work; child reads via usePage() Access usePage().props.flash — flash is top-level: usePage().flash Wrap layout in JSX return for persistence — use Page.layout = ... or global layout inside createInertiaApp's resolve callback Page Component Structure Pages are default exports receiving controller props as function arguments. Use type Props = { ... } (not interface — causes TS2344 in React). Vue uses defineProps() , Svelte uses let { ... } = $props() . type Props = { posts : Post [ ] } export default function Index ( { posts } : Props ) { return < PostList posts = { posts } /> } Persistent Layouts Layouts persist across navigations — no remounting, preserving scroll, audio, etc. 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

Default layout in entrypoint: // 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 } Navigation <Link> and router Use <Link href="..."> for internal navigation (not ) and router.get/post/patch/delete for programmatic navigation. Key non-obvious features: // 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' ] } ) Full router API, visit options, and event callbacks are in references/navigation.md — see loading trigger below. Client-Side Prop Helpers Update props without a server round-trip: // 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 } , } ) ) These are shortcuts to router.replace() with preserveScroll and preserveState automatically set to true . router.replaceProp vs router.reload : Use router.replaceProp for client-only state changes (toggling a modal, incrementing a counter) — no server round-trip. Use router.reload when you need fresh data from the server (updated records, recalculated stats). URL-Driven State (Dialogs, Tabs, Filters) URL state = server state = props. ALWAYS implement both sides: Controller — read params and pass as a prop Component — derive UI state from that prop (no useState , no useEffect ) Update — router.get with query params to change URL (triggers server round-trip, new props arrive) NEVER use useState + useEffect to sync URL ↔ dialog/tab/filter state. The server is the single source of truth — the component just reads props.

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

) } Why not useEffect? When router.get('/users', { user_id: 5 }) fires, Inertia makes a request to the server → controller runs with params[:user_id] = 5 → returns new props with selected_user_id: 5 → component re-renders with the dialog open. The cycle is: URL → server → props → render. Parsing window.location client-side duplicates what the server already does. Shared Props Shared props (auth, flash) are typed globally via InertiaConfig (see inertia-rails-typescript skill) — page components only type their OWN props: type 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 } /> } Flash Access Flash is top-level on the page object, NOT inside props — this is the #1 flash mistake. Flash config is in inertia-rails-controllers ; toast UI is in shadcn-inertia . // BAD: usePage().props.flash ← WRONG, flash is not in props // GOOD: usePage().flash ← flash.notice, flash.alert Component Renders fallback until deferred props arrive. Children can be plain ReactNode or () => ReactNode render function. Either way, the child reads the deferred prop from page props via usePage() — the render function receives no arguments . 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): // }> // {() => } // // BAD — render function does NOT receive data as argument: // {(data) => } Component Automatic infinite scroll — loads next pages as user scrolls down. Pairs with InertiaRails.scroll on the server (see inertia-rails-controllers ): import { 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

) } Props: data (prop name), loading (fallback), manual (button instead of auto), manualAfter={3} (auto for first 3 pages, then button), preserveUrl (don't update URL). Component Loads data when element enters viewport. Use for lazy sections (comments, related items), NOT for infinite scroll (use above): import { WhenVisible } from '@inertiajs/react' < WhenVisible data = " comments " fallback = { < Spinner /> }

< CommentsList /> </ WhenVisible

Troubleshooting Symptom Cause Fix Layout remounts on every navigation Wrapping layout in JSX return instead of Page.layout Use persistent layout Deferred children never render Render function expects args {(data) => ...} Render function receives NO arguments — use {() => } or plain . Child reads prop via usePage() Flash is undefined Accessing usePage().props.flash Flash is top-level: usePage().flash , not inside props URL state lost on navigation Parsing window.location in useEffect Derive from props — controller reads params and passes as prop WhenVisible never triggers Element not in viewport or prop name wrong data must match a prop name the controller provides on partial reload Component state resets on router.get Missing preserveState: true Add preserveState: true to visit options for filter/sort/tab changes Scroll jumps to top after form submit Missing preserveScroll Add preserveScroll: true to the visit or form options

返回排行榜