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

Set query state causes that component to rerender 4 times #755

Open
kanasva opened this issue Nov 11, 2024 · 1 comment
Open

Set query state causes that component to rerender 4 times #755

kanasva opened this issue Nov 11, 2024 · 1 comment
Labels
adapters/next/app Uses the Next.js app router bug Something isn't working
Milestone

Comments

@kanasva
Copy link

kanasva commented Nov 11, 2024

Context

What's your version of nuqs?

2.1.1

What framework are you using?

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 23.6.0: Fri Jul  5 17:54:52 PDT 2024; root:xnu-10063.141.1~2/RELEASE_ARM64_T8103
  Available memory (MB): 16384
  Available CPU cores: 8
Binaries:
  Node: 22.10.0
  npm: 10.9.0
  Yarn: 1.22.22
  pnpm: 9.12.2
Relevant Packages:
  next: 15.0.3 // Latest available version is detected (15.0.3).
  eslint-config-next: 15.0.3
  react: 19.0.0-rc-66855b96-20241106
  react-dom: 19.0.0-rc-66855b96-20241106
  typescript: 5.6.3
  • ✅ Next.js (app router)

Description

Setting query state causes that component to rerender for 4 times both in development and production on my local machine. However, in nuqs version 2.0.4, it rerenders 2 times.

Reproduction

Example: Steps to reproduce the behavior:

// page.tsx

import { Suspense } from "react";
import QueryState from "./QueryState";

export default function page() {
  return (
    <div>
      <Suspense>
        <QueryState />
      </Suspense>
    </div>
  );
}
// QueryState.tsx

"use client";

import { parseAsInteger, useQueryState } from "nuqs";
import { useRef } from "react";

export default function QueryState() {
  const count = useRef(0);
  const [, setQueryState] = useQueryState(
    "queryState",
    parseAsInteger.withDefault(0)
  );

  // Computation expensive to test
  const computeExpensiveValue = () => {
    let total = 0;
    for (let i = 0; i < 1e8; i++) {
      total += Math.sqrt(i);
    }
    return total;
  };

  console.log("Render", (count.current += 1));
  console.time("computeExpensiveValue"); // Start timing
  computeExpensiveValue();
  console.timeEnd("computeExpensiveValue"); // End timing

  return (
    <button
      onClick={() =>
        setQueryState((prev) => {
          count.current = 0;
          return prev + 1;
        })
      }
    >
      Set queryState + 1
    </button>
  );
}
  1. Click the 'Set queryState + 1' button and see the console
@kanasva kanasva added the bug Something isn't working label Nov 11, 2024
@franky47
Copy link
Member

Thanks for the report, it sounds like this is a side-effect of the optimistic useSearchParams update we did in #718.

The order of renders looks like the following:

  1. The internal state is immediately updated and returned
  2. When the URL update queue is flushed, the optimistic searchParams is updated
  3. When the URL has finished updating, the stock useSearchParams from Next.js picks up the change and re-renders
  4. Not sure what that 4th render comes from

One vector of optimisation would be to set the optimistic search params in the same tick as the internal state update, I'll see what I can do about that.

As an aside regarding computationally expensive operations that block the main thread, you might want to consider plugging them onto a deferred value to avoid race conditions, see #722.

@franky47 franky47 added the adapters/next/app Uses the Next.js app router label Nov 11, 2024
@franky47 franky47 added this to the 🪵 Backlog milestone Nov 11, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
adapters/next/app Uses the Next.js app router bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants