vercel / next.js

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

[NEXT-1187] Link navigation with loading.tsx is not instant for dynamic pages #43548

Closed tonypizzicato closed 1 year ago

tonypizzicato commented 1 year ago

Verify canary release

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 22.1.0: Sun Oct  9 20:14:30 PDT 2022; root:xnu-8792.41.9~2/RELEASE_ARM64_T8103
Binaries:
  Node: 18.12.1
  npm: 8.19.2
  Yarn: 1.22.19
  pnpm: N/A
Relevant packages:
  next: 13.0.6-canary.2
  eslint-config-next: 13.0.5
  react: 18.2.0
  react-dom: 18.2.0

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

App directory (appDir: true), Routing (next/router, next/navigation, next/link)

Link to reproduction - Issues with a link to complete (but minimal) reproduction code will be addressed faster

https://github.com/tonypizzicato/next-13-loading

To Reproduce

Describe the Bug

When you have a dynamic page with loading.tsx the route change and showing the loading animation are instant only for the page, which was freshly loaded. For other dynamic pages it hits the server first, then shows the loading state

Screenshot 2022-11-29 at 23 19 38

https://user-images.githubusercontent.com/640122/204661070-b9409f71-3814-45e6-bf56-923cdf4e2fc6.mov

Expected Behavior

Instantly navigate to the desired dynamic page and show loading component

Which browser are you using? (if relevant)

No response

How are you deploying your application? (if relevant)

No response

NEXT-1187

Fredkiss3 commented 1 year ago

That is how it works, in order to do the loading state, your browser has to first make the request to nextjs server, then nextjs will first return the loading state, and after resolves your page.

The thing is that in need, it is even slower that in production, because nextjs do a bigger amount of work. You could try to run it in production with yarn build && yarn start, but you'll find that it is not really instant, the slowness comes from the speed of your internet connection but also the distance between you and the server. If a normal request from you to the server is about 1s for example, then you'll see the loading state after 1s. But what is cool about the loading state if for example you make a request in your page.tsx which takes more that 5s (for example), you'll always see the loading state after 1s.

All of this is part of streaming rendering : https://beta.nextjs.org/docs/data-fetching/streaming-and-suspense#concepts

tonypizzicato commented 1 year ago

at is how it works, in order to do the loading state, your browser has to first make the request to nextjs server, then nextjs will first return the loading state, and after resolves your page.

that totally makes sense, that's why i've added throttling for testing on the client and delay on the api side. but isn't it a job of page prefetch feature to get the loading state before navigating to it? and if that's an intended behaviour, why does the first page loaded work differently (instant loading state on client every time)? and what is the reason to use streaming api with loading.ts feature in next.js if it is only useful (after 1s in your example) for API endpoints that take some significant time? for the most applications with simple db access endpoints its seems useless and client loading state would be preferred.

More than that Next.js docs say that loading.tsx should be instant compared to custom Suspense boundaries

Unlike loading.js, where navigation happens immediately and loading UI is displayed before the request to the server is complete. When manually defining Suspense Boundaries, navigation will happen after the new segment loads. https://beta.nextjs.org/docs/routing/loading-ui#manually-defining-suspense-boundaries

I mean, I understand why it works the way you described for custom suspense boundaries, but it's not obvious from the docs that the loading.tsx should work the same way

Fredkiss3 commented 1 year ago

But what is cool about the loading state if for example you make a request in your page.tsx which takes more that 5s (for example), you'll always see the loading state after 1s.

As i said, for api endpoints that take some time to fetch (for example 5s) it would always take the same amount of time to show you the loading UI, the time it would take you to show the loading state is the time your browser to connect to the server. So if your browser takes 1s to connect to the server, the loading ui would show after 1s and not after 5s, so instead of waiting 5s to show your page, you get a loading state sooner. This is better for when you reload the page for ex.

And you're right, with prefetching it should be instant, since it has already the data... but it seems that when you have a loading state it prefetches twice ? 🤔 In the example below it does two requests to prefetch data (see by the next-router-prefetch header). I don't know if this is a bug or not :

https://user-images.githubusercontent.com/38298743/204781992-7c49730d-5796-43ed-8c84-39b343b73877.mov

But prefetching isn't also a silver bullet. Take the case when your page hasn't finished prefetching the page, it would still take 1s before showing you the loading state and i think even if your page where static it would take the same amount of time : the time it takes for your browser to connect to your server and your server to serve you the static page.

Showing a loading state on the client would definitely make it faster, but only if you don't issue a navigation : like you fetch on the client without navigating to another page i think... 🤔

dumbravaandrei22 commented 1 year ago

Still happening in 13.1.3-canary.0

dumbravaandrei22 commented 1 year ago

I think that the loading.tsx should be instant without any network calls. I know that the loading.tsx will be under a page request, but like @tonypizzicato said the website should prefetch the Links that are on the page. That's the idea, from a user's POV, the user should receive instant feedback from the app. If the loading.tsx will take some time to load (even a small one) it will be useless.

eug-vs commented 1 year ago

Absolutely agree with all problems raised in this issue, this is the only thing stopping our project from migrating to App Router - navigation creates just horrible experience with sometimes up to 300ms wait time before even the loading streams in (I know this can be solved by moving the server closer e.g. on edge, but that's a different story). Other side of the problem is that there's no way to display any other loading state on the UI while loading hasn't streamed in yet - you are forced into this UX.

I truly don't understand the reason behind this: if loading.tsx doesn't accept any props and is essentially static (usually just a bunch of skeletons), why can't we include it in client-side bundle (or at least opt-in to that)? Or event better - do not include it into a bundle, but fetch it and cache properly on the client - so that subsequent visits of the same page immediately show the fetch UI from client-side cache. That is probably how prefetch should work, but currently my experience is that it's working in a very random fashion (even in NextJS playground: https://app-dir.vercel.app/streaming/edge/product/2)

With Suspense it's a bit different because it allows for granular control, but even then the initial state where Suspense is showing fallback could be used with the same approach.

hc0503 commented 1 year ago

@tonypizzicato , I've resolved same issue with group route. https://beta.nextjs.org/docs/routing/defining-routes#route-groups Look at my app route structure. image Dynamic page uses the nearest parent loading.tsx, but announcements must has another loading.tsx. To resolve this problem, I used group route. I hope this solution works well for you.

mhesham32 commented 1 year ago

for me when I added a root loading.tsx file it was picked immediately after I started the navigation to another page while the loading.tsx file inside the other page's folder wasn't picked and I was getting the same behavior as @tonypizzicato before adding the root loading.tsx file. also the other SSR page is a search page and it gets the search query from URL search params I was expecting the loading.tsx to appear when I change the params within the page to fetch new data but instead, it was displaying the old results after clicking on search and then suddenly the new results replace them which is not good for UX it was like the site was unresponsive

mhesham32 commented 1 year ago

it's worth mentioning the above behavior was happening with next@13.2.4 but when I upgraded to 13.3 it was no longer happening

hc0503 commented 1 year ago

@mhesham32 , did you use client component in 13.3? that has error in my end. https://github.com/vercel/next.js/issues/43930#issuecomment-1501767146

mhesham32 commented 1 year ago

Yes the navigation is triggered from a client component to a server page

sourcecodes2 commented 1 year ago

We are also experiencing this issue, and it is still present on v13.3.

The concept of "loading" for the end user means anything that is delaying their request, whether it's due to server processing or network latency.

There should be no distinction to the end user on technical merit. This is purely a UX issue.

End users need immediate feedback, whether that's the data they've requested or feedback to say their request is being handled.

Currently, this implementation fails to do this.

This is a core part of the age-old business arguement for retaining impatient users that we work ever so hard to get, that cannot be overlooked.

The fact that the on-request loading feedback needs loading on-request makes this implementation feel like one step forward and two steps back.

We are very grateful for NextJS and we do appreciate that the AppDir is still in beta, but please make sure this UX issue is fixed before release.

Fredkiss3 commented 1 year ago

It seems that to make loading instant you have to define it on the parent folder of the dynamic route like they do in App dir playground repository.

image

You can also use route groups like in @devdreamsolution comment : https://github.com/vercel/next.js/issues/43548#issuecomment-1497829622

dumbravaandrei22 commented 1 year ago

It works with https://github.com/vercel/next.js/issues/43548#issuecomment-1497829622 solution. It doesn't work for non-slug routes like: 'feed/page.tsx'. I tried to create '(feed)/feed/page.tsx' having loading.tsx inside (feed) but it doesn't work.

I will postpone the use of app folder until will become stable.

unleashit commented 1 year ago

Glad to have found this issue for a partial fix. Hope the behavior changes for manual Suspense so that the fallback is shipped to the client and run at the start of the transition. That's the UX users are going to expect. I'm not sure what the behavior was pre app directory with getServersideProps. But in other isomorphic React projects I've worked on you could always show a loading state on the component level (not saying you couldn't in Next.js in a useEffect or getInitialProps, but my understanding is with GSSP it could only be done on the page level).

Fredkiss3 commented 1 year ago

@unleashit my understanding is that pre-app router the loading fallback could be controlled by the user with router events (https://nextjs.org/docs/api-reference/next/router#routerevents) there is even an example used for showing a loading indicator : https://github.com/vercel/next.js/blob/8050a6c8e056efb336f15f6fa5be789cd040574d/examples/with-loading/pages/_app.tsx

unleashit commented 1 year ago

Hey thanks for the reply but I'm pretty sure those router events were only for the full page (and not at the component level) as I was mentioning. I'm pretty sure Suspense fallbacks are intended as the solution, but I don't think it's implemented as well as it could be. It definitely an improvement over those router events, because you can control it on the "route segment" level with a child loading.tsx. That's better than it was, but better still would be to support the same behavior (fallback shown immediately on a client transition) in a manual Suspense fallback.

Fredkiss3 commented 1 year ago

@unleashit those events where for page changes (or URL changes I think ?), it is in a useEffect so you can use it even at component levels, it is basically the same as loading.tsx.

I would not say that Suspense is implemented badly,but what it seems is that the client router is not aware of the loading.tsx components, or more likely, it wait for things to suspend (with async/await or use) to show the loading fallback, you can try for example to export a simple non async component and put a loading.tsx beside the page, it would not show the loading fallback (at least that's my understanding, I may be wrong).

I also recently inspected the RSC payload on navigation, and have seem to find that a layout.tsx ships with a loading indicator, so it may be the fact that if a layout for a page is located in the same level as a loading.tsx file, when the layout will load, next navigations will show the loading fallback instantly as it has already prefetched it.

I will try this again and post an update with my findings later.

unleashit commented 1 year ago

Yeah I agree with most of that and was only using the router transition as a reference for how things were UX wise with the NextJS experience. Suspense is an upgrade over that because it has more granular support for loading content from the server, vs only the top/page level for getServerSideProps.

But I think there definitely are some issues that people will agree on and will hopefully get ironed out. As someone mentioned above, as a "user", I would expect either the content or a loading transition to appear immediately after clicking a link, button, etc. Not after a delay, whether the delay is caused by the network or a data request somewhere, because the distinction doesn't really matter to them.

The experience is different with a loading.js and a manual Suspense. In the docs, the former is basically an automatic suspense for a page, but the loading behavior isn't the same. As an example, you can go to: https://jasongallagher.org/portfolio/tatteasy and click next/prev buttons. You should see a loader (a nested loading.tsx) just between the header and footer (* at least in Firefox, see below).

But then the behavior isn't the same with manual . I have one on the home page part way down the page. You can test by clicking on "work" in navbar. If you refresh with throttling on in dev tools, notice that the checkerboard of images never shows a spinner. The site is statically exported, but I've also tried this in SSR mode (and these are server components that query data) and the behavior is the same: almost never a spinner, even with throttling. In some of my testing while running in SSR mode, I did sometimes see a blip of a fallback in some places, but not in the way I would expect. IMO for a good UX, the fallback state should last for the full time that the HTML markup can't be shown in the browser regardless of why.

I assume a static/SSG site doesn't show a suspense fallback because the RSC output is statically available. Yet the spinner works (albeit inconsistently) in Next's implementation of loading.js. It would be much nicer if the fallback was displayed consistently no matter the environment.

Fredkiss3 commented 1 year ago

@dumbravaandrei22 maybe try to put a dummy Layout at (feed)/layout.tsx, maybe next skips your loading.tsx file because there is no layout at that level ? 🤔 i could be wrong though

Fredkiss3 commented 1 year ago

@unleashit Normally the Suspense boundary is supposed to always show if your page is SSR'ed on a full page refresh, that's because of streaming (something like this : https://locaci.fredkiss.dev/search). If it don't show with SSR, then it might be a bug, it might also be because your data fetching is very fast, what throttling does is only slow down you request to the server, if it is fast enough you will sometimes get a flash of loading states, or maybe not at all since when the browser receives the response your suspense boundary has already finished loading.

You could try to add an artificial delay to your fetches of for ex 1 to 2 seconds, to see it better.

unleashit commented 1 year ago

Normally the Suspense boundary is supposed to always show if your page is SSR'ed on a full page refresh

Yeah, that's exactly what I suspect the intention is but I don't think it's good UX. A better version of the Fallback would start running on client as soon as the dom is available, and/or as soon as a route transition begins if inside a layout. Then ending either as soon as content starts to paint, or when its finished (probably best). We can disagree on this, but there's no reason why it can't be implemented this way... and no reason I can think of that it wouldn't be a better user experience than a loader that appears before and/or after a pause. It already works like that in loading.tsx for the most part (seemingly with bugs), which is why you see the transition between pages on site that's static.

I get that it appears to work more nicely in situations where the server component has more work do (nice work on your site). But in other situations where the reason for the delay is network, browser parsing, etc, especially a page that already has content... it shouldn't matter what is causing the delay when it comes to fallback behavior. With client components we can use useEffect and have that control. But right now with server components you only have the behavior the Suspense fallbackoffers.

Interesting what you originally pointed out about the prefetched text files on my site. Just noticed it. I think they're just the static RSC output which for some reason on a static exported site have a txt extension (normally I think they're rsc). But I could be wrong!

Fredkiss3 commented 1 year ago

For link navigations manual <Suspense>, do not show until after a network request, instead the the current page is shown (because of useTransition), this is because react cannot know if one of your components will suspend or not, so it makes a request to the server and as soon as it finds a suspense boundary it automatically flush out a response with the fallback, then a little while after, it shows the complete page, you might have a request that only suspend on the first render (like the page i just shared, which suspends only on first request and uses client side fetching when navigating to make it fast for navigations).

For loading.tsx however it seems to work a little differently. Here is what i understand :

I have an exemple without prefetching which shows the loading spinner only after a delay then show them instantly on subsequent navigations

I also have an with prefetch=true that shows them instantly

it shouldn't matter what is causing the delay when it comes to fallback behavior.

I think the logic of the react team is that Suspense gives you the power to show the loading spinner whenever you like, by default it does not do exactly that, but you can take over this by using a transition, i've done something like that here : https://parallel-routes-modal-next.vercel.app/boards_search/oWNLDSH2nQTVrDcWS3zg7o

You can see here that the suspense boundary show up instantly because i suscribed to the isPending given by useTransition.

However this example is very hacky as i have to opt out of the link component entirely : https://github.com/Fredkiss3/parallel-routes-modal-next/blob/c9443997ebc8ccaadc03c0a68a2e8ab17ed1f260/src/app/boards_search/%5Bbid%5D/card-list.tsx#L41

I think the canonical solution here is the loading.tsx file, you just have to make sure to place them in the right place and add prefetching.

unleashit commented 1 year ago

I think it comes down to justifying behaviors because of what may seem to make sense "technically". But with user experience, you do your best to achieve behavior based on what the user would expect and prefer. If the cost is high, you sometimes have to compromise of course. I'm not going to have one of the first tickets to Mars for example. My UX is probably going to be going camping instead. But since I don't think this is quite rocket science and Sever Components are still new, it seems like a good time to focus on what the UX of it should be.

I think the acceptance of this is a Next thing... coming from the fact that getServersideProps was accepted as the norm even though it was always bad UX. There were always better ways to handle client route changes via lazy loading such as this, which allowed finer control of the experience that what GSSP offered. RSCs are an upgrade over both methods, but the fallback situation is still less than ideal. It may work fine in some situations, but in others it doesn't. There's no reason why React and Next can't fix it if they want. Just show the fallback as soon as the route transition begins and stop showing it when the rsc is ready to hydrate. I'm not an expert on Server Components, haven't watched any two hour videos and don't currently have time, but I just can't image what the technical barrier to that could be.

Fredkiss3 commented 1 year ago

You are totally right about the expectations of user experiences.

I don't know if it is "fixable" by the Next or React team, it doesn't seem like it for Suspense to me, but I may be totally wrong.

I'm totally in the same length as you about what we can do to make UX better.

My suggestions (with informations I have today) :

1 - use Loading.tsx for page transitions 2 - use Suspense for the first load if you want to have Streaming on just some components and takeover with useTransition to show loading fallback instantly on the client

In a live video with Ben Holmes (the whiteboard guy who works at Astro) & Dan Abramov, they made a little app with a Search list, they did the same as my second suggestion : https://github.com/bholmesdev/simple-rsc/tree/main/app

unleashit commented 1 year ago

I don't see why it isn't fixable. For each suspense, add a HOC to the client bundle that loads the fallback immediately on mount. Then end it once it has children. Seems like loader.js is already doing that (although not perfectly), and probably not far off from a normal suspense. I would think this is probably something for Next rather than React to do. Of course I'm being an arm chair directory who hasn't taken the time to look at the code, but if this issue is for some reason unsolvable than in my opinion is is a big mark against server components.

I appreciate the ideas! For my little project I won't sweat it too much. I've looked at useTransition and it seems more for not blocking the UI when doing some work? I'll have to check that out at some point.

Fredkiss3 commented 1 year ago

There is a PR about a potential solution for this : https://github.com/vercel/next.js/pull/49077

TLDR : next will now prefetch pages content until it finds a loading.tsx which will be prefetched also.

Edit :

Tested it on the latest canary and it works : https://github.com/Fredkiss3/next-13-loading-prefetched . Before, next did not do any real prefetching (it did not run the page on the server), now next will prefetch the page until it finds a loading.tsx file, so that on navigation the loading fallback will show instantly, it works also with suspense.

For suspense there is something interesting : if the page is prefetched (either on hover on DEV, and by default on production), it will eagerly run and return the suspense boundary, and if suspense resolves before you navigates to that page, you won't see the fallback but the page directly. this one is pretty cool.

Updated this example which is more complete : https://github.com/Fredkiss3/parallel-routes-modal-next

unleashit commented 1 year ago

Thanks for the update. But this only works with prefetching? I wonder why they're against simply bundling the loaders with the initial payload. That would be a much better solution IMO. Prefetching can be expensive to a cloud hosting bill (and there is another discussion about the inability to turn it completely off on Link components), and it doesn't solve the cases where a user clicks on a link before the files are resolved. Unless I'm missing something, there would still be a delay.

Fredkiss3 commented 1 year ago

Yes, in slow connections you can have a delay, i still think this is an interesting solution. I don't work in the next team so i can't change it any way that matters, i can understand how it is done and i have some intuition about how it all works, but i didn't dig in the source code or talked with the team for their insights maybe @feedthejim can answer ? (sorry for tagging you).

You can disable prefetching with <Link prefetch={false}>.

unleashit commented 1 year ago

Just pointing out that the way loaders were always done on SSR React apps besides Next.js, this problem didn't even exist. So now coming to Next it's new to me. Server components make it more complex, but I think the issue is really the way Next handles code splitting (more is always better). But it's not. It should really be a developer choice to optimize and decide the best tradeoff between UX and performance. Adding the loaders (maybe say down to the next route segment) in the initial bundle probably only adds a few K, and would even save a few HTTP requests. Then it works smooth as butter.

In a similar vein it's sometimes be nice to bundle a group of common pages and to download on every route. That can make the UX much more snappy as long as they aren't too expensive. I like Next, but it's kind of sad sometimes that so much is under the hood not easy to control...

You can disable prefetching with <Link prefetch={false}>.

Unless they've changed it recently, you can't disable it completely. In the docs prefetch=false still triggers prefetch on hover: https://nextjs.org/docs/api-reference/next/link. If you're on a budget and paying by the request, this may not be such a good thing. Prefetching is a cool idea, but it's not always better than the selective bundling I mentioned... and certainly not more budget friendly. But enough of my opinions! My guess is this is a won't fix since they seem to really really want to maximize HTTP reqs. For some reason.

Fredkiss3 commented 1 year ago

@unleashit please let's keep the conversation positive. I can understand that you are frustrated but don't be snarky.

Unless they've changed it recently, you can't disable it completely.

I've tested this with the latest canary before answering. It does indeed disable prefetching.

unleashit commented 1 year ago

Hey, not sure what made you offended... it certainly wasn't my intention. I'm not even really frustrated. Just evaluating Next.js and server components on a simple app and made some observations. If you don't like them, that's ok,, we can disagree. That said, my opinions were only about Next.js and the decisions they've made, not you in case you felt that for some reason. Vercel is a for profit company and I respect that Next.js (and you and I) are a business model for them. I have no issues with that. But I also don't see a problem with discussing the potential pitfalls of that situation. And there are some. Just on the Link/prefetching issue for example there are numerous threads (Google and you'll find) going back several years of people not happy with the inability to turn off without creating your own . If they fixed it recently, good on them.

Anyoo, I've 100% come in peace with the goal of discussing my experience with this issue.

vimtor commented 1 year ago

After several attempts, I realized that the issue for me was using generateStaticParams. I'm not sure why, but removing it solved the problem.

abdelkadersettah commented 1 year ago

After several attempts, I realized that the issue for me was using generateStaticParams. I'm not sure why, but removing it solved the problem.

Its worked for me to after removing the generateStaticParams

l-you commented 1 year ago

Nothing mentioned above helped. We expect <Suspense> fallback to be displayed each time page parameter changes. E.g. /products?cat=shoes and /products?cat=shoes&size=36. As for now server component is waiting until api requests will be completed. In case of slow search customer does not see any effect caused by link click until request is fulfilled. This is weird. I hope this will be fixed as soon a s possible.

Fredkiss3 commented 1 year ago

@rusted-love i also thought this was a bug, but this is expected, the solution is to use a different key to Suspense to force React to consider the Suspense as a different component each time and show the fallback, i've done something like that here : https://github.com/Fredkiss3/todo-app-beautiful-ux/blob/main/src/app/(routes)/(app)/%40todo_app/page.tsx#L49-L54

You could either encore the search params to string to set a unique key per search, or use Math.random() to have a random key each time and always show the fallback. Encoding the searchParams for the key could be interesting if the user refetch the same page, since the key is the same React won't show the fallback and the user will still see the old content, while when navigating to another page, it will show them a fallback.

I think this behavior is related to how Suspense work with transitions. the thing is that when using Suspense if the fallback have already been shown once, if you use a transition, React will hold the old data until the Suspense boundary has resolved, and Next uses transitions for all the navigations. Using a different key forces React to consider the Suspense as a different component.

This is from the React documentation here : https://react.dev/reference/react/Suspense#preventing-already-revealed-content-from-hiding

l-you commented 1 year ago

@Fredkiss3 Thank you for information. It helped me a lot. In our case main issue was caused by slow generateMetadata. I thought that generateMetadata result is lazy-injected in order to improve TTFB metric. Hope with time generateMetadata will be lazy-injected for non-robot users.

Fredkiss3 commented 1 year ago

@rusted-love generateMetadata is not lazy loaded, in fact it is loaded before your page (for SEO reasons), It is explained in the docs :

Next.js will wait for data fetching inside generateMetadata to complete before streaming UI to the client. This guarantees the first part of a streamed response includes tags.

You can learn more here : https://nextjs.org/docs/app/building-your-application/optimizing/metadata#dynamic-metadata (in the "Good to know" section down).

There is also the fact that you can call redirect & notFound functions in here (to force 404 or 301 codes on SSR), from the docs :

The redirect() and notFound() Next.js methods can also be used inside generateMetadata.

link : https://nextjs.org/docs/app/api-reference/functions/generate-metadata#returns

What i would suggest is to not do expensive operations in there or to cache your fetch/queries in there.


Note

if you really want to force next to not block on non SSR navigations but still have your correct title, you could do this hack i made that detects where the navigation is SSR with headers and skip the call to the fetch :

function isSSR() {
  return headers().get("accept")?.includes("text/html"); // for RSC navigations, it uses either `Accept: text/x-component` or `Accept: */*`, for SSR browsers and other client use `Accept: text/html` 
}

export async function generateMetadata() {
  const title = isSSR() ? (await fetch('http://...').then(r => r.json())).title : undefined
  return {
    title,
  };
}

export async function Page() {
  return (
      <React.Suspense
            fallback={<Skeleton />}
            key={props.searchParams?.filter}
          >
            <AsyncComp filter={props.searchParams?.filter} />
      </React.Suspense>
  )
}

And you can use a <title> tag inside your component and React will auto hoist them to the head (i also think it is the same for <meta> tags 🤔 ) :

async function AsyncComp(props: {filter?: string}) {
   const data = await fetch('http://...').then(r => r.json());
   return (
        <>
          {!isSSR() && <title>{data.title}</title>}
          {/*rest of your code...*/}
        </>
   )
}

Warning

I wouldn't recommend this for anyone, as it is a very hacky workaround and if you stream your data you will have a time window where you won't see the <title> update to the current page.

If you want to learn about React hoisting tags in the <head> , that was announced as a thing that react will add in the future, but there are already some bits in the react@canary version that next is using. You can read about it here : https://react.dev/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023#document-metadata

l-you commented 1 year ago

@Fredkiss3 Update on my previous reply. In the development mode, the issue disappeared. However, when we deployed our website to production, it turned out that nothing had changed. It's weird that the app directory currently exhibits such different behavior between production and development.

@rusted-love i also thought this was a bug, but this is expected, the solution is to use a different key to Suspense to force React to consider the Suspense as a different component each time and show the fallback, i've done something like that here : https://github.com/Fredkiss3/todo-app-beautiful-ux/blob/main/src/app/(routes)/(app)/%40todo_app/page.tsx#L49-L54

You could either encore the search params to string to set a unique key per search, or use Math.random() to have a random key each time and always show the fallback. Encoding the searchParams for the key could be interesting if the user refetch the same page, since the key is the same React won't show the fallback and the user will still see the old content, while when navigating to another page, it will show them a fallback.

I think this behavior is related to how Suspense work with transitions. the thing is that when using Suspense if the fallback have already been shown once, if you use a transition, React will hold the old data until the Suspense boundary has resolved, and Next uses transitions for all the navigations. Using a different key forces React to consider the Suspense as a different component.

This is from the React documentation here : https://react.dev/reference/react/Suspense#preventing-already-revealed-content-from-hiding

Fredkiss3 commented 1 year ago

Something like this happened to me, but it was a false alarm : i had to reload the browser at least 3 or 4 times to see the Suspense fallback. i used vercel so my assumption is that the cache had not updated yet.

You have to try to build locally and run next start and see the result in your local machine, if it works there then the problem might be on your host (maybe the cache or something).

It should work the same way on development and production. the code sample i gave you is deployed on vercel and it displays fallbacks on client navigation.

l-you commented 1 year ago

@Fredkiss3 This is bug in NextJs. It persists on the local production server. Additionally, we have debugged the dev server multiple times, and sometimes it exhibits different behavior with for the same pages.

Fredkiss3 commented 1 year ago

@rusted-love Sorry i may have lost the context with all these comments, the issue you have is that generateMetada is slow or is it the one where the Suspense fallback do not show between navigations ?

l-you commented 1 year ago

@Fredkiss3 the issue with suspense. It already discussed before by other participants. We decided to revert our app to the client side rendering for all routes.

unleashit commented 1 year ago

Just wondering if there is anyone that disagrees that the "ideal" behavior for a suspense boundary (either loading.js or manual) would be to show a fallback whenever its children aren't available, regardless of the reason being work is being done on the server, network delay, etc. If you have a reasoning this wouldn't be the "ideal" (technical issues aside), it would be interesting to hear.

In my mind anything short of fallbacks firing as soon as the parent DOM is available (during initial load or client transition) up until the at least the time the children begin to render on the screen is a bug. Sometimes a serious show stopping bug depending on the case. Updates based on search filters, etc. should probably be the same, although I feel at least in this case you have options to manually control the behavior if you like.

StampixSMO commented 1 year ago

Two cents from our practical implementation side: we are noticing significant delays after clicking a button that performs router.push(). In the old pages setup, you would usually set up a global router listener to show a loading state, but there's no recommended built-in way to do this with the app directory.

From a UX perspective, you just clicked a button and nothing happens for a while until suddenly your page jumps to a new one 2-3s later. Definitely cache related, since this only happens on new deploys (to Vercel) and navigation is immediate on subsequent navigations.

nachthammer commented 1 year ago

Hey I have the same issue, is there maybe any way to work around (so the moment I click on the link, some kind of loading state is shown). I wrote (almost) everything with server side components, so my app works properly without (almost) any JavaScript enabled in the browser. Is there any (maybe even dirty) workaround, so I do not have to migrate back to pages directory?

Fredkiss3 commented 1 year ago

@StampixSMO If you are manually call router.push you can wrap your call with useTransition, it will give you a isPending state that you can use to show a loading state.

An example usage :

import { useTransition } from "react" 

const Component = () => {
  const [isPending, startTransition] = useTransition()

  return <button onClick={() => startTransition(() => router.push("/destination")))}>{isPending ? "Navigating...": "Go to /destination"}</button>
}

But I would recommend using Link if possible to navigate between pages, as you don't have prefetching enabled for router.push.

Fredkiss3 commented 1 year ago

@DoctorManhattan123 if you have a loading state, then it is working normally... right ? What is the issue ?

You have to note : if you are on the latest version (next@13.4.4), there have been a rework of the way prefetching works, so by default if you have a link pointing to the destination path, you will see the loading state as soon as possible if the page have been prefetched (it is activated if the Link is in the viewport).

nachthammer commented 1 year ago

So my current component inside app/page.tsx looks like this:

export default function RedirectLoginCard() {
  return (
    <Link href={"/login"} className="group justify-self-center hover:brightness-95">
      ...
    </Link>
  );
}

And I have a loading.tsx at the same level. But when I click on this Link (if I have not clicked on it before). There is nothing happening for 1s, then the loading state of the loading.tsx is displayed. I tried it out on the latest next stable release and on canary. It is the same on both versions. I can try to convert all of the things using Link to client side and try the workaround with useTransition mentioned above.

Edit: I tried it out with useTransition and the pending state works. I will rewrite my navigation components like this for now. But I think it is definitely a good proposition to use loading.tsx at the same moment as the pending state of useTransition is set to true. As this is the only thing holding me back right now from doing everything server sided (server actions work very well for me btw :partying_face: ).

Fredkiss3 commented 1 year ago

@Fredkiss3 the issue with suspense. It already discussed before by other participants.

We decided to revert our app to the client side rendering for all routes.

@rusted-love i am sorry for that, there are also another bug with prefetching (this : https://github.com/vercel/next.js/issues/42991) where if next have already prefetched the page, it won't show any loading state or Suspense fallback after subsequent navigations, even for dynamic pages, with the semantics being that to recall the server, you have to wait for 30 seconds, and the timer is reinitialised if you navigate to the same page before the 30 secs have finished.

It may be because of that, though I don't know your case and this issue happens only when you use a Link component. If this is not your case, I would want to see if you can give a little reproduction as I want to test.

Thank you.