vercel / next.js

The React Framework
https://nextjs.org
MIT License
127k stars 26.99k forks source link

[NEXT-1550] App Router with `output: export` does not support `useParams()` on client #54393

Closed styfle closed 7 months ago

styfle commented 1 year ago

Verify canary release

Provide environment information

Operating System:
      Platform: darwin
      Arch: arm64
      Version: Darwin Kernel Version 22.5.0
    Binaries:
      Node: 18.17.1
      pnpm: 8.6.12
    Relevant Packages:
      next: 13.4.20-canary.2
      react: 18.2.0
      react-dom: 18.2.0
    Next.js Config:
      output: export

Which area(s) of Next.js are affected? (leave empty if unsure)

App Router, Static HTML Export (output: "export")

Link to the code that reproduces this issue or a replay of the bug

https://github.com/curated-tests/next-issue-48022

To Reproduce

The page in question is /app/blog-app/[slug]/page.tsx

next build

Describe the Bug

Fails with an error:

Error: Page "/blog-app/[slug]" is missing "generateStaticParams()" so it cannot be used with "output: export" config.

This is expected right now because its not implemented, but it would be nice to support this so that App Router can match Pages Router.

Expected Behavior

Ideally, the build should complete and navigation should work client side.

pnpm build
cd out
python3 -m http.server 3000

Which browser are you using? (if relevant)

Chrome

How are you deploying your application? (if relevant)

python3 -m http.server 3000

NEXT-1550

xray-robot commented 1 year ago

Perhaps there are dates when this is scheduled to be done? Or is it better to use Pages Router?

FYI: Gatsby allows you to use dynamic routers in static exports without configuring server-side redirects. That's the only thing stopping us from moving our PWAs from Gatsby to Next.js

danbockapps commented 1 year ago

I did not see this error on 13.4.13. I started seeing it when I upgraded to 13.5.3.

petejodo commented 1 year ago

Can confirm this is working on 13.4.13. I'm curious if the functionality is therefore a regression or a bug that's become a feature 😅

GabenGar commented 1 year ago

But does it output the static page? If it doesn't and there is no error, then it's definitely a regression.

styfle commented 1 year ago

@petejodo What does "working" mean in this case? Previously there was no error message but it was failing silently. So in 13.5.0, a helpful error message was added explaining how to fix it.

See https://github.com/vercel/next.js/issues/48022#issuecomment-1688599391 for more details.

However maybe there is a case when the error message shouldn't be printed? If you have code that was working with useParams() 13.14 as is no longer working in 13.15, please share the minimal code by creating a new issue and I'll get it fixed, thanks!

Please do NOT comment on this issue since it describes a new feature that needs to be implemented (not a regression).

Frtrillo commented 1 year ago

@petejodo What does "working" mean in this case? Previously there was no error message but it failing silently. So in 13.5.0, a helpful error message was added explaining how to fix it.

See #48022 (comment) for more details.

However maybe there is a case when the error message shouldn't be printed? If you have code that was working with useParams() 13.14 as is no longer working in 13.15, please share the minimal code by creating a new issue and I'll get it fixed, thanks!

Please do NOT comment on this issue since it describes a new feature that needs to be implemented (not a regression).

I can confirm that this issue persisted in version 13.4.13 as well. It failed silently as you said; there was no error during the build, but accessing the dynamic route resulted in a 404 error. Sadly it appears that using dynamic routes with the app router and 'use client' is currently not possible. I hope it gets addressed soon, as it's just what's left for me, at least, to fully commit to the app router

petejodo commented 1 year ago

yeah I didn't see that at first and thought that it was working in my testing but I guess I somehow messed something up in my testing where I thought it was working but it definitely was not. Sorry, my mistake!

This put me in a bind because I misinterpreted the opening paragraphs to https://nextjs.org/docs/app/building-your-application/deploying/static-exports which made it seem I could port a SPA to nextjs and we just about finished porting it. That's my problem though, not yours' 😅 I think I can re-hack react-router back in even though that was giving me trouble at the start of the project

C-W-D-Harshit commented 1 year ago

