Next.js: Router query object not immediately populated from dynamic route params

Created on 19 Mar 2020  Â·  14Comments  Â·  Source: vercel/next.js

Bug report

The router query object is not populated immediately from dynamic route params on either the server or the client.

Describe the bug

The example for Dynamic routing shows using useRouter() to extract a query param to use for rendering. However, on the server, and on client first load, query is an empty object.

This becomes a problem when relying on the param to do anything more than display it - for example using swr to retrieve data based on it.

It's also unexpected, because it appears to be information that is knowable immediately, given either a req or a window.location.

To Reproduce

Example repro:

// [id].tsx
const TestPage: NextPage = () => {
  const router = useRouter()
  const { query } = router
  const id = query.id
  console.log('Props', process.browser, router)

  return <h1>ID: {id}</h1>
}

Actual behaviour

Visiting /42

There is one entry in the server log:

Props false undefined

The component runs twice on the client, with two entries in the client console:
Props true undefined
Props true 42

Expected behavior

I expect query to be populated the first time I call useRouter().

I expect a server log of Props false 42.

I expect the function to render once on the client, with one entry in the console: Props true 42

System information

  • Version of Next.js: 9.3.1

Most helpful comment

I bumped into this today as well, and tried withRouter as well, but it behaves the same.

All 14 comments

Interestingly, if I define a getServersideProps – even one that doesn't do anything – query is populated on both the server and the client first load. Is this to do with ASO, perhaps? If so, we might just need clearer documentation or worked examples of how to safely hybridise a page.

In particular, we can't conditionally call useSWR, for example, because react complains about differing orders of hooks being executed. And calling useSWR with the wrong URL is not an option.

I bumped into this today as well, and tried withRouter as well, but it behaves the same.

My temporal workaround:

const withRouterFix = (
  WrappedComponent: FC<{ router: NextRouter }>,
  filter: (query: ParsedUrlQuery) => boolean,
) => () => {
  const router = useRouter();
  if (filter(router.query)) return null;
  return <WrappedComponent router={router} />;
};

const PageID: FC<{ router: NextRouter }> = withRouterFix(
  ({ router }) => {
  },
  (query) => !query.id,

Duplicate of #11484

Fix in #9370

@Timer, is adding a no-op getServersideProps() the suggested way to disable ASO? This adds a bunch of complexity to pages on dynamic routes when ASO is enabled. (I know getStaticProps is a thing, but many kinds of dynamic page can't be properly populated at build time, and it seems like a common pattern to retrieve data based on the route param.)

Adding a no-op getServerSideProps is not the suggested way of working around this, though, it'd work. You should really defer the data fetching until the router is ready, as will be made easy via #9370 (if you rely on query information).

I'm using SWR for data fetching. I can't useSWR inside useEffect, and I can't conditionally call useSWR (both due to rules of hooks).

Can you please share your useSWR component?

A simplified version looks like:

const ProfilePage = ({query}: Props) => {
  const { username } = query
  const { data: user } = useSwr(`/api/v1/users/${username}`) 

  if (!user) return null
  return <div>{user.name}</div>
}

which gives 404s as it tries to load from /api/v1/undefined. (fetcher is set in SWRConfig higher up). (Same deal if I pass ['/api/v1/users/', username] to SWR.)

Really, if query isn't going to be ready on first load (which again, it does seem knowable immediately), then this page is not suitable for ASO (as most of my pages are not), but I can't seem to disable it other than through GSSP.

This should work:

const ProfilePage = ({ query }: Props) => {
  const { username } = query;
  const { data: user } = useSwr(() =>
    username ? `/api/v1/users/${username}` : false
  );

  if (!user) return null;
  return <div>{user.name}</div>;
};

Cool, thank you 🙂

A little workaround:

const useRouterWithQueryReady = (): NextRouter & { queryReady: boolean } => {
  const router = useRouter();
  const queryReady =
    Object.keys(router.query)
      // Server uses amp prop for some reason.
      .filter((k) => k != 'amp').length > 0;
  return { ...router, queryReady };
};

If you are using TypeScript, the key function must return null:

const { data: store } = useSWR<Store>(() => (id ? `stores/${id}` : null), fetch)
Was this page helpful?
0 / 5 - 0 ratings

Related issues

havefive picture havefive  Â·  3Comments

renatorib picture renatorib  Â·  3Comments

YarivGilad picture YarivGilad  Â·  3Comments

olifante picture olifante  Â·  3Comments

lixiaoyan picture lixiaoyan  Â·  3Comments