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

Nuqs changes router.pathname when the query param is changed with rewrite in middleware #914

Closed
julius-retzer opened this issue Feb 13, 2025 · 16 comments · Fixed by #917
Closed
Labels
adapters/next/pages Uses the Next.js pages router bug Something isn't working released

Comments

@julius-retzer
Copy link

julius-retzer commented Feb 13, 2025

Context

What's your version of nuqs?

    "nuqs": "1.20.0",

What framework are you using?

  • ✅ Next.js (pages router)

Which version of your framework are you using?

    "next": "15.0.2",

(edited)

Description

We have a multitenant setup in our next js app and we use middleware to rewrite different hosts to path. So for example www.test.com will rewrite to www.ourapp.com/test. (see the simplified middleware in the example).

When we use nuqs, the pathname seems to change unexpectedly after nuqs does the URL manipulation. Altough there could be bug in our middleware as well, without nuqs, it works as expected. So I suspect that the issue is happening somehwere when nuqs does the manipulation with the URLs and it somehow clashes with our middleware rewrite

Reproduction

https://stackblitz.com/edit/sb1-zrnx1nia?file=pages%2F%5BtenantAlias%5D%2Fcontacts.tsx
Example: Steps to reproduce the behavior:

  1. open the app and click the link to go to contacts page
  2. write something into the input
  3. Observe the logs. After nuqs adds the query to the url, router.pathName changes from '/[tenantAlias]/contacts' to '/[tenantAlias]'. Also in the middleware, we're seeing req.nextUrl.href http://localhost:3000/contacts?search=p&tenantAlias=contacts. Why is tenantAlias getting added to the query params?

-->

@julius-retzer julius-retzer added the bug Something isn't working label Feb 13, 2025
@franky47 franky47 added the adapters/next/pages Uses the Next.js pages router label Feb 13, 2025
@franky47 franky47 added this to the 🪵 Backlog milestone Feb 13, 2025
@franky47
Copy link
Member

Thanks for the report, please note that next@14.0.2 specifically isn't supported due to a bug in Next.js core:
#423 (comment)

If you are able to upgrade Next.js to 14.2.0, you should be able to use nuqs@^2 for which I'll try and reproduce your use-case.

@julius-retzer
Copy link
Author

julius-retzer commented Feb 13, 2025

@franky47 Uhm, I'm sorry, I confused the numbers, we actually use "next": "^15.0.2". I updated the reproduction to use this next version. The behaviour is the same.

I also tried update to nuqs 2, and the issue was the same

@franky47
Copy link
Member

For point n°2 (the tenant alias being added to the search params), I think this is due to how Next.js pages router stores dynamic routes internally. If you inspect the internal router state after typing in the search bar, you'll see that the router.state.query object contains both search params and URL params:

Image

The pages router adapter could probably use a better implementation, I'll try and reproduce your use-case in the e2e test environment and iterate on possibles fixes.

@julius-retzer
Copy link
Author

Yes, you might be right about that. The router.pathname issue still stands though, I think it produces unexpected navigation to the index page on writing to input (but not always, I couldn't reproduce this in the example)

@franky47
Copy link
Member

franky47 commented Feb 14, 2025

While I was adding tests in #917, I found that using shallow: false preserves the pathname in the pages router.

And to add to the confusion, some of the tests fail in dev but pass in prod 😓

@julius-retzer
Copy link
Author

julius-retzer commented Feb 14, 2025

Yes, but shallow: false doesn't make much sense in our case, we're using client rendering and notifying server is not what we wan't here.

Maybe we can add it as a workaround to fix our current prod issue until this is fixed. But since it looks like this won't be an easy fix and it's causing serious instability, seems like we will have to move away from nuqs :/

@franky47
Copy link
Member

Could you please show me how you updated search params with shallow: false in your dynamic middleware-rewritten URL before using nuqs in a way that preserved that pathname?

There may be something I'm missing, but calling the router methods with a rewritten path always ends up in an error, as the dynamic part of the route (the rewritten [tenant] param) is not part of the asPath (and so mismatches with the href).

@franky47
Copy link
Member

franky47 commented Feb 14, 2025

Ah wait I think I found the secret combination:

router.replace(
  {
    pathname: router.pathname,
    query: {
      // Add the URL params here
      tenant: 'david',
      // AND the search params (if they don't conflict)
      search: 'Hello, world!'
    }
  },
  {
    pathname: router.asPath,
    query: {
      // Add _all_ search params here, but not the URL params:
      search: 'Hello, world!'
    }
  },
  { shallow: true }
)

Now onto finding how to correctly extract only the URL params and merge them with the non-conflicting search params in the first query..

@julius-retzer
Copy link
Author

julius-retzer commented Feb 14, 2025

Yes, I came to more or less same conclusion, but I also needed to strip existing query params from router.asPath.

This is my own implementation that accounts for the rewrite param:

import { useEffect, useState, useMemo } from 'react';
import { useRouter } from 'next/router';

export type URLStateOptions<T> = {
  defaultValue: T;
  clearOnDefault?: boolean;
  shallow?: boolean;
};

export const usePagesRouterURLState = <T extends string = string>(
  key: string,
  { defaultValue, clearOnDefault, shallow = true }: URLStateOptions<T>,
): [T, (value: T) => void] => {
  const router = useRouter();
  const [value, setValue] = useState<T>(() => {
    if (!router.isReady) {
      return defaultValue;
    }
    return (router.query[key] as T) || defaultValue;
  });

  const queryValue = useMemo(() => router.query[key] as T, [router.query, key]);

  // Sync from URL to state
  useEffect(() => {
    if (router.isReady) {
      setValue(queryValue || defaultValue);
    }
  }, [router.isReady, queryValue, defaultValue]);

  const updateValue = (newValue: T) => {
    if (newValue === value) return;
    setValue(newValue);

    const currentQuery = { ...router.query };
    if (clearOnDefault && newValue === defaultValue) {
      delete currentQuery[key];
    } else {
      currentQuery[key] = String(newValue);
    }

    const url = {
      pathname: router.pathname,
      // Path query should include tenantAlias
      query: currentQuery,
    };

    const asPath = {
      // Remove query from asPath
      pathname: router.asPath.split('?')[0],
      // asPath query should not include tenantAlias
      query: filterQuery(currentQuery, ['tenantalias']),
    };

    router.replace(url, asPath, { shallow });
  };

  return [value, updateValue];
};

type QueryValue = string | string[] | undefined;

const filterQuery = (
  query: Record<string, QueryValue>,
  excludeKeys: string[],
) => {
  const filtered: Record<string, string> = {};
  Object.entries(query).forEach(([key, value]) => {
    if (!value || excludeKeys.includes(key.toLowerCase())) {
      return;
    }
    if (Array.isArray(value)) {
      filtered[key] = value[0];
    } else {
      filtered[key] = value;
    }
  });
  return filtered;
};

@franky47
Copy link
Member

Hah, I came to the same conclusion just before I saw your message 🙌

Here's a build that passes the repro test, let me know if it works for you:

pnpm add https://pkg.pr.new/nuqs@917

I'll add some more tests to check for robustness (catch-all segments, push/replace, shallow true/false etc..).

@julius-retzer
Copy link
Author

Thanks, but I'm running into issues when wrapping with Nuqs adapter in _app.tsx. I'm not using pnpm so not sure if that's a pnpm issue

https://stackblitz.com/edit/sb1-kbhrcumz?file=pages%2F_app.tsx

@franky47
Copy link
Member

Ah yes that's the missing .js extension we had in #368 and #693. I've updated the preview deployment, can you try this please?

npm install https://pkg.pr.new/nuqs@016c2a81cae14af4d61dc651fbdfa40491d7717f

@julius-retzer
Copy link
Author

Great! In my app that seems to be working. However on stackblitz.com/edit/sb1-kbhrcumz?file=pages%2F_app.tsx it seems like the query is not updated in the URL at all? Not sure why is that

@franky47
Copy link
Member

That's because the rendered webpage runs in an iframe, so it's the iframe URL that gets updated:

Image

Thanks for your feedback and your patience! #917 should land today in nuqs@2.4.0 on NPM.

@julius-retzer
Copy link
Author

Thank you for the efforts as well!

Copy link

🎉 This issue has been resolved in version 2.4.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@franky47 franky47 removed this from the 🚀 Shipping next milestone Feb 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
adapters/next/pages Uses the Next.js pages router bug Something isn't working released
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants