47ng / nuqs

Type-safe search params state manager for React frameworks - Like useState, but stored in the URL query string.
https://nuqs.47ng.com
MIT License
4.91k stars 105 forks source link

Regression: Component conditionally rendered on query parameter fails to update hook value from null to non-null #702

Closed firatciftci closed 1 month ago

firatciftci commented 1 month ago

Context

What's your version of nuqs?

"nuqs": "2.0.3"

What framework are you using?

Which version of your framework are you using?

 next: 14.2.16
 eslint-config-next: 14.2.16
 react: 18.3.1
 react-dom: 18.3.1
 typescript: 5.6.3

Description

After upgrading to nuqs@2.0.3 (from nuqs@1.20.0), a regression appeared where changing a query parameter from null to any value does not trigger an update to the value stored in useQueryState(s) if the component that calls the hook is rendered conditionally. The hook returns null on initial change of the query parameter from null to a value, and starts behaving as expected only after subsequent changes to the query parameter.

Reproduction

Custom query parameter hook:

"use client";

import { parseAsString, useQueryStates } from "nuqs";

export function useSelectedItem() {
  return useQueryStates(
    { itemId: parseAsString, subItemId: parseAsString },
    {
      urlKeys: {
        itemId: "item",
        subItemId: "subitem",
      },
      history: "push",
    },
  );
}

The main component:

"use client";

import { useSelectedItem } from "@/hooks/use-selected-item";
import ChildComponent from "@/components/child-component";

export default function MainComponent() {
  const [{ itemId }] = useSelectedItem();

  return (
    <div>
      ...
      {itemId !== null ?
        <ChildComponent />
      : null}
      ...
  );
}

The child component:

"use client";

import { useSelectedItem } from "@/hooks/use-selected-item";

export default function ChildComponent() {
  const [{ itemId }] = useSelectedItem();

  return (
    <div>Item ID: {itemId}</div>
  );
}

There exists a separate component with a list of cards in which pressing on a card changes the itemId value to a specific string:

"use client";

import { useSelectedItem } from "@/hooks/use-selected-item";

export default function Cards() {
  const [_, setSelectedItem] = useSelectedItem();

  ...
  return (
    ...
    <button onClick={() => setSelectedItem("value1")}>
      Value 1
    </button>
    <button onClick={() => setSelectedItem("value2")}>
      Value 2
    </button>
    ...
  );
}

When no value is selected, pressing on the first button changes the URL appropriately, but <ChildComponent /> does not register that itemId is set to "value1"; it continues to return null. However, while itemId is "value1", a subsequent click on the second button changes the URL appropriately once again, and now <ChildComponent /> is able to register the query value as "value2".

Removing the conditional render of <ChildComponent /> makes the issue go away. This bug does not appear in the version of nuqs prior to version 2.

franky47 commented 1 month ago

Ah I thought this could come back up when I was refactoring for v2, but the test that covered it before passed.

I believe the issue was that we need to take the value from the update queue into account when reading the initial search params for the internal state. There was an issue for this, let me dig around and I'll come back to you.

Edit: yes it was #359. So maybe the test isn't going far enough.

franky47 commented 1 month ago

Could you try this and let me know if it fixes your problem please?

pnpm add https://pkg.pr.new/nuqs@703
firatciftci commented 1 month ago

Seems to be fixed! Thank you so much once again for your super swift response.

franky47 commented 1 month ago

Cool, I'll find a way to properly test this to prevent regressions and I'll publish a release tomorrow.

github-actions[bot] commented 1 month ago

:tada: This issue has been resolved in version 2.0.4 :tada:

The release is available on:

Your semantic-release bot :package::rocket: