Skip to content

feat(inertia): add Shared Data support - #2124

Merged
yusukebe merged 9 commits into
honojs:mainfrom
nkfr26:fix/inertia-shared-data
Sep 20, 2026
Merged

yusukebe merged 9 commits into
honojs:mainfrom
nkfr26:fix/inertia-shared-data

Conversation

@nkfr26

@nkfr26 nkfr26 commented Sep 5, 2026 •

Copy link
Copy Markdown
Contributor

Overview

Add a share option to inertia() so that props shared across pages can be configured with a synchronous callback. Asynchronous values can still use the existing lazy function props mechanism.

Changes

feat(inertia): add shared props through a share callback

  • Accept a synchronous callback that receives the current Hono context and returns shared props
  • Combine shared props with page props, with page-specific props taking precedence when keys overlap
  • Process shared props in the same way as props passed to c.render(), including lazy function props
  • Infer shared prop types from the return value of share and include them in PageProps
  • Add sharedProps page metadata containing the top-level keys returned by share

fix(inertia): correct the c.render() return type

  • Update the c.render() type to Response | Promise<Response> and add tests for both return paths

Tests and documentation

  • Add runtime and type tests for shared props, partial reloads, sharedProps, and c.render() return values
  • Update the README with shared data, type inference, and sharedProps documentation

Inferring types from the share callback return value

import { Hono, type Context } from 'hono'
import { inertia } from '@hono/inertia'

type Session = { user: { name: string } }
type SessionEnv = { Variables: { session: Session | null } }

const app = new Hono<SessionEnv>()

const route = app
  .use((c, next) => {
    c.set('session', { user: { name: 'John Doe' } })
    await next()
  })
  .use(
    inertia({
      share: (c: Context<SessionEnv>) => ({
        session: c.get('session'),
      }),
    })
  )

The shared props type is inferred from the return value of the share callback:

{ session: Session | null }

The inferred type is passed to the inertia middleware through Env and reflected in the page props of c.render(). The shared props type does not need to be specified separately. Type tests use PagePropsFor to verify page props for each app without relying on the global AppRegistry.

Shared props for apps mounted with .route()

When another app (sub-app) is mounted on a parent app with .route(), the inertia({ share }) configured on the sub-app works at runtime. However, the share type defined in the sub-app's middleware is not propagated to the parent app's PageProps.

To include the sub-app's shared data in PageProps, the data must be passed explicitly as props to c.render(). provide is a middleware helper that stores the shared data in the Context and makes it reusable from each handler.

In the non-curried form, Context<SubEnv> is explicitly specified as the provider parameter, as in provide('shared', (c: Context<SubEnv>) => ...). In the curried form, provide<SubEnv>() provides the type for the provider's Context.

Example implementation of `provide`
import type { Context, Env, MiddlewareHandler } from 'hono'

type ProvidedEnv<K extends PropertyKey, V> = {
  Variables: Record<K, V>
}

type Provider<E extends Env, V> = (c: Context<E>) => V | Promise<V>

type Provide<E extends Env> = <K extends PropertyKey, V>(
  key: K,
  provider: Provider<E, V>
) => MiddlewareHandler<ProvidedEnv<K, V>>

// provide(key, provider)
function provide<E extends Env, K extends PropertyKey, V>(
  key: K,
  provider: Provider<E, V>
): MiddlewareHandler<ProvidedEnv<K, V>>

// provide()
function provide<E extends Env>(): Provide<E>

function provide<E extends Env, K extends PropertyKey, V>(
  ...args: [] | [K, Provider<E, V>]
): Provide<E> | MiddlewareHandler<ProvidedEnv<K, V>> {
  const create: Provide<E> = (key, provider) => async (c, next) => {
    c.set(key, await provider(c as unknown as Context<E>))
    return next()
  }

  if (args.length === 0) {
    return create
  }

  return create(...args)
}
type Session = { user: { name: string } }
type SubEnv = { Variables: { session: Session | null } }

const sub = new Hono<SubEnv>()
  .use((c, next) => {
    c.set('session', { user: { name: 'John Doe' } })
    await next()
  })
  .use(
    provide<SubEnv>()('shared', (c) => ({
      session: c.get('session'),
    }))
  )
  .get('/dashboard', (c) => {
    return c.render('Sub/Dashboard', {
      ...c.get('shared'),
      message: 'sub',
    })
  })

const app = new Hono<SessionEnv>()

const route = app.use(inertia()).route('/sub', sub)

Since provide is a generic middleware helper that does not affect Inertia's functionality, a PoC PR is planned for the main Hono repository.

Curried form

By using currying, Context does not need to be imported; inertia<SessionEnv>() types the Context passed to the share callback.
The curried form is being developed on a separate branch. If adopted, it will either be included in this PR or split into a separate PR.

URL: nkfr26@8feaef5

type Session = { user: { id: string } }
type SessionEnv = { Variables: { session: Session | null } }

const app = new Hono<SessionEnv>().use(
  inertia<SessionEnv>()({
    share: (c) => ({
      session: c.get('session'),
    }),
  })
)

The author should do the following, if applicable

  • Add tests
  • Run tests
  • pnpm changeset at the top of this repo and push the changeset
  • Follow the contribution guide

@changeset-bot

changeset-bot Bot commented Sep 5, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 510ab6c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@hono/inertia Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@nkfr26 nkfr26 changed the title feat(inertia): add shared data support feat(inertia): add Shared Data support Sep 5, 2026
@nkfr26
nkfr26 marked this pull request as ready for review September 5, 2026 12:50
@ashunar0

ashunar0 commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Thanks for taking this on! I read through it — this is in good shape, and the type approach in particular turned out better than what I had in mind.

On the Env marker approach

In the issue I proposed ③ as a SharedProps registry augmented via declare module. I think the InertiaSharedEnv approach in this PR is the better one: users don't have to write the type a second time, which is a clear win. Putting a unique symbol in Variables does make hover output a little noisier, but I'd take the ergonomics over that. Let's go this direction.

Below are the things I noticed, heaviest first.

1. c.render() starts returning a Promise (needs discussion)

Splitting this one out because it's different in kind from the rest.

-    ): Response & TypedResponse<...>
+    ): Promise<Response & TypedResponse<...>>

The renderer currently keeps a needsAsync flag and stays fully synchronous — returning a plain Response — when there are no function props and no deferred props. This PR turns every c.render() into a Promise, including for users who never touch share. That's a change to a public type, and it feels large for a minor.

Proposal: what if share were restricted to a synchronous callback?

share() in inertia-laravel is synchronous too, and async values can already be expressed with the existing lazy function props:

inertia({
  share: (c) => ({
    appName: 'App Name',
    flash: () => getFlash(c), // goes through the existing resolution path
  }),
})

That keeps { ...share(c), ...propsInput } synchronous and preserves the fast path entirely. As a side benefit, a shared prop that a partial reload filtered out no longer gets computed — with the current implementation the whole share callback runs on every render regardless.

I also considered resolving share in the middleware body before next(), but that would prevent share from reading anything a downstream middleware c.set()s, so I don't think it works. Laravel resolves shared props at response time for the same reason.

2. Dependency on always()

This test is doing exactly what it says:

it('filters shared props during a partial reload', ...)

The reason that isn't a problem in Laravel is always(). HandleInertiaRequests::share() wraps errors in Inertia::always() precisely for this. Without always(), landing share on its own means auth / errors / flash disappear on every partial reload with no way out.

I've opened ① as #2125. Once that lands, the share examples here could use errors: always(...).

3. page.sharedProps

Noted that it isn't included yet — we're on the same page. Since it's part of the protocol, I'd just like to settle whether it goes in this PR or a follow-up. My inclination is this PR: sharedPropKeys is only the top-level keys of the share return value, so it should be a small addition.

4. provide is out of scope for this PR

Thanks for digging into the .route() case where the type doesn't propagate to the parent app. That said, provide is a general-purpose helper independent of Inertia, so I'd leave it out of this PR and just document the limitation in the README. You mentioned a PoC PR against Hono core — let's let that conversation settle first.

5. Deciding on the curried form

Given TypeScript has no partial type-argument inference, inertia<E>()({ share }) is a reasonable workaround. My concern is that having it alongside the explicitly annotated share: (c: Context<E>) => ... form leaves us with two API shapes. I'd like to settle which one the README presents as the recommended way. I lean toward the curried form since it avoids importing Context, but a middleware factory that returns a curried function isn't a shape Hono's other middleware use much, so I'd like @yusukebe's read on that too.

6. Typo

SissionEnv in the README looks like it should be SessionEnv (two occurrences).

@codecov

codecov Bot commented Sep 8, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.47%. Comparing base (efddc78) to head (510ab6c).
⚠️ Report is 15 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2124      +/-   ##
==========================================
+ Coverage   92.44%   92.47%   +0.02%     
==========================================
  Files         119      119              
  Lines        4274     4291      +17     
  Branches     1121     1124       +3     
==========================================
+ Hits         3951     3968      +17     
  Misses        288      288              
  Partials       35       35              
Flag Coverage Δ
inertia 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@nkfr26
nkfr26 force-pushed the fix/inertia-shared-data branch from 16b0f2e to a70e422 Compare September 9, 2026 21:36
@nkfr26

nkfr26 commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor Author

@ashunar0
Thank you for the review!

I applied the change because I think restricting share to a synchronous callback is a good choice, especially as it encourages the use of lazy function props. However, since c.render() returns a Promise when asynchronous resolution is required, the current type appears to be inaccurate. I changed it to Response | Promise and added tests, so please take a look. Personally, I don't think this warrants a separate PR, but the commits can be reverted if necessary.

I also added page.sharedProps and fixed the typo. This allows us to focus on deciding whether to use the curried form.

I personally also think the curried approach is better. If there are places where it could be useful in other middleware, it might be interesting to adopt it there as well.

@yusukebe

yusukebe commented Sep 9, 2026

Copy link
Copy Markdown
Member

@ashunar0 @nkfr26 Thanks!

For this middleware, I'll leave it to you two! So, if @ashunar0 's review is okay, I can merge.

@nkfr26
nkfr26 force-pushed the fix/inertia-shared-data branch from a70e422 to 744278a Compare September 9, 2026 23:36
@nkfr26

nkfr26 commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

I forgot to mention sub apps mounted with .route() in the README, though I think this is more of a general @hono/inertia concern than something specific to share.

I assume it's generally understood that this middleware is meant to be registered only on the top-level app, but do you think we should document that explicitly?

If Hono itself adds provide in the future, I think we can update the documentation then. Until then, I don't think we need to document shared data for sub apps.

@ashunar0 ashunar0 left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the quick turnaround — I've gone through everything.

  • c.render() return type: Agreed with restricting share to a synchronous callback and changing the return type to Response | Promise<Response> instead of always Promise. Verified the sync fast path is preserved when share isn't used and no props need async resolution — no regression there. No need to split this into a separate PR; keeping it here is fine.
  • page.sharedProps: Looks right. Confirmed it always reflects the full set of top-level keys from share(), even during a partial reload where most of them get filtered out of props — that matches how the client needs it for instant visits.
  • Curried form: I looked at how this landed and I think it's the right call. Since E gets inferred from an explicit type annotation on the share callback's parameter, we get the same ergonomics as a curried form without introducing a second API shape. Let's go with this.
  • Typo: Confirmed fixed.
  • .route() sub-apps: I'd leave this out of this PR's docs. The constraint isn't specific to share — it's a general @hono/inertia limitation — so it reads better as part of the provide documentation once that discussion settles, rather than bolted onto the Shared Data section here.

This looks good to me. Approving.

@nkfr26

nkfr26 commented Sep 17, 2026

Copy link
Copy Markdown
Contributor Author

@ashunar0
Thanks for the approval!

I merged the implementation of the curried form after updating the README and JSDoc.

I also made one additional change: share is now required when using the curried form, since the curried form is intended to always be used with share. If this looks good to you, I believe the PR is ready to merge. If you would prefer share to remain optional, I’ll revert that change promptly.

@yusukebe

Copy link
Copy Markdown
Member

@nkfr26 @ashunar0 Thank you! I'll merge this and release a new version.

@yusukebe
yusukebe merged commit dbf7bae into honojs:main Sep 20, 2026
98 checks passed
@github-actions github-actions Bot mentioned this pull request Sep 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants