Skip to content
8 changes: 6 additions & 2 deletions packages/core/src/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { hrefToUrl, isSameUrlWithoutHash } from './url'

class CurrentPage {
protected page!: Page
protected swapComponent!: PageHandler
protected swapComponent!: PageHandler<any>
protected resolveComponent!: PageResolver
protected componentId = {}
protected listeners: {
Expand All @@ -18,7 +18,11 @@ class CurrentPage {
protected cleared = false
protected pendingDeferredProps: Pick<Page, 'deferredProps' | 'url' | 'component'> | null = null

public init({ initialPage, swapComponent, resolveComponent }: RouterInitParams) {
public init<ComponentType = Component>({
initialPage,
swapComponent,
resolveComponent,
}: RouterInitParams<ComponentType>) {
this.page = initialPage
this.swapComponent = swapComponent
this.resolveComponent = resolveComponent
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ export class Router {
interruptible: false,
})

public init({ initialPage, resolveComponent, swapComponent }: RouterInitParams): void {
public init<ComponentType = Component>({
initialPage,
resolveComponent,
swapComponent,
}: RouterInitParams<ComponentType>): void {
currentPage.init({
initialPage,
resolveComponent,
Expand Down
48 changes: 43 additions & 5 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,12 +162,12 @@ export interface ClientSideVisitOptions<TProps = Page['props']> {

export type PageResolver = (name: string) => Component

export type PageHandler = ({
export type PageHandler<ComponentType = Component> = ({
component,
page,
preserveState,
}: {
component: Component
component: ComponentType
page: Page
preserveState: boolean
}) => Promise<unknown>
Expand Down Expand Up @@ -363,10 +363,10 @@ export type PollOptions = {

export type VisitHelperOptions<T extends RequestPayload = RequestPayload> = Omit<VisitOptions<T>, 'method' | 'data'>

export type RouterInitParams = {
export type RouterInitParams<ComponentType = Component> = {
initialPage: Page
resolveComponent: PageResolver
swapComponent: PageHandler
swapComponent: PageHandler<ComponentType>
}

export type PendingVisitOptions = {
Expand All @@ -388,7 +388,45 @@ export type InternalActiveVisit = ActiveVisit & {
export type VisitId = unknown
export type Component = unknown

export type InertiaAppResponse = Promise<{ head: string[]; body: string } | void>
interface CreateInertiaAppOptions<TComponentResolver, TSetupOptions, TSetupReturn> {
resolve: TComponentResolver
setup: (options: TSetupOptions) => TSetupReturn
title?: HeadManagerTitleCallback
}

export interface CreateInertiaAppOptionsForCSR<
SharedProps extends PageProps,
TComponentResolver,
TSetupOptions,
TSetupReturn,
> extends CreateInertiaAppOptions<TComponentResolver, TSetupOptions, TSetupReturn> {
id?: string
page?: Page<SharedProps>
progress?:
| false
| {
delay?: number
color?: string
includeCSS?: boolean
showSpinner?: boolean
}
render?: undefined
}

export interface CreateInertiaAppOptionsForSSR<
SharedProps extends PageProps,
TComponentResolver,
TSetupOptions,
TSetupReturn,
> extends CreateInertiaAppOptions<TComponentResolver, TSetupOptions, TSetupReturn> {
id?: undefined
page: Page<SharedProps>
progress?: undefined
render: unknown
}

export type InertiaAppSSRResponse = { head: string[]; body: string }
export type InertiaAppResponse = Promise<InertiaAppSSRResponse | void>

export type HeadManagerTitleCallback = (title: string) => string
export type HeadManagerOnUpdateCallback = (elements: string[]) => void
Expand Down
60 changes: 43 additions & 17 deletions packages/react/src/App.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,52 @@
import { createHeadManager, PageHandler, router } from '@inertiajs/core'
import { createElement, useEffect, useMemo, useState } from 'react'
import {
createHeadManager,
HeadManagerOnUpdateCallback,
HeadManagerTitleCallback,
Page,
PageHandler,
PageProps,
router,
} from '@inertiajs/core'
import { createElement, FunctionComponent, ReactNode, useEffect, useMemo, useState } from 'react'
import HeadContext from './HeadContext'
import PageContext from './PageContext'
import { LayoutFunction, ReactComponent, ReactPageHandlerArgs } from './types'

let currentIsInitialPage = true
let routerIsInitialized = false
let swapComponent: PageHandler = async () => {
let swapComponent: PageHandler<ReactComponent> = async () => {
// Dummy function so we can init the router outside of the useEffect hook. This is
// needed so `router.reload()` works right away (on mount) in any of the user's
// components. We swap in the real function in the useEffect hook below.
currentIsInitialPage = false
}

export default function App({
type CurrentPage = {
component: ReactComponent | null
page: Page
key: number | null
}

export interface InertiaAppProps<SharedProps extends PageProps = PageProps> {
children?: (options: { component: ReactComponent; props: PageProps; key: number | null }) => ReactNode
initialPage: Page<SharedProps>
initialComponent?: ReactComponent
resolveComponent?: (name: string) => ReactComponent | Promise<ReactComponent>
titleCallback?: HeadManagerTitleCallback
onHeadUpdate?: HeadManagerOnUpdateCallback
}

export type InertiaApp = FunctionComponent<InertiaAppProps>

export default function App<SharedProps extends PageProps = PageProps>({
children,
initialPage,
initialComponent,
resolveComponent,
titleCallback,
onHeadUpdate,
}) {
const [current, setCurrent] = useState({
}: InertiaAppProps<SharedProps>) {
const [current, setCurrent] = useState<CurrentPage>({
component: initialComponent || null,
page: initialPage,
key: null,
Expand All @@ -35,17 +61,17 @@ export default function App({
}, [])

if (!routerIsInitialized) {
router.init({
router.init<ReactComponent>({
initialPage,
resolveComponent,
resolveComponent: resolveComponent!,
swapComponent: async (args) => swapComponent(args),
})

routerIsInitialized = true
}

useEffect(() => {
swapComponent = async ({ component, page, preserveState }) => {
swapComponent = async ({ component, page, preserveState }: ReactPageHandlerArgs) => {
if (currentIsInitialPage) {
// We block setting the current page on the initial page to
// prevent the initial page from being re-rendered again.
Expand Down Expand Up @@ -73,18 +99,18 @@ export default function App({

const renderChildren =
children ||
(({ Component, props, key }) => {
const child = createElement(Component, { key, ...props })
(({ component, props, key }) => {
const child = createElement(component, { key, ...props })

if (typeof Component.layout === 'function') {
return Component.layout(child)
if (typeof component.layout === 'function') {
return (component.layout as LayoutFunction)(child)
}

if (Array.isArray(Component.layout)) {
return Component.layout
if (Array.isArray(component.layout)) {
return (component.layout as any)
.concat(child)
.reverse()
.reduce((children, Layout) => createElement(Layout, { children, ...props }))
.reduce((children: any, Layout: any) => createElement(Layout, { children, ...props }))
}

return child
Expand All @@ -97,7 +123,7 @@ export default function App({
PageContext.Provider,
{ value: current.page },
renderChildren({
Component: current.component,
component: current.component,
key: current.key,
props: current.page.props,
}),
Expand Down
Loading