Skip to content

feat(inertia): add merge props with merge(), prepend(), and deepMerge()#1932

Merged
yusukebe merged 1 commit into
honojs:mainfrom
ashunar0:feat/inertia-merge-props
Jun 6, 2026
Merged

feat(inertia): add merge props with merge(), prepend(), and deepMerge()#1932
yusukebe merged 1 commit into
honojs:mainfrom
ashunar0:feat/inertia-merge-props

Conversation

@ashunar0

@ashunar0 ashunar0 commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Third step of the @hono/inertia v3 protocol breakdown: ③ Merge props, built on top of the partial reload foundation from #1904.

Adds three helpers — merge(), prepend(), and deepMerge() — that mark a prop for client-side combination on the next partial reload, instead of the default replace behavior. The wrapped value travels as-is; the renderer advertises which keys to combine via the new page.mergeProps / page.prependProps / page.deepMergeProps arrays and emits dot-paths on page.matchPropsOn for the client's dedupe logic.

import { deepMerge, inertia, merge, prepend } from '@hono/inertia'

app.use(inertia())

app.get('/feed', (c) =>
  c.render('Feed', {
    posts: merge(await db.posts.page(n), { matchOn: 'id' }),
    notifications: prepend(await fetchNotifications(), { matchOn: 'id' }),
    conversations: deepMerge(
      { data: await db.messages.page(n), meta: { nextCursor } },
      { matchOn: 'data.id' },
    ),
  }),
)

Protocol behaviour

Visit kind Behaviour
Initial / partial / JSON request Wrapped value is unwrapped and sent as a plain prop
Response with at least one merge marker page.mergeProps / prependProps / deepMergeProps / matchPropsOn emitted on every response (initial + partial) so the client knows which keys to combine on the next partial reload
Merge-marked prop excluded by only/except Stripped before any metadata is recorded (no leak in mergeProps etc.)
Function value inside merge() (e.g. merge(() => fetchPosts(), ...)) Resolved on the same lazy path as plain function props from #1904

Merging itself only takes effect on subsequent partial reloads — full page visits always replace props entirely (matches the upstream protocol).

Implementation

  • merge<T>(data, { matchOn? }): T, prepend<T>(...), deepMerge<T>(...) return opaque markers (Symbol.for('@hono/inertia/merge')) that the renderer unwraps. The return type is T, so call sites and TypedResponse stay transparent — mirrors the defer() pattern from feat(inertia): add deferred props with defer() #1911.
  • matchOn accepts a string or array; entries are emitted as "<propKey>.<field>" on page.matchPropsOn (e.g. merge(posts, { matchOn: 'id' })matchPropsOn: ['posts.id']).
  • PageObject gains four optional fields — mergeProps?: string[], prependProps?: string[], deepMergeProps?: string[], matchPropsOn?: string[] — emitted only when at least one prop in the response carries the corresponding marker.

Scope intentionally kept small (single PR per protocol feature, like #1904 / #1911):

  • ✅ Top-level `merge()` / `prepend()` / `deepMerge()` markers
  • ✅ Single string and array `matchOn`
  • ✅ Function value inside `merge()` (resolved via the existing lazy path)
  • ✅ Strategy mixing in a single response
  • ❌ Nested / dot-path merge markers — separate PR (would need to land alongside dot-path partial reloads)
  • ❌ `defer()` inside `merge()` — separate PR (semantics deserve their own design discussion)
  • ❌ Infinite scroll markers — separate PR (④ in the breakdown)

Test plan

  • `yarn workspace @hono/inertia test` (`40 passed`, 12 new tests covering each helper, single + array `matchOn`, strategy mixing, partial reload behaviour, excluded-prop guard, function-inside-merge, HTML embed, JSON request)
  • `eslint packages/inertia` clean
  • `prettier --check` clean
  • `tsc -b` clean
  • `tsdown` build success

@changeset-bot

changeset-bot Bot commented Jun 5, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e005fd2

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

@codecov

codecov Bot commented Jun 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.98%. Comparing base (80e20eb) to head (e005fd2).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1932      +/-   ##
==========================================
+ Coverage   91.90%   91.98%   +0.08%     
==========================================
  Files         115      115              
  Lines        3991     4032      +41     
  Branches     1031     1047      +16     
==========================================
+ Hits         3668     3709      +41     
  Misses        287      287              
  Partials       36       36              
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.

@yusukebe yusukebe left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!

@yusukebe

yusukebe commented Jun 6, 2026

Copy link
Copy Markdown
Member

@ashunar0 Nice! Thank you!

@yusukebe yusukebe merged commit 5318463 into honojs:main Jun 6, 2026
98 checks passed
@github-actions github-actions Bot mentioned this pull request Jun 6, 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.

2 participants