Skip to content

State desynchronization when rendering <Navigate /> on first render #1245

@kouak

Description

@kouak

Context

What's your version of nuqs?

    "nuqs": "2.8.2",

What framework are you using?

  • ❌ Next.js (app router)
  • ❌ Next.js (pages router)
  • ✅ React SPA (no router)
  • ❌ Remix
  • ✅ React Router

Which version of your framework are you using?

    "react-router": "^6.30.2",
    "react-router-dom": "^6.30.2",

Description

When rendering a <Navigate /> component which updates the search params on first render, nuqs state will never synchronize its state back.

Reproduction

import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { parseAsString, useQueryStates } from 'nuqs';
import { NuqsAdapter } from 'nuqs/adapters/react-router/v6';
import { BrowserRouter, Link, Navigate } from 'react-router-dom';
import { describe, expect, test } from 'vitest';

function TestComponent() {
  const [nuqsState] = useQueryStates({
    search: parseAsString.withDefault(''),
  });

  if (nuqsState.search === 'REDIRECT') {
    return (
      <Navigate
        to={{
          search: '?search=foo',
        }}
      />
    );
  }

  return (
    <>
      <span>{nuqsState.search}</span>
      <Link to={{ search: '?search=REDIRECT' }}>Trigger redirect</Link>
    </>
  );
}

describe('nuqs repro', () => {
  test('with initial search param', async () => {
    using _ = withInitialUrl('/?search=foo');

    const { container } = render(
      <NuqsAdapter>
        <BrowserRouter>
          <TestComponent />
        </BrowserRouter>
      </NuqsAdapter>,
    );

    expect(container).toHaveTextContent('foo');
  });

  test('with a click on a link which triggers the rendering of <Navigate />', async () => {
    using _ = withInitialUrl('/?search=foo');
    const user = userEvent.setup();

    const { container, getByRole } = render(
      <NuqsAdapter>
        <BrowserRouter>
          <TestComponent />
        </BrowserRouter>
      </NuqsAdapter>,
    );

    const redirectButton = getByRole('link', { name: 'Trigger redirect' });
    await user.click(redirectButton);

    expect(container).toHaveTextContent('foo');
  });

  test('with initial search param', async () => {
    /**
     * With this URL, <TestComponent /> will render <Navigate /> on first render.
     */
    using _ = withInitialUrl('/?search=REDIRECT');

    const { container } = render(
      <NuqsAdapter>
        <BrowserRouter>
          <TestComponent />
        </BrowserRouter>
      </NuqsAdapter>,
    );

    await expect.poll(() => container).toHaveTextContent('foo');
  });
});

function withInitialUrl(url: string) {
  window.history.pushState({}, '', url);

  return {
    [Symbol.dispose]: () => {
      window.history.pushState({}, '', '/');
    },
  };
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    adapters/react-routerUses the React Router adapterbugSomething isn't working

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions