Skip to content

fix: bridge SSR payload mismatch when client search provider differs from server default - #2379

Closed
gusa4grr wants to merge 1 commit into
npmx-dev:mainfrom
gusa4grr:search-ssr-mismatch-bridge
Closed

fix: bridge SSR payload mismatch when client search provider differs from server default#2379
gusa4grr wants to merge 1 commit into
npmx-dev:mainfrom
gusa4grr:search-ssr-mismatch-bridge

Conversation

@gusa4grr

@gusa4grrgusa4grr commented Apr 4, 2026

Copy link
Copy Markdown
Contributor

🔗 Linked issue

Resolves#2380

🧭 Context

When the user's searchProvider value is npm, there will be hydration errors and more. Routes affected are search?q=vue or /settings page, but it may be more, like /~[username] route

Moreover, on search?q=vue, the user is additionally presented with "no results screen" after flashing actual search results 😅

In this MR I:

  • I introduced the Search SSR Bridge between both searchProviders, as I found that the results are of the same structure and assumed I could "reuse" them during hydration
  • added vue org to the e2e fixture, so it mimics now the same scenario as on the prod app, as before in tests, there were no "org" checks in search results
  • aligned the "interaction" spec with arrow-up/down behaviour to accommodate new expected behaviour

In order to see this behaviour yourself, you need to:

  1. Go to settings, change your provider to npm from algolia
  2. write in url bar manually /search?q=vue. Observe hydration + no results
  3. write in url bar manually /settings. Observe hydration error

More details in "details" section below 😉

Images attached:

search page:

image

settings page:

image
  • Centralize ?p query param merge into useSearchProvider() getter
  • Remove duplicated searchProviderValue computed from all consumers
  • Add bridgeSearchSSRPayload() to copy SSR cache to client's provider key during hydration
  • Destructure asyncData before spreading to avoid shadowing custom data ref
  • Update e2e tests for org suggestion cards in keyboard navigation

📚 Description

During my adventures with the implementing server-synced user preferences (please check that one too if you fancy 🙂 ), I stumbled upon the e2e tests failing with hydration errors, which were:

  • landing on /search?q=vue
  • settings page
    Also, the interactions spec and url-compatibility spec have each failed tests 🤯

So I went ahead and started digging into what "have I done" so it broke.
after bunch of hours of little to no success I (finally) decided to check how does the main branch behave and just went ahead to npmx.dev prod app and I found that with npm as searchProvider, th things are actually not working as they should.

This became visible during my implementation, as preferences are now server-synced, which just uncovered those issues. And the combination of the fact that in the "test" environment, searchProvideris defaulted tonpm`.

I continued digging and found a misalignment in the searchProvider value during hydration. Basically the fetch was always happening with algolia, and then when NPM "results" were tried to be hydrated on client - it sees nothing due to cache key mismatch between providers.

so I dig more and more and I come up with the solution you see in this MR.

I'm happy to discuss further or implement a better approach.

NOTE: When navigating via the website to the search page, all is OK. like for instance if you're on the /settings and write in npmx search bar vue => it will render OK. The secret here is the ?p=npm query parameter, which is programmatically added, thereby enabling the search page to find results.

@vercel

vercelBot commented Apr 4, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
npmx.devReadyReadyPreview, CommentApr 5, 2026 2:48pm
2 Skipped Deployments
ProjectDeploymentActionsUpdated (UTC)
docs.npmx.devIgnoredIgnoredPreviewApr 5, 2026 2:48pm
npmx-lunariaIgnoredIgnoredApr 5, 2026 2:48pm

Request Review

@codecov

codecovBot commented Apr 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 76.47059% with 4 lines in your changes missing coverage. Please review.

Files with missing linesPatch %Lines
app/composables/npm/search-utils.ts60.00%3 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@gusa4grr

gusa4grr commented Apr 4, 2026

Copy link
Copy Markdown
ContributorAuthor

Fixes #2380

@gusa4grr
gusa4grrforce-pushed the search-ssr-mismatch-bridge branch 2 times, most recently from 2d26409 to 180d757CompareApril 4, 2026 23:14
@coderabbitai

coderabbitaiBot commented Apr 4, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3c35512a-fb60-4c7c-83e0-01846ff4c237

📥 Commits

Reviewing files that changed from the base of the PR and between 180d757 and 7a77f97.

📒 Files selected for processing (9)
  • app/components/SearchProviderToggle.client.vue
  • app/composables/npm/search-utils.ts
  • app/composables/npm/useOrgPackages.ts
  • app/composables/npm/useSearch.ts
  • app/composables/npm/useUserPackages.ts
  • app/composables/useGlobalSearch.ts
  • app/composables/useSettings.ts
  • test/e2e/interactions.spec.ts
  • test/fixtures/npm-registry/search/@vue.json
✅ Files skipped from review due to trivial changes (2)
  • test/e2e/interactions.spec.ts
  • test/fixtures/npm-registry/search/@vue.json
🚧 Files skipped from review as they are similar to previous changes (7)
  • app/composables/useSettings.ts
  • app/components/SearchProviderToggle.client.vue
  • app/composables/npm/useOrgPackages.ts
  • app/composables/npm/search-utils.ts
  • app/composables/useGlobalSearch.ts
  • app/composables/npm/useUserPackages.ts
  • app/composables/npm/useSearch.ts

📝 Walkthrough

Walkthrough

This pull request removes route-query-based provider normalization and updates components and composables to use the reactive searchProvider directly. It adds bridgeSearchSSRPayload(prefix, identifier, provider) which, on the client during hydration, copies an Algolia SSR payload entry into a provider-specific client cache key when missing. Several npm composables (useOrgPackages, useSearch, useUserPackages, useGlobalSearch) and SearchProviderToggle.client.vue were updated to rely on searchProvider, adjust cache keys, and invoke the SSR bridging; e2e keyboard-navigation tests were adjusted and a new npm fixture for @vue packages was added.

Possibly related PRs

Suggested labels

needs review

Suggested reviewers

  • danielroe
  • alexdln
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check nameStatusExplanation
Description check✅ PassedThe pull request description is directly related to the changeset, explaining the SSR hydration mismatch issue when searchProvider is npm and detailing the implemented solutions.
Linked Issues check✅ PassedThe PR addresses all coding requirements from issue #2380: centralizing the ?p query param merge, bridging SSR cache mismatches between providers during hydration, and updating tests for the new behaviour.
Out of Scope Changes check✅ PassedAll changes are within scope of resolving the SSR hydration mismatch issue: composable refactoring, SSR bridge implementation, cache key normalisation, and test fixture updates.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot 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.

🧹 Nitpick comments (1)
test/e2e/interactions.spec.ts (1)

105-119: Consider adding explicit focus assertion for consistency.

The other modified tests (lines 143, 170, 177) assert that orgSuggestion is focused after ArrowDown, but this test relies solely on the final URL check. Adding an assertion after line 117 would make the test more consistent and easier to debug if it fails.

🔧 Suggested improvement
 // ArrowDown again, then Enter navigates to the suggestion
// URL is /package/vue or /org/vue or /user/vue. Not /vue
await page.keyboard.press('ArrowDown')
+ await expect(orgSuggestion).toBeFocused()
await page.keyboard.press('Enter')
await expect(page).toHaveURL(/\/(package|org|user)\/vue/)

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: daf4de20-08e0-4555-b419-178494a8c1a0

📥 Commits

Reviewing files that changed from the base of the PR and between 5fe1486 and d5c343d.

📒 Files selected for processing (9)
  • app/components/SearchProviderToggle.client.vue
  • app/composables/npm/search-utils.ts
  • app/composables/npm/useOrgPackages.ts
  • app/composables/npm/useSearch.ts
  • app/composables/npm/useUserPackages.ts
  • app/composables/useGlobalSearch.ts
  • app/composables/useSettings.ts
  • test/e2e/interactions.spec.ts
  • test/fixtures/npm-registry/search/@vue.json

@coderabbitaicoderabbitaiBot 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.

🧹 Nitpick comments (1)
app/composables/npm/search-utils.ts (1)

1-26: Well-implemented SSR payload bridge.

The logic correctly handles the hydration mismatch by copying SSR-cached data from the algolia-keyed entry to the client's provider-specific key. The guard conditions are comprehensive:

  • import.meta.client ensures client-only execution
  • isHydrating ensures this only runs during hydration
  • id && prevents malformed keys from empty identifiers
  • The !nuxtApp.payload.data[clientKey] check avoids overwriting existing data

Consider using the stricter SearchProvider type instead of string for the provider parameter:

♻️ Optional: Stricter typing
+import type { SearchProvider } from '~/composables/useSettings'+
export function bridgeSearchSSRPayload(
prefix: string,
identifier: MaybeRefOrGetter<string>,
- provider: MaybeRefOrGetter<string>,+ provider: MaybeRefOrGetter<SearchProvider>,
): void {

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b0bd3e3f-7b8b-48bb-b5b8-db13fd315b81

📥 Commits

Reviewing files that changed from the base of the PR and between d5c343d and 180d757.

📒 Files selected for processing (9)
  • app/components/SearchProviderToggle.client.vue
  • app/composables/npm/search-utils.ts
  • app/composables/npm/useOrgPackages.ts
  • app/composables/npm/useSearch.ts
  • app/composables/npm/useUserPackages.ts
  • app/composables/useGlobalSearch.ts
  • app/composables/useSettings.ts
  • test/e2e/interactions.spec.ts
  • test/fixtures/npm-registry/search/@vue.json
✅ Files skipped from review due to trivial changes (2)
  • app/composables/npm/useSearch.ts
  • test/fixtures/npm-registry/search/@vue.json
🚧 Files skipped from review as they are similar to previous changes (4)
  • app/composables/npm/useOrgPackages.ts
  • test/e2e/interactions.spec.ts
  • app/composables/npm/useUserPackages.ts
  • app/composables/useGlobalSearch.ts

Comment threadapp/composables/useGlobalSearch.ts Outdated
return
}
router.push({
void router.push({

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.

are these voids necessary?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

oh, my bad, I just "fixed" the errors in my IDE, but apparently that's my local oxc.typeAware: true config, which marks this as error.

I'll revert the changes

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

done @ghostdevv 👍🏻

- Centralize `?p` query param merge into `useSearchProvider()`
- Remove duplicated `searchProviderValue` computed from all consumers
- Add `bridgeSearchSSRPayload()` to copy SSR cache to client's provider key during hydration
- Destructure asyncData before spreading to avoid shadowing custom `data` ref
- Update e2e tests for org suggestion cards in keyboard navigation
@gusa4grr
gusa4grrforce-pushed the search-ssr-mismatch-bridge branch from 180d757 to 7a77f97CompareApril 5, 2026 14:45
@gusa4grr
gusa4grr requested a review from ghostdevvApril 7, 2026 09:38
@serhalpserhalp added the needs review This PR is waiting for a review from a maintainer label May 9, 2026
@github-actionsgithub-actionsBot added stale This has become stale and may be closed soon and removed stale This has become stale and may be closed soon labels Jun 9, 2026
@github-actionsgithub-actionsBot added the stale This has become stale and may be closed soon label Jul 11, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs reviewThis PR is waiting for a review from a maintainerstaleThis has become stale and may be closed soonstale-to-close

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SSR hydration mismatch and "no results" flash when searchProvider is set to npm

3 participants

@gusa4grr@ghostdevv@serhalp