RFC: Allow layout router to retain state when navigating within dynamic segments #50711
Replies: 10 comments 18 replies
-
@timneutkens I have made the necessary changes on my local (canary branch) following the contributor guidelines. I implemented it so that it only applies when the configuration |
Beta Was this translation helpful? Give feedback.
-
Hello, i am also having the same problem ! i am having a hard time understanding how i should implement this is my layout , can you give a littile more detailed example ? |
Beta Was this translation helpful? Give feedback.
-
This bit us while trying to upgrade one of our internal apps as a PoC to the app router. It also affects our other internals apps. Here's a simplified example. We have a section of our app that is fully dynamic.
Hoping to see a solution to this. ps: Hoping that layout.tsx (and page.tsx) can preserve state for those using layout given that anyone who wants the unmount/mount behavior can simply use template.tsx as per the documentation. |
Beta Was this translation helpful? Give feedback.
-
@jkuhs Thanks for writing this proposal up. Our current thinking about why the behavior today makes sense is that usually when a dynamic segment changes the page represents a new object. Not keying the page can lead to security issues for instance retaining state about one account while updating the view to show a different account. The use case of locale is somewhat unusual in that it isn't describing a new object, it is just contextualizing the page with some information that alters it's appearance generally. However the more common cases are that the dynamic segment means the identify of the page is distinct. If there are shared components in a page or layout that feel like they are not part of that identity then they can probably be lifted into a layout higher up that is not a child of the dynamic segment. Route groups can be useful if you want to lift up the layout but not share it with every other route at that higher level. This is in contrast to search params which are not part of the key but are also not available in layouts, only pages. These can be updated without resetting the state of the tree but it also means that the searchParams can only be used "higher up" in client components where updates to the location can be reacted to without refetching the route. So if we start from the perspective that components not directly tied to the identity represented by the dynamic segment should not remount when that dynamic value changes the first step is to see if that Component can be modeled as being part of a shared layout above the dynamic segment itself. I'd be curious to know if you have cases where this is not functional or practical? this doesn't address the internationalization case though and it's something we have discussed internally and don't yet have a plan to communicate how we will address it but understand that we recognize it is not the same category of dynamic param use as most things are and it may require a more deliberate solution rather than making users choose to change dynamic param handling via config for instance. Your proposal opened with this example but it lays out a number of examples later that have more to do with blog posts. Was your motivation to open this primarily originating from the locale use case or did you encounter this in other contexts? |
Beta Was this translation helpful? Give feedback.
-
I provided my feedback in another discussion about the same issue: There are no workarounds for the use cases I listed. @jkuhs summarizes the issue and its side effects very well. Had exactly these issues. |
Beta Was this translation helpful? Give feedback.
-
This RFC is almost 10 months old and clearly has some strong arguments. Can we conclude this before it goes stale? What's the Next.js teams plans? |
Beta Was this translation helpful? Give feedback.
-
We should do this. |
Beta Was this translation helpful? Give feedback.
-
I think for now at least I have basically come to terms with that the bridge between server and client has been built but it's missing some pillars and has collapsed in certain places. I think for now I will have to accept that regarding the locale it is going to behave much like any other server rendered app except you do not see a hard reload. I think the best ux experience would probably be to put the local switch in a place where a page does not contain stateful content. This will at least prevent confusion or jarring layout shifts. If somehow someday we can bridge this gap I will finally be a proud app router user. But now I need to think about the existing apps in our company which are going to need i18n support. Of course keeping in mind that for the average user the accept language headers are more than sufficient to detect the correct language and preventing the user from actually clicking on the locale switch button. I don't know how often locals switch buttons are used and your websites or web apps I've never kept track of this metric. |
Beta Was this translation helpful? Give feedback.
This comment was marked as off-topic.
This comment was marked as off-topic.
-
I running into this issue as well. I have a |
Beta Was this translation helpful? Give feedback.
-
Goal
Allow client-side navigation within the same dynamic segment without all components being unmounted/remounted.
The Issue
Currently, when navigating client-side from one path to another within the same dynamic route segment, e.g. from
/en
to/de
, the layout router will cause all components on the page to get unmounted and remounted.More specifically, the layout router renders
<TemplateContext.Provider key={createRouterCacheKey(preservedSegment, true)}>
whereas the cache key created bycreateRouterCacheKey
includes the dynamic values when within a dynamic segment.Consider the following example:
/app/[lang]/page.js
/en
['lang', 'en', 'd']
lang|en|d
/app/[lang]/page.js
/de
['lang', 'de', 'd']
lang|de|d
With the above example, navigating client-side from
/en
to/de
will cause all components on the page to get unmounted and remounted, even though we remain in the same dynamic segment ([lang]
) and on the same page. This is due to the cache key including the dynamic value, i.e. changing fromlang|en|d
tolang|de|d
.The same is applies for any other files, such as
layout.js
, that might be located in a dynamic segment.This is problematic for various reasons, including:
useParams
,useSearchParams
) has changed, is wasteful and unnecessary.useState
anduseRef
). Oftentimes, that state is used for context providers that wrap the application near the "root" level. Remounting the whole component tree results in all that state being lost and being re-initialized (or even failing).useEffect
's defined by remounted components will run again, possibly causing undesired side effects and unnecessary resources to be used (e.g. redundant requests, repeated init logic, etc.).Background
The page router's
_app.jsx
behaviour is to keep state between navigations and to instead manually pass <Component key={path} {...pageProps} /> to force a state reset on navigation.Personally, I think the page router's approach is correct and it should be up to developers to force a state reset when and where they need it, rather than Next.js forcing it.
Proposal
Update the logic in the layout router and the
createRouterCacheKey
function to omit any dynamic values when using the cache key askey
property on<TemplateContext.Provider>
. Effectively, that will make it work the same as it already does for search parameters.Using the initial example from above, the created cache key would be
lang|d
for both the/en
and the/de
routes (instead oflang|en|d
andlang|de|d
respectively).Updating the logic could simply be implemented with a change along the lines of (simplified):
If there are concerns about omitting dynamic values of dynamic segments by default, then a compromise would be to add a configuration option for developers to opt-in to the behavior. For example:
Examples of various routes, their cache keys without dynamic values, and the resulting React tree
The purpose of the following examples is to demonstrate that omitting the dynamic values is OK and doesn't cause issues due to conflicting cache keys.
Keep in mind, that we are only omitting the dynamic values when creating the cache key for the
key
property on<TemplateContext.Provider>
. Cache keys created for other purposes (e.g. route specific data caching) will still include the dynamic values!Note, that the React trees below are simplified and only show the relevant
<TemplateContext.Provider>
components with theirkey
properties. There are other components and properties rendered in the layout router that are not shown here since they are not relevant to illustrate the issue at hand.Static page with search parameters
This already works today. The dynamic part (
?{"my-search-param":"true"}
) is omitted./app/blog/page.js
/blog?my-search-param=true
__PAGE__?{"my-search-param":"true"}
__PAGE__
Root dynamic segment
/app/[path]/page.js
/blog
['path', 'blog', 'd']
path|d
Root dynamic catch-all segment
/app/[...path]/page.js
/blog
/blog/one
['path', 'blog', 'c']
['path', 'blog/one', 'c']
path|c
Root dynamic optional catch-all segment
/app/[[...path]]/page.js
/blog
/blog/one
['path', 'blog', 'oc']
['path', 'blog/one', 'oc']
path|oc
Nested dynamic segment
/app/blog/[path]/page.js
/blog/one
blog
for the static `blog` directory segment['path', 'one', 'd']
for the dynamic `[path]` segmentpath|d
Beta Was this translation helpful? Give feedback.
All reactions