If anyone gets the solution plz update here! 😁

eric-burel commented 1 year ago

Hi, is this related to the error encountered in @leerob SPA example? The error message seems surprising, and params are meant to be obtained via page props rather than the useParams hook, but I wonder if it is considered the same root issue

goerlitz commented 1 year ago

Hi, I just started learning Next.js and I want to deploy my app as static SPA on Amazon S3. Hence, I added output: export. But I'm getting errors for the dynamic route pages using App Router.

  1. Error: Page "/datasets/[slug]" is missing "generateStaticParams()" so it cannot be used with "output: export" config. when I execute npm run build .
  2. After adding generateStaticParams() with some dummy values (because the real route params cannot be known at build time) I'm getting a runtime error (in dev mode) when calling the dynamic path with any other value. Error: Page "/datasets/[slug]/page" is missing param "/datasets/foo" in "generateStaticParams()", which is required with "output: export" config.
  3. When further adding 'use client' I get Error: Page "/datasets/[slug]/page" cannot use both "use client" and export function "generateStaticParams()".
  4. When I just add 'use client' but remove generateStaticParams() I'm getting Error: Page "/datasets/[slug]" is missing "generateStaticParams()" so it cannot be used with "output: export" config. again.

I'm stuck here. What am I supposed to do to get the dynamic routes working with the static export?

Maybe, I'm don't fully understand generateStaticParams() (yet), but when I use a dynamic route, I typically have many entries in a database that I wanna fetch and render on demand (either client or server side). I don't wanna pre-render millions of pages at build time. Moreover, I don't wanna rebuild the app whenever the database gets updated.

eric-burel commented 1 year ago

@goerlitz starting step 3 and 4 I think you get confused:

The thing is that if you use a dummy static param, Next only knows the dummy "/datasets/foo" route. So "/datasets/bar" won't work. You could do an URL rewrite from "/datasets/bar" to "/datasets/foo", but then the route parameter is lost.

You could opt for a query parameter instead.

Sadly until Next.js supports exporting dynamic routes that are not statically rendered like it did in Next 12 pages dir, I don't think you can use a dynamic route and do a static export, you should prefer a query parameter, + optionally an URL rewrite from a route param to a query param.

goerlitz commented 1 year ago

Thanks @eric-burel for the guidance. Actually, after diving much deeper into Next.js, I realized that I was trying to make two different paradigms work together - which is a bad idea.

Having worked on SPA with Api backends in the past, I thought I could make Next.js output a SPA bundle with separate api code. Actually, I think that the first paragraph in https://nextjs.org/docs/app/building-your-application/deploying/static-exports is quite misleading in that sense:

Next.js enables starting as a static site or Single-Page Application (SPA), then later optionally upgrading to use features that require a server.

No, Next.js is not made for SPAs - it is a totally different paradigm with the goal to NOT do all rendering and routing in the browser but move more code to the server where computing is more efficient (SSR etc.). Hence, a typical Next.js app will never be an SPA (and should not be), because the application code is split up and runs on server and client likewise.

IMHO, for most applications - that usually have dynamic routing - the static export does not make sense to me at all.

AndonMitev commented 1 year ago

Does exists on 13.5.5 as well, I have tried to build a view with this routing structure:

/games
  [id]
     page.tsx

on production i had issues for previous versions, [id] was not found and it was navigating to home page, and now not even able to build to the this issue:

Error: Page "/page/[id]" is missing "generateStaticParams()" so it cannot be used with "output: export" config
andreasfrey commented 1 year ago

Can you please give an update, when this feature is planned? I mainly switchedto nextjs, because of the routing functionality. Now it forces me to host on a node server...

