Skip to content

Commit

Permalink
feat: add deferred props
Browse files Browse the repository at this point in the history
  • Loading branch information
Julien-R44 committed Oct 10, 2024
1 parent 6487515 commit b1ff5d8
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 6 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"typecheck": "tsc --noEmit",
"lint": "eslint",
"format": "prettier --write .",
"quick:test": "node --import=ts-node-maintained/register/esm bin/test.ts",
"quick:test": "node --enable-source-maps --import=ts-node-maintained/register/esm bin/test.ts",
"pretest": "npm run lint",
"test": "c8 npm run quick:test",
"prebuild": "npm run lint && npm run clean",
Expand Down
77 changes: 72 additions & 5 deletions src/inertia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ import type {
*/
export const kLazySymbol = Symbol('lazy')

/**
* Symbol used to identify deferred props
*/
export const kDeferredSymbol = Symbol('deferred')

/**
* Main class used to interact with Inertia
*/
Expand All @@ -50,6 +55,20 @@ export class Inertia {
return typeof value === 'object' && value && kLazySymbol in value
}

/**
* Check if a value is a deferred prop
*/
#isDeferredProps(value: any) {
return typeof value === 'object' && value && kDeferredSymbol in value
}

/**
* Check if the current request is a partial request
*/
#isPartial(component: string) {
return this.ctx.request.header('x-inertia-partial-component') === component
}

/**
* Pick props to resolve based on x-inertia-partial-data header
*
Expand All @@ -62,13 +81,13 @@ export class Inertia {
?.split(',')
.filter(Boolean)

const partialComponent = this.ctx.request.header('x-inertia-partial-component')

let entriesToResolve = Object.entries(props)
if (partialData && partialComponent === component) {
if (this.#isPartial(component) && partialData) {
entriesToResolve = entriesToResolve.filter(([key]) => partialData.includes(key))
} else {
entriesToResolve = entriesToResolve.filter(([key]) => !this.#isLazyProps(props[key]))
entriesToResolve = entriesToResolve.filter(
([key]) => !this.#isLazyProps(props[key]) && !this.#isDeferredProps(props[key])
)
}

return entriesToResolve
Expand All @@ -85,17 +104,52 @@ export class Inertia {
return [key, await value(this.ctx)]
}

/**
* Resolve lazy props
*/
if (this.#isLazyProps(value)) {
const lazyValue = (value as any)[kLazySymbol]
return [key, await lazyValue()]
}

/**
* Resolve deferred props
*/
if (this.#isDeferredProps(value)) {
const { callback } = (value as any)[kDeferredSymbol]
return [key, await callback()]
}

return [key, value]
})

return Object.fromEntries(await Promise.all(entries))
}

/**
* Resolve the deferred props listing. Will be returned only
* on the first visit to the page and will be used to make
* subsequent partial requests
*/
#resolveDeferredProps(component: string, pageProps?: PageProps) {
if (this.#isPartial(component)) return {}

const deferredProps = Object.entries(pageProps || {})
.filter(([_, value]) => this.#isDeferredProps(value))
.map(([key, value]) => ({ key, group: (value as any)[kDeferredSymbol].group }))
.reduce(
(groups, { key, group }) => {
if (!groups[group]) groups[group] = []

groups[group].push(key)
return groups
},
{} as Record<string, string[]>
)

return deferredProps
}

/**
* Build the page object that will be returned to the client
*
Expand All @@ -107,9 +161,10 @@ export class Inertia {
): Promise<PageObject<TPageProps>> {
return {
component,
url: this.ctx.request.url(true),
version: this.config.versionCache.getVersion(),
deferredProps: this.#resolveDeferredProps(component, pageProps),
props: await this.#resolvePageProps(component, { ...this.#sharedData, ...pageProps }),
url: this.ctx.request.url(true),
}
}

Expand Down Expand Up @@ -200,6 +255,18 @@ export class Inertia {
return { [kLazySymbol]: callback }
}

/**
* Create a deferred prop
*
* Deferred props feature allows you to defer the loading of certain
* page data until after the initial page render.
*
* See https://v2.inertiajs.com/deferred-props
*/
defer<T>(callback: () => MaybePromise<T>, group = 'default') {
return { [kDeferredSymbol]: { callback, group } }
}

/**
* This method can be used to redirect the user to an external website
* or even a non-inertia route of your application.
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export interface PageObject<TPageProps extends PageProps = PageProps> {
url: string
ssrHead?: string
ssrBody?: string
deferredProps?: Record<string, string[]>
}

type IsLazyProp<T> = T extends { [kLazySymbol]: () => MaybePromise<any> } ? true : false
Expand Down
66 changes: 66 additions & 0 deletions tests/inertia.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,72 @@ test.group('Inertia', () => {

assert.deepEqual(result.props, { foo: 'baz' })
})

test('dont execute deferred props on first visit', async ({ assert }) => {
setupViewMacroMock()

const inertia = await new InertiaFactory().create()
let executed = false

await inertia.render('foo', {
foo: 'bar',
baz: inertia.defer(() => {
executed = true
return 'baz'
}),
})

assert.deepEqual(executed, false)
})

test('deferred props listing are returned in page object', async ({ assert }) => {
setupViewMacroMock()

const inertia = await new InertiaFactory().create()

const result: any = await inertia.render('foo', {
foo: 'bar',
baz: inertia.defer(() => 'baz'),
qux: inertia.defer(() => 'qux'),
})

assert.deepEqual(result.props.page.deferredProps, {
default: ['baz', 'qux'],
})
})

test('deferred props groups are respected', async ({ assert }) => {
setupViewMacroMock()

const inertia = await new InertiaFactory().create()

const result: any = await inertia.render('foo', {
foo: 'bar',
baz: inertia.defer(() => 'baz', 'group1'),
qux: inertia.defer(() => 'qux', 'group2'),
lorem: inertia.defer(() => 'lorem', 'group1'),
ipsum: inertia.defer(() => 'ipsum', 'group2'),
})

assert.deepEqual(result.props.page.deferredProps, {
group1: ['baz', 'lorem'],
group2: ['qux', 'ipsum'],
})
})

test('execute and return deferred props on partial reload', async ({ assert }) => {
const inertia = await new InertiaFactory()
.withXInertiaHeader()
.withInertiaPartialReload('foo', ['baz'])
.create()

const result: any = await inertia.render('foo', {
foo: 'bar',
baz: inertia.defer(() => 'baz'),
})

assert.deepEqual(result.props, { baz: 'baz' })
})
})

test.group('Inertia | Ssr', () => {
Expand Down

1 comment on commit b1ff5d8

@afgallo
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.