Skip to content

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

Open
gusa4grr wants to merge 1 commit intonpmx-dev:mainfrom
gusa4grr:search-ssr-mismatch-bridge
Open

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

Conversation

@gusa4grr
Copy link
Copy Markdown
Contributor

@gusa4grr gusa4grr commented Apr 4, 2026

🔗 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
Copy link
Copy Markdown

vercel bot commented Apr 4, 2026

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

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Apr 5, 2026 2:48pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Apr 5, 2026 2:48pm
npmx-lunaria Ignored Ignored Apr 5, 2026 2:48pm

Request Review

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 4, 2026

Codecov Report

❌ Patch coverage is 76.47059% with 4 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
app/composables/npm/search-utils.ts 60.00% 3 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@gusa4grr
Copy link
Copy Markdown
Contributor Author

gusa4grr commented Apr 4, 2026

Fixes #2380

@gusa4grr gusa4grr force-pushed the search-ssr-mismatch-bridge branch 2 times, most recently from 2d26409 to 180d757 Compare April 4, 2026 23:14
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 4, 2026

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 name Status Explanation
Description check ✅ Passed The 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 ✅ Passed The 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 ✅ Passed All 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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

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

return
}
router.push({
void router.push({
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.

are these voids necessary?

Copy link
Copy Markdown
Contributor Author

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
Contributor Author

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

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

2 participants