As a workaround i try out useing pages router again. [https://nextjs.org/docs/pages/building-your-application/routing/pages-and-layouts]

goerlitz commented 1 year ago

Now it forces me to host on a node server...

@andreasfrey, this is what I meant with "Next.js follows a different paradigm" than SPA. Running a Next.js app on a node server is the default deployment.

If you are trying to use Next.js to create a traditional SPA (with dynamic routes) that can be packaged for hosting on a CDN, then you are doing something (conceptually) wrong. This is not what Next.js was designed for.

If you want to move from an SPA to Next.js (with all the nice features, like SSR etc.), you should

  1. be okay with deploying everything to a node server (Vercel, Netlify, etc.) and changing your deployment pipelines
  2. identify and define suitable boundaries between client and server components in your app (ie. dynamic vs. static parts)
goerlitz commented 1 year ago

Maybe, I'm getting something wrong here. @durchanek and @ramirorinaldi, can you explain why you voted down?

andreasfrey commented 1 year ago

@andreasfrey, this is what I meant with "Next.js follows a different paradigm" than SPA. Running a Next.js app on a node server is the default deployment.

Maybe this is true.

I really like to develop React applications, because they are fast to build and require not much of a server (running even on CDN...). Now react enforces encourages me to use a framework and suggests next.js to use. And I did.

And now I wanted to implement a really basic use case - which is not possible anymore, because you insist on using a server component?

And yes it is true, I do not need server components. And I am sure many other people do't need them as well.

So, again I repeat the question: When is it planned to implement dynamic routing on the client side? I am sure many ppl are waiting for it.

(I switched to Page Routes meanwhile ... maybe the next project is again simple React with react-router .....)

ramirorinaldi commented 1 year ago

@goerlitz with Pages Router you can use dynamic routes and output: "export", yes you need to provide all possible paths with getStaticPaths, but this is something not supported now with App Router.

As React itself suggests to use React-based frameworks, being NextJs its first option, one should expect to use it for a SPA without much conflicts, that's why I don't think you are doing something conceptually wrong when trying to do so...

Next does create an html file for each defined route, but in any case it can be up to the developer to sacrifice bundle size/load speed and load what they think it is "unnecessary JavaScript" to fully support dynamic routing for cases where you don't have the full list of possible paths

magratheaner commented 1 year ago

Totally agree with @ramirorinaldi. I landed in this situation because the official React docs recommended next.js and there were no (clear) warnings along the way for the use-case of statically-hosted SPAs using the app router paradigm.

So as long as the issue exists it would be nice to put some warnings about this somewhere in the next.js docs, and maybe also urge the React doc maintainers to do the same, to avoid too many others arriving here.

Apart from that it should be technically possible for next.js to support this if I'm not mistaken. So it would also be nice to see this on the roadmap down the line for later projects ☺️

andreasfrey commented 1 year ago

Totally agree with @ramirorinaldi. I landed in this situation because the official React docs recommended next.js and there were no (clear) warnings along the way for the use-case of statically-hosted SPAs using the app router paradigm.

So as long as the issue exists it would be nice to put some warnings about this somewhere in the next.js docs, and maybe also urge the React doc maintainers to do the same, to avoid too many others arriving here.

Apart from that it should be technically possible for next.js to support this if I'm not mistaken. So it would also be nice to see this on the roadmap down the line for later projects ☺️

You can use the Pagesd Router meanwhile (https://nextjs.org/docs/pages/building-your-application/routing) image

jserpapinto commented 1 year ago

I have this page.tsx

import type { Metadata } from 'next'
import getStands from '@/lib/getStands'
import getStand from '@/lib/getStand'
import { notFound } from "next/navigation"

type Params = {
  params: {
    standId: string
  }
}

export async function generateMetadata({ params: { standId } }: Params): Promise<Metadata> {
  const standData: Promise<Stand> = getStand(standId)
  const stand: Stand = await standData

  if (!stand) {
    return {
      title: "stand not found!",
      description: "stand not found!"
    }
  }

  return {
    title: stand.name,
    description: `This is the page of ${stand.name}`
  }
}

export default async function StandPage({ params: { standId } }: Params) {

  const standData: Promise<Stand> = getStand(standId)
  const stand: Stand = await standData

  if (!stand) return notFound()

  return (
    <div>{JSON.stringify(stand)}</div>
  )
}

// generate static params function
export async function generateStaticParams() {
  const standsData: Promise<Stand[]> = getStands()
  const stands: Stand[] = await standsData

  console.log(stands)

  return stands.map((stand) => ({
    params: {
      standId: stand.id
    }
  }))
}

And also getting:

Error: Page "/[standId]" is missing "generateStaticParams()" so it cannot be used with "output: export" config.
goerlitz commented 1 year ago

same issue. Is there any solution?

No, unfortunately not. output: export is meant for static site generation of a fixed number of predefined content items (like Blog, documentation pages, a catalog, ...).

If you want to develop a fully dynamic SPA where the rendered pages depend on the user status and interactions, you should not use output: export but embrace the server side rendering of Next.js (and using Vercel is the easiest option to do so).

GabenGar commented 1 year ago

@goerlitz Your posts are coping because pages router allows to output static pages with dynamic params just fine, with the assumption that the consuming server will map its own routing logic to specific output static files. So nextjs clearly wasn't "designed" to force you into a runtime nodejs server, it would be DoA otherwise, since convincing a team to switch a framework with only some routing changes is way easier than convincing a team to shove another cloud http server in the stack. It's just an unfortunate combination of things like react docs recommending to start with nextjs, while nextjs recommending to start with app router which can't migrate a typical react SPA codebase anymore.

fromthemills commented 1 year ago

One additional thing I noticed is that when you try to "trick" the app router in returning a catch all route it still does not work because useParams returns only the params returned by generateStaticParams and not dynamically based on the current url.

// app/posts/[id]/page.js
import PostDetailsPage from "./PostDetailsPage"; 

export async function generateStaticParams() {
  return [{ id: "fallback" }];
}

export default function Page() {
  return <PostDetailsPage />;
}
// app/posts/[id]/PostDetailsPage
"use client";

import { useParams } from "next/navigation";

export default function PostDetailsPage() {
  const { id } = useParams(); // always returns 'fallback' also when you navigate to /posts/1
  return <div>Post: {id}</div>;
}
{
    "rewrites": [
        {
            "source": "posts/*",
            "destination": "/posts/fallback.html"
        }
    ]
}
alexanderhorner commented 1 year ago

Here is a feature request and discussion outlining the issue and solutions more clearly: https://github.com/vercel/next.js/discussions/55393#discussion-5627726

alessandro-r-amos commented 1 year ago

Is this really a bug? Can I continue developing without the output: export until it's fixed?

alexanderhorner commented 1 year ago

Yes, if you don’t use output export it works just fine.

GabenGar commented 1 year ago

@alessandro-r-amos

Is this really a bug? Can I continue developing without the output: export until it's fixed?

Only if you use Vercel as a host for your nextjs codebase.

cheraff commented 1 year ago

Also have this issue. Have downgraded from 14 to 13.4.13 as a fix in the meantime.

Very important feature of various applications I develop that rely on unknown paths to render content based on that path in static export and static hosting enviroments.

cometyang commented 1 year ago

I use Next.js with tauri, so need this feature. It will b e really nice otherwise have to downgrade to a lower version. :-(

eric-burel commented 1 year ago

@cometyang no downgrade is needed at all, you can stick to the pages folder in Tauri, and even build a true SPA using a JS router https://colinhacks.com/essays/building-a-spa-with-nextjs

@goerlitz I agree that when wanting a true SPA (= with exporting, no node server around), Next is not the best fit. I would personnally favour Vite. But it's not impossible, you can perfectly use the pages folder for that and stick to client-only features. SSR can be almost totally avoided by using a NoSsr wrapper compoment or next/dynamic. Again, convoluted, not very advised, but not impossible if necessary.

lsegal commented 1 year ago

The odd part here is that this used to work just fine with the app router, so clearly this is possible to support. It seems very much like this limitation is artificial.

I use both server-side builds and client side generated (statically generated) code in one single application. The application uses server-side code to mock data prior to be building the static application, and this is necessary for my use case. What is the alternative here?

GabenGar commented 12 months ago
  1. Use pages router.
  2. Switch all useParams() dynamic calls to useSearchParams() (and therefore use search parameters instead).

I wouldn't say the pages way of handling route params on static export was super stellar, as it forced all dynamic segments to be client-side, which also forced to implement multilang support client-side (the main candidate for partial static render).

Ridd0 commented 11 months ago

It's a bit of a shame that the basic functionality doesn't work in the stable version of Next.js. Before the next, stable version is announced, maybe it would be good to test it first. Building the app in a production environment is rather a must-have.

I had an issue with building next on CI and back to the 13.4.13 version solved that issue, but still output: export doesn't work for dynamic routes.

Migrating to the pages routing instead of the app, probably will solve the problem, but I am still verifying this.

GabenGar commented 11 months ago

It largely depends on what you meant by "working". The only difference pages router brings is it outputs dynamic segments right in the file path, and it's up to the consuming server to map its own routing logic to the client-side NextJS routing logic.

SimonStAmand commented 11 months ago

I'll ask to same question as @alessandro-r-amos; should we wait for a fix? This is quite an issue for my team and we cannot use the app folder as it is. We are using page, but we are afraid of seeing the support for page go without a sound replacement.

wwwhatley commented 11 months ago

App router should have never been released, what an absolute nightmare.

pavelkornev commented 11 months ago

I'll ask to same question as @alessandro-r-amos; should we wait for a fix? This is quite an issue for my team and we cannot use the app folder as it is. We are using page, but we are afraid of seeing the support for page go without a sound replacement.

We share the same concerns. If pages support goes out (which will most likely happening in the next 2-3 years) and App Router doesn't support dynamic routes (I mean "unknown" path segments at build time), then it's a question if we should consider switching from Next.js to some other framework asap to avoid huge migration efforts in future...

Any comments from Vercel at least on the plans?

Frtrillo commented 11 months ago

I'll ask to same question as @alessandro-r-amos; should we wait for a fix? This is quite an issue for my team and we cannot use the app folder as it is. We are using page, but we are afraid of seeing the support for page go without a sound replacement.

I switched to useSearchParams since I doubt this is gonna be addressed soon

haugseth commented 11 months ago

This is my first comment, I apologize if its not valid or clear, but I have tracked this issue to be introduced with v13.4.20-canary.1 as its working with v13.4.20-canary.0. I have tested this with the repro repo, and with https://github.com/leerob/next-static-export-example.

I found this change in the file packages/next/src/server/base-server.ts:

+      hasFallback = typeof fallbackMode !== 'undefined'

-      const hasFallback = typeof fallbackMode !== 'undefined'
+      if (this.nextConfig.output === 'export') {
+        const page = components.pathname
+    
+        if (fallbackMode !== 'static') {
+          throw new Error(
+            `Page "${page}" is missing exported function "generateStaticParams()", which is required with "output: export" config.`
+          )
+        }
+        const resolvedWithoutSlash = removeTrailingSlash(resolvedUrlPathname)
+        if (!staticPaths?.includes(resolvedWithoutSlash)) {
+          throw new Error(
+            `Page "${page}" is missing param "${resolvedWithoutSlash}" in "generateStaticParams()", which is required with "output: export" config.`
+          )
+        }
+      }

If i find this source and remove it in the file node_modules/next/dist/server/base-server.js for any versions later than v13.4.20-canary.0, even at v14 it works as expected.

I think this is because its expected that we should know of all routes when making an static export, as its stated in the docs, dynamic routes with client components is unsupported in the app router. I think this limits the usefulness of making an SPA with Nextjs. Its the similar situation in SvelteKit with the adapter-static, but then its possible to override this limitation by making it explicit that we don't want to prerender that dynamic route, and then making it truly dynamic, by making the client fetch dynamic data or display a 404? Maybe a similar flag is needed in Nextjs?

In SvelteKit these flags are:

export const prerender = false;
export const ssr = false;
GabenGar commented 11 months ago

@haugseth Interesting find, but I suspect it was locked out because app router works slightly differently to the pages one. Specifically whereas useRouter() of pages was an amalgamation of client and server routers (along with very liberal pathname and search query parsing on top), for better or worse, the app router is split into 4 different client-only hooks: usePathname(), useParams(), useSearchParams() and useRouter(). Server-side they also differ as pages have getServerSideProps()/getStaticProps() being basically a source of "truth" for a given page, while segments of app pages are free to interpret params and search params independently. The most important question does it work outside of nextjs server (the whole point of static export)? I.E. for the given example above, does returning {out_path}/spa-post/[id].html for the path /spa-post/:id in an expressjs server means useParams() inside {out_path}/spa-post/[id].html is aware of value of :id? I strongly doubt it is the case as there is not isReady flag on useParams() to check for hydration state and there is no server-side render involved to populate its values.

lsegal commented 11 months ago

The most important question does it work outside of nextjs server (the whole point of static export)?

I'm not sure this is the right question-- even if it "doesn't work" for this specific use case (it can, see below), there are other cases where the value of static builds are useful even if dynamic segments are (initially) ignored-- especially when, as mentioned, the behavior used to work in a capacity that was sufficient for various users, then stopped. Clearly users were getting enough value out of the previous functionality to have had it enabled until it was broken by an update.

If there are edge cases that cannot be supported out of the box, they can be carved out and documented so that downstream users of Next.js can know about these limitations and handle them via plugin or other mechanism-- or Next.js can simply implement support following in the footsteps of other frameworks that have taken on this problem space:

I.E. for the given example above, does returning {out_path}/spa-post/[id].html for the path /spa-post/:id in an expressjs server means useParams() inside {out_path}/spa-post/[id].html is aware of value of :id?

I've seen other frameworks handle this via extra configuration. Qwik, as an example, can handle SSG for dynamic segments. Vike has specific APIs to handle dynamic segments. It's certainly a solvable problem.

haugseth commented 11 months ago

My use case are storing the build app bundled as static files and served as a SPA from capacitor and tauri. These tools does not include a server, static export is the only options. This seems not that simple after migrating to the app router, and support for dynamic routes suddenly removed/changed after v13.4.20.canary.0++. Options would be to migrate back to the pages folder, or to move components over to a vite and use another router like react-router-dom.

Edit: Maybe I am just confused by the app router, it could be that this was never intended to work like this, and the pages router is in fact the correct way to build a SPA using nextjs. My apologies in advance.

campos20 commented 10 months ago

I feel that the deprecation of CRA led me to this frustrating point. I followed react docs to nextjs (with the client alternative), I created some pages (with the app router), they are already deployed and working using CDN and now I'm feeling that I'll need to look for alternatives to nextjs because the 'use client' is not enough. What I'm really considering is the complete switching away from react (probably to vue).

leerob commented 10 months ago

We plan to fix this. I'm sorry we haven't gotten to it yet. But it will get fixed.

munjalpatel commented 10 months ago

@campos20 I can relate. I have been using Nuxt / Vue for years to build SPAs with dynamic routing without any problems.

For once I decided to try Next on my new project and found such a basic limitation. Having an ability to pre-render is great, but that can't be an only option. There are so many use cases with fetching data from client using API is just fine.

I will try to switch to Pages Router instead of App for now and see if that allows me to create a SPA without spinning up a NodeJS server.

SebastianGode commented 10 months ago

Is there already some kind of unstable version of NextJS available which adressed that issue?

@munjalpatel Is there an easy way to go back to the pages router with an exisiting project using the app router?

@leerob Is there some kind of timeline for this fix (like let's say a month or a year)?

callmevari commented 10 months ago

@munjalpatel did you try it? I mean, to go back to the Pages router and see if you can export statically with dynamic routes (tell us the Next.js version please). My use case is with Next.js + Capacitor.js. We want to export the static app to later use it in iOS/Android devices. Seems that solution is to go back (does that works? anyone tried it?) to Pages router or to select another framework.

GabenGar commented 10 months ago

There is no "easy way" to go back to the pages router and the static export with dynamic paths assumes you have a control over routing on server to make it understand that /[lang]/blog/[id]/index.html means it has to be returned at /:lang/blog/:id route. I.e. you can't just chuck it into Github Pages and expect it to work out of the box.

SebastianGode commented 10 months ago

@GabenGar I tried going back and gave up, it's too much work. Wondering when this issue finally gets fixed. At least it has an official [NEXT-xxxx] Issue tag already.