Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Key isolation #855

Open
wants to merge 4 commits into
base: next
Choose a base branch
from
Open

feat: Key isolation #855

wants to merge 4 commits into from

Conversation

franky47
Copy link
Member

@franky47 franky47 commented Jan 9, 2025

Key isolation is the fact that updating a search param only re-renders hooks subscribed to this key.

Example:

  • <ComponentA> has a useQueryState('a')
  • <ComponentB> has a useQueryState('b')

Updating the state for a doesn't re-render ComponentB.

Most frameworks (Next.js & React Router) re-render every call site of their useSearchParams hooks when any search params change (via navigation events), causing unnecessary re-renders of useQueryState(s). Some other routers like TanStack Router do implement key isolation.

Since in the React SPA adapter we control the whole thing (there is no router so to speak), we can implement caching to detect only relevant differences and avoid re-rendering unnecessarily.

For React Router & Remix, we can do this too in shallow mode, since their useSearchParams doesn't re-render on shallow updates. When using shallow: false, everything would re-render after the URL has updated (when coming back from the loader). This feels like a natural behaviour that is in line with the frameworks navigation.

For Next.js, this might require closer collaboration with the core team to implement this filtering built-in, because their useSearchParams does re-render on any search param change, even when done in a shallow manner. More research to do here.

Copy link

vercel bot commented Jan 9, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
nuqs ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jan 16, 2025 2:26pm

Copy link

pkg-pr-new bot commented Jan 9, 2025

Open in Stackblitz

npm i https://pkg.pr.new/nuqs@855

commit: 3202da1

cy.get('#state').should('have.text', 'pass')
cy.location('search').should('contain', 'test=pass')
assertLogCount('render', expected.mount + expected.update)
Copy link
Member Author

@franky47 franky47 Jan 9, 2025

Choose a reason for hiding this comment

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

note: Unrelated change to help the render count test be less flaky: by asserting the log count after testing for correctness, Cypress will wait for the URL to be correct (and give logs time to flush) before testing.

@@ -12,30 +17,38 @@ function updateUrl(search: URLSearchParams, options: AdapterOptions) {
url.search = renderQueryString(search)
const method =
options.history === 'push' ? history.pushState : history.replaceState
method.call(history, history.state, '', url)
method.call(history, history.state, historyUpdateMarker, url)
Copy link
Member Author

Choose a reason for hiding this comment

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

note: Looks like it this should be a bug fix for external history updates & sync optimisations, but it doesn't seem to make a difference. Better be aligned with the patch-history expected behaviour anyway.

@@ -83,7 +83,7 @@ export function useQueryStates<KeyMap extends UseQueryStatesKeysMap>(
),
[stateKeys, urlKeys]
Copy link
Member Author

Choose a reason for hiding this comment

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

note: urlKeys isn't guaranteed to be stable (if defined inline, it's not), so this would cause a cascade of referential instability.

Add a test & fix:

Suggested change
[stateKeys, urlKeys]
[stateKeys, JSON.stringify(urlKeys)]

@franky47 franky47 changed the title feat: Key isolation in the React SPA adapter feat: Key isolation Jan 10, 2025
@franky47 franky47 added adapters/remix Uses the Remix adapter adapters/react-router Uses the React Router adapter labels Jan 10, 2025
@franky47 franky47 marked this pull request as ready for review January 10, 2025 14:06
@franky47 franky47 requested a review from Copilot January 14, 2025 04:29

Choose a reason for hiding this comment

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

Copilot reviewed 11 out of 26 changed files in this pull request and generated no comments.

Files not reviewed (15)
  • packages/e2e/react/src/routes/key-isolation.useQueryState.tsx: Evaluated as low risk
  • packages/e2e/react-router/v7/cypress/e2e/shared/key-isolation.cy.ts: Evaluated as low risk
  • packages/e2e/react-router/v6/src/routes/key-isolation.useQueryState.tsx: Evaluated as low risk
  • packages/e2e/react-router/v6/src/routes/key-isolation.useQueryStates.tsx: Evaluated as low risk
  • packages/e2e/react/src/routes/key-isolation.useQueryStates.tsx: Evaluated as low risk
  • packages/e2e/react-router/v7/app/routes/key-isolation.useQueryState.tsx: Evaluated as low risk
  • packages/e2e/react-router/v7/app/routes/key-isolation.useQueryStates.tsx: Evaluated as low risk
  • packages/e2e/react-router/v6/cypress/e2e/shared/key-isolation.cy.ts: Evaluated as low risk
  • packages/e2e/react/cypress/e2e/shared/key-isolation.cy.ts: Evaluated as low risk
  • packages/e2e/react/src/routes.tsx: Evaluated as low risk
  • packages/e2e/react-router/v7/app/routes.ts: Evaluated as low risk
  • packages/e2e/react-router/v6/src/react-router.tsx: Evaluated as low risk
  • packages/nuqs/src/adapters/lib/context.ts: Evaluated as low risk
  • packages/e2e/shared/specs/render-count.cy.ts: Evaluated as low risk
  • packages/e2e/remix/app/routes/key-isolation.useQueryStates.tsx: Evaluated as low risk
Key isolation is the fact that updating a search param
only re-renders hooks subscribed to this key.

Most frameworks (Next.js & React Router) re-render
every call site of their useSearchParams hooks when
any search params change, causing unnecessary
re-renders.

Since in the React SPA we control the whole thing
(there is no router so to speak), we can implement
caching to detect only relevant differences and avoid
re-rendering unnecessarily.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
adapters/react Uses the React SPA adapter adapters/react-router Uses the React Router adapter adapters/remix Uses the Remix adapter
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant