Closed mkarajohn closed 11 months ago
hey @mkarajohn you can dismiss a modal like this via adding a catch-all route, see https://nextjs.org/docs/app/building-your-application/routing/parallel-routes#dismissing-a-modal
it doesn't work in your example because the router maintains routes already mounted until they their slot navigates
@feedthejim Hi again, sorry, I am new to Next and maybe I am still misunderstanding, but the docs read:
you can dismiss the modal by calling router.back() or by using a Link component.
In the case described in the issue, I am using a Link
component, the route changes, like it would if I had used router.back()
but the change does not get reflected to the UI, unlike when using router.back()
. Sorry for insisting, but the docs say "or by using a Link component" and this is clearly not the case here.
I added a catchAll route as per your suggestion, and I tried moving it around from app/
to app/@modal/
to app/@modal/(..)photos/
and see in which dir it will work as described and it did not work.
Either I am doing something very wrong, which is very likely and in which case I would appreciate if you could make the adjustment in the repro codesandbox so that I can see or explain it in a little more detail, or something is not working as described, or the docs are saying something that is not true.
Thank you
I tried changing the Link
with
<button
className="action"
onClick={() => {
router.push(`/`);
}}
>
Close
</button>
and this still does not work, while
<button
className="action"
onClick={() => {
router.back();
}}
>
Close
</button>
this works.
So, router.back()
(moving back in the history stack) and router.push()
(adding to the history stack) behave differently somehow for this case? They both change the url to /
in the end. I am confused at how it's determined that in one case the route changed while in the other it's treated as if it did not.
+1 I'm having this issue as well. Like OP, I've also tried using the the catch all route and putting default.tsx in various places but nothing works.
The only way for me to close the modal seems to be to use router.back() since using a Link to the previous page changes the URL but leaves the modal opened.
Yeah as far as I can tell catch all routes just don't work for clearing an intercepted route. See https://github.com/vercel/next.js/issues/48719 and https://github.com/vercel/next.js/issues/49531
I am also having this issue. I found a workaround by grouping the intercepted route and placing the target route outside of this group.
@vetledv Can you show how? (using something simple, like the reproduction repo in the OP)
@feedthejim considering other people are reporting this too, maybe this should be reopened?
@mkarajohn I see now that it only works in my specific use page (I was linking to an equivalent of /hello in this example). Here is a repo that I believe should cover all the behaviours. This does very much not seem like intended behaviours. https://github.com/vetledv/repro-intercept
@vetledv thanks for this, very informative, I played with it a little. Like you said, the grouping is the only thing that seems to be "fixing" this behaviour, nice find. The [...catchAll]
route doesn't seem to do anything, can be deleted without effect.
And, again, like you said, it does not seem like "intended behaviour" considering you cannot return to /
using a Link
, since it's in the same group.
I suppose the only reason it works is because it is no longer rendering the layout containing the modal. And yeah, the catchAll or any of the suggestions does not seem to work. Would like to see this reopened.
Just thought of it now, I guess it's worth noting that you could make the layout a client component and conditionally render the modal based on the pathname to fix it entirely. Hopefully that could help for now until it's sorted out.
//(some-group)/layout.tsx
"use client";
import { usePathname } from "next/navigation";
export default function GroupLayout({
children,
modal,
}: {
children: React.ReactNode;
modal: React.ReactNode;
}) {
const pathname = usePathname();
const shouldShowModal = pathname.includes("/photos/");
return (
<div>
{shouldShowModal && <div>{modal}</div>}
<div>{children}</div>
</div>
);
}
@feedthejim Why is this issue closed? This is still a problem.
Also, it is somehow related to what @mkarajohn is saying - router.push()
doesn't seem to work either. Only router.back()
does, but in my case, I don't want to use router.back()
. When someone gets sent the link with the modal opened then they click on the button to close it, router.back()
doesn't make sense. It would be better to call router.push()
and navigate the user to the route behind the modal.
UPDATE: I also noticed that the value, returned by the useSelectedLayoutSegment
hook doesn't change when navigating away from the parallel route with router.push()
. The only workaround currently is to make the layout a client component, and then write some additional logic to conditionally render the parallel route segment, based on the pathname, returned by the usePathname
. I believe I saw this solution in some other thread but now I can't find it.
This issue is also related to the following one: https://github.com/vercel/next.js/issues/49731
Has there been any update or progress on this? Ive just been implementing some parallel / intercepting routes for a model preview and discovered it not possible to navigate from the preview modal to the actual page. I think this is the same issue as this thread unless i'm missing something with how to navigate from within the modal 🤷
Working setup I have is here: https://github.com/lmatteis/nextgram
Apparently you need the right page.js that returns null
at the right level to tell the slot to not render anything.
This still does not work if...
The route you are intercepting and showing a modal is article/[articleId]
and you have an overview page with a bunch of articles to pop a modal up on categories/[categoryId]
.
We then want to close that modal by doing a router.push('/categories/animals')
since it's where you opened the modal from. Nothing happens. No unmount etc...
Why do we need this? Let's say you are in a modal and have a bunch of related articles. You are now routing from article/1
to article/2
, then article/3
. We want to close the modal entirely... A router.back()
is not sufficient because we don't want to go from article/3
to article/2
but unmount the modal.
You could do a history.go(-3)
which acts like a history back where -3
is the "nested" level within the modal since modal entry but that means you will have to track a separate history state which is flaky.
+1 The Modal slot mechanism is buggy. I have forced redirects or used router.back, while the url changes, the ModalInterceptor always get executed. Sometimes, the new route Im trying to go to gets properly executed and others it never does, as if it ended on the intercepted route and thats it.
Here's a tiny example that reproduces the issue:
https://codesandbox.io/p/sandbox/nifty-bessie-d9632z
Adding @modal/[...catchAll]/page.tsx
as suggested in the docs here didn't help.
Don't want to use router.back due to the reasons described by @tiersept
Here's a tiny example that reproduces the issue:
There's something wrong with the setup. Hard navigating to test/dashboard/modal1
gives a 404. Mind sharing a git repo instead?
Overall though I think you just need to put a page.tsx
that returns null directly inside the @modal folder.
@lmatteis
There's something wrong with the setup. Hard navigating to test/dashboard/modal1 gives a 404. Mind sharing a git repo instead?
Here's a repo with the files from the sandbox:
https://github.com/tomas-c/nifty-bessie-d9632z
Adding /app/test/dasbhoard/default.tsx
makes hard navigation work.
Overall though I think you just need to put a page.tsx that returns null directly inside the @modal folder.
That's helped, thanks! I've created a branch update-1
on that repo with both changes applied:
https://github.com/tomas-c/nifty-bessie-d9632z/tree/update-1
It works nearly perfectly now.
There's one remaining issue for me with using this strategy for modals. It can be reproduced on the same update-1
branch):
If you hard navigate to /test/dashboard
, click on /test/dashboard/modal1
and then click on /test/dashboard
then the "children slot" stays the same throughout and renders "Page for dashboard" - great!
Screencast from 09-06-23 19:35:13.webm
But, if you hard navigate to /test/dashboard/modal1
, click on /test/dashboard
and then click on /test/dashboard/modal1
then the "children slot" switches from "Default for dashboard" to "Page for dashboard" and then back to "Default for dashboard"
Screencast from 09-06-23 19:38:13.webm
This means that in the second scenario, opening and closing a modal, makes main page component lose state.
@tomas-c It seems you want two pages to show this input field. Why not just put it in layout?
Ran into the same issue and the usePathname
technique mentioned by @vetledv worked for me. 🙏 🙏 🙏
Would be nice to have a proper way though
I fixed this by making the modal itself a client component and checking if it should show based on the pathname
// /@modals/(.)posts/create/page.tsx
"use client";
import { usePathname } from "next/navigation";
export default function Dialog() {
const pathname = usePathname();
const shouldShowModal = pathname.includes("/posts/create");
if (!shouldShowModal) return null;
return (
<div>my modal content</div>
);
}
Definitely a bug though, I shouldn't have to do this. Are intercepting routes ready for production?
I fixed this by making the modal itself a client component and checking if it should show based on the pathname
// /@modals/(.)posts/create/page.tsx "use client"; import { usePathname } from "next/navigation"; export default function Dialog() { const pathname = usePathname(); const shouldShowModal = pathname.includes("/posts/create"); if (!shouldShowModal) return null; return ( <div>my modal content</div> ); }
Definitely a bug though, I shouldn't have to do this. Are intercepting routes ready for production?
This fix seems to create an issue (infinite loop), if I navigated from the modal to another page, then I re-open the modal again (now on the new page) when trying to dismiss the modal with router.back
(as we usually do when clicks on the backdrop) the modal component get render again and again (infinite loop).
Looks like it is somehow broken by design, hm. what if there are two intercepting routes, say, one to /sign-in
and another one to /sign-up
, which both lead to a modal dialog, and there is a link from signin modal to signup modal and in other direction as well, which is a common scenario))
I completely agree with the real-life scenarios, I was working on an authentication modal and found myself in exact same situation and been searching for a solution. At the end, I decided to take my own approach. My solution is to :
static
and dynamic
route pathnames in a config file.ModalProvider
using React's createContext()
.usePathname()
to listen for pathnames and check if they correspond to modal pathnames.lastPathnameBeforeModal
and isModalActive
through the provider.lastPathnameBeforeModal
prop to a ModalClose
button component.isModalActive
prop to a ModalOrNot
component.Here is my repo about the issue with an authentication modal for static and photos showcase for dynamic routes : ilkergonenc/nextjs-dismiss-router-modal-workaround
I am working on to improve this solution and transform the provider into a small, reusable package and I am open to any ideas, suggestions.
I know this is bit out of discussion but we also tried implementing this in our e-commerce but now we are trying to evaluate if the use case was right
in our case we made something that product details view would show product image and pricing details and some few other thing+add to cart button in the intercepted route
and in the actual route::
we would like like to see all the above + things like product description suggested products reviews..etc..
we tried finding way to expand from intercepted route to actual route and we could not find... (maybe something like (intercept=false so default behavior is intercept=true for backward compatibility) to the Link as a prop i don't know how easy that is of course to implement
That could maybe also be tackled together with this coz we even ended up adding router called mini-product/[id]
to intercept then route.push could go to product/[id]
but as the issue suggest that was not possible and also beats the all logic of our intended use case for instance
What I understand, it seems that you have implemented intercepting routes for the quick view modal to provide a convenient way to preview your products. However, this implementation is now causing an issue where you are unable to navigate to the actual product page because the interception takes precedence over regular navigation.
Actually, this situation presents an interesting real-life scenario. The concept of parallel and intercepted routes is relatively new, and determining their appropriate usage is a learning process for us to discover.
Right now, I'm not sure about any available options to bypass these intercepting routes as you mentioned. It would indeed be a great feature to have the flexibility and control over these routing options, allowing us to choose between different usage scenario by providing customizable routing options.
I will look for potential workarounds and if I come across any viable solutions, I will definitely let you know and add to my repo.
@ilkergonenc yea.. that is exactly what we are doing and issue facing.. yea.. will appreciate if you find workaround and let me know
Hİ @kimutaiRop , I searched on this around the blogs and I tried a lot of different workaround but I couldn't find a way to achieve this issue but It made me understand the intercepted and parallel routes conventions and how they work. For your case I believe the best solution is to build an old-school modal and with many other new features it would be still very easy and performant. With the new features we can make the modal client component but the set the quick view data as a server component and use new next/navigation useRouter hook to replace the url and achieve nearly the same outcome as the intercepted router offers.
Looks like it is somehow broken by design, hm. what if there are two intercepting routes, say, one to
/sign-in
and another one to/sign-up
, which both lead to a modal dialog, and there is a link from signin modal to signup modal and in other direction as well, which is a common scenario))
I had the same problem. The only solution was to use a single Link component to intercept the initial route, either /sign-in
or /sign-up
, and then use anchor tags inside the modal instead of Link components. This way, the subsequent route won't be intercepted.
Also if you have two intercepting routes, /sign-in
and /sign-up
, both leading to modal dialogs, and you try to redirect to /sign-in
after a successful signup, the signup modal keeps popping up, intercepting the signin page. So I don't think it's production ready yet.
Should really mark it as experimental feature considering how unfinished intercepting/parallel routes are. I've wasted too much time trying to get this to work.
Has there been any word of progress on this? this seems annoyingly buggy, especially for something outright recommended in the docs :(
@Apollo-XIV I was able make intercepted routes unmounted by creating dummy page routes under the @slot
folder. For example, if we have page.tsx
, foo/page.tsx
and bar/[id]/page.tsx
, creating @modal/page.tsx
, @modal/foo/page.tsx
and @modal/bar/[id]/page.tsx
helps with unmounting. If we open @modal/(.)foobar/page.tsx
and then navigate away from the intercepted /foobar
route, Next.js renders another route in the @modal
slot.
You can find an example in https://github.com/vercel/next.js/issues/53170. It’s a bug report related to multiple route groups, but if you have just one group with one layout, the trick with dummy routes might do the unmounting for you.
@Apollo-XIV I was able make intercepted routes unmounted by creating dummy page routes under the
@slot
folder. For example, if we havepage.tsx
,foo/page.tsx
andbar/[id]/page.tsx
, creating@modal/page.tsx
,@modal/foo/page.tsx
and@modal/bar/[id]/page.tsx
helps with unmounting. If we open@modal/(.)foobar/page.tsx
and then navigate away from the intercepted/foobar
route, Next.js renders another route in the@modal
slot.You can find an example in #53170. It’s a bug report related to multiple route groups, but if you have just one group with one layout, the trick with dummy routes might do the unmounting for you.
You are an absolute lifesaver, you've easily saved me hours of finding a workaround. Thank you sm :))
This is still a big issue. Working off the oficial nextgram example for reference, if you want to link away to a different part of the app from the intercepted, parallel route modal you're just stuck with it wherever you go...
Catch-all pattern suggested by the docs seems to do nothing. Placing a null returning page.tsx
like @lmatteis suggested works if you're linking back to /
but if you're linking somewhere else in the app like /dashboard
then the modal just stays open...
This is still a big issue. Working off the oficial nextgram example for reference, if you want to link away to a different part of the app from the intercepted, parallel route modal you're just stuck with it wherever you go...
Catch-all pattern suggested by the docs seems to do nothing. Placing a null returning
page.tsx
like @lmatteis suggested works if you're linking back to/
but if you're linking somewhere else in the app like/dashboard
then the modal just stays open...
Okay so, in case it helps anyone, the only way to handle this right now is to isolate the @slot
and the layout.tsx
which uses it in it's own space (either nested or using a route group), so that this layout.tsx
does not cover the pages where your modal is not required. The nextgram example isn't good for this because it's the root layout that uses the slot (modal) so all pages you make in there will be covered by it.
I am trying to recreate the image gallery built in Unsplash:
<Link>
to navigate to the selected oneIt works fine, back/forward buttons are working as expected, but when I click on the close button or overlay I cannot find a way to close all modals at once. Right now, you have to click the amount of times you clicked to open each image to dismiss it.
For this, I created a route provider which stores the last pathname I had before opening a modal. When I want to close the modal, I simply used route.push(pathname)
. But, sadly, the push method doesn't work as expected. I thought it would have the effect of router.back()
.
Shouldn't the push method work that way?
Still not figured it out so any help is appreciated. Meanwhile I managed to get the behaviour I needed using a context provider which tracks the amount pathname
changes made inside the modal. When I want to close the modals, I loop the counter, smth like this, since router.back()
is the only way to close this intercepted routes:
if (clickCount === 0) {
router.back()
}
else {
for (let i = 0; i < clickCount; i++) {
router.back()
}
}
clearClickCount()
So far it works pretty good with no glitches. However, I am hoping for official support on this issue.
I also have an issue with intercepting and parallel routes: I've got a creation form that can be opened in a modal, using an slot with an interceptor route (so the user can access said form on any point within the app) but I've also got a normal route in case the user decides to bookmark the URL or visit it directly typing it in the browser.
My issue us that navigating to the URL by typing it directly in the browser bar causes the modal to render on top of the actual route. Not sure if I am doing something wrong here but are interceptor routes not supposed to be triggered by client navigation (ie using or router.push) as opossed by visiting the URL directly?
I also had a headache with this problem, but solved it by using dynamic routes as follows. (Sorry it's hard to understand as it's code for a service I'm working on)
The complete source code is posted next. https://github.com/kght6123/ors/tree/main/app/(dataEntry) I don't think this will be a complete solution, but it will be a temporary measure.
Having same issue. Wasn't able to fix this with using Link - went back to router.back till it works
Same issue. In my case I'm trying to redirect to another page using router.push()
however, while it redirects to the correct page, the modal stays up. My solution to this for now is just checking the current pathname like other users suggested.
Any update on this? This issue still makes the feature completely unusable for many use cases, or extremely convoluted at minimum with all the workarounds needed.
I have also found another issue that simply breaks the router history if stumbled upon. My previous repo that was linked in this thread a while back has been updated to latest 13.4.20-canary.31. Repo: https://github.com/vetledv/repro-intercept
To reproduce this issue:
Example: Navigate to three random pages. Refresh the third page. Go to second page, won't render. Refresh. Go back to the first intercepted page. Also won't render.
I see now that this must be similar to @extrabright 's issue. Maybe a feature to pop all intercepted routes would be beneficial? Edit: Specifying if you want to intercept or not could be nice maybe. This issue would be avoided if it wasn't possible to intercept from an intercepted route.
Intercepted routes is perfect for so many things I want to do, so I would love to see these issues ironed out. I would probably have no chance, but if someone can point me to the right places I would love to at least try to help out here.
Intercepted routes is perfect for so many things I want to do, so I would love to see these issues ironed out. I would probably have no chance, but if someone can point me to the right places I would love to at least try to help out here.
I agree. One of the biggest selling points of migrating to NextJS app/
directory for me was the intercept/parallel routes feature. It's a bit disheartening right now to have fully migrated to it and so many things are broken.
If these aren't fixed, I will ultimately just have to go back to the old way of doing things (parallel/intercept routes with useSearchParams -> regular React state/manually change URL).
You don't have to use Link to close modal, see my implementation of intercepting modal Live: https://nextflix-blush.vercel.app/
@Apollo-XIV I was able make intercepted routes unmounted by creating dummy page routes under the
@slot
folder. For example, if we havepage.tsx
,foo/page.tsx
andbar/[id]/page.tsx
, creating@modal/page.tsx
,@modal/foo/page.tsx
and@modal/bar/[id]/page.tsx
helps with unmounting. If we open@modal/(.)foobar/page.tsx
and then navigate away from the intercepted/foobar
route, Next.js renders another route in the@modal
slot.You can find an example in #53170. It’s a bug report related to multiple route groups, but if you have just one group with one layout, the trick with dummy routes might do the unmounting for you.
This is exactly what I needed to do to workaround this without additional code. I was missing @modal/page.tsx but had a page.tsx file in all subsequently nested @modal routes. e.g) @modal/(.)add/page.tsx, @modal/(.)edit/page.tsx.
It is not documented in Next docs this is required which is perhaps the biggest culprit that could easily be updated to ease a lot of developer pain from this.
So in summary for my case, the @modal needed a page.tsx null return similar to default.tsx to wipe out the intercepted route with router.push()
after saving in the modal without needing router.back()
or any other code workarounds.
If you aren't use .tsx but .ts or .js in this file structure you have to have them match all the way through for it to work too.
I am not convinced this is really a bug. If something is mounted into a slot I don't think it should be unmounted if slot still exists, which is the case in such setups where modal slot is on root level and one navigates from intercepted route /images/[id]
to root /
. Imho appropriate solution is to just move slot one level down. Eg. in a hypothetical case of a blog with expandable images, that would mean going from this:
|-@modal
|-- (.)photos
|-blog
|-photos
to this
|- blog
|-- @modal
|--- (...)photos
|-photos
Still not figured it out so any help is appreciated. Meanwhile I managed to get the behaviour I needed using a context provider which tracks the amount
pathname
changes made inside the modal. When I want to close the modals, I loop the counter, smth like this, sincerouter.back()
is the only way to close this intercepted routes:if (clickCount === 0) { router.back() } else { for (let i = 0; i < clickCount; i++) { router.back() } } clearClickCount()
So far it works pretty good with no glitches. However, I am hoping for official support on this issue.
Can you tell me how can you do this?
Okay, I have done some significant work here to make this work for me. Here is the context:
I will do everything in my power to stay away from client-side state management. I love using routes to manage all of my locational state (including modals). I noticed that whenever I was doing this, redirect("/profile")
did not redirect me back! Instead, my modals stayed open.
Using hints from above, I discovered that what is really important is the layout
on which your modal is being rendered. That is, you should be rendering your slot in the layout
that minimally contains your intercept routes.
So, if I have a route structure like such:
/profile
/profile/group/[id]/edit // A modal.
/profile/group/[id]/delete // A modal.
I really only want to be slotting in the minimal containing folder. Here, that would be group
. Let's discuss why we want this.
Let's start by describing our file structure. Here, I lay out what we do not want.
/profile
/profile/layout.tsx
/profile/@dialog/(.)group/[id]/edit
/profile/@dialog/(.)group/[id]/delete
The layout.tsx
here looks something like...
import { ReactNode } from "react";
export default function Layout({
children,
dialog,
}: {
children: ReactNode;
dialog: ReactNode;
}) {
return (
<>
{children}
{dialog}
</>
);
}
Now, if I am in the edit
modal and I redirect back to /profile
, am I changing the layout properties? No. I might argue that I should be, but right now you are not. Instead, you are just saying "rerender the layout in the state you already have", which still contains your slot.
I'll keep the answer simple: we want to use route groups.
/profile
/profile/layout.tsx
/profile/(modal)/layout.tsx
/profile/(modal)/@dialog/(.)group/[id]/edit
/profile/(modal)/@dialog/(.)group/[id]/delete
There are some nuances here. First, remember that we do not want the profile/layout.tsx
to be rendering the @dialog
slot. Do not do that. Instead, use the (modal)/layout.tsx
to render your dialog slot. Second, do not use (modal)/layout.tsx
to render children either. Only use it to render your dialog slot.
// /profile/layout.tsx
import { ReactNode } from "react";
export default function Layout({
children,
}: {
children: ReactNode;
}) {
return (
<>
{children}
</>
);
}
// (modal)/layout.tsx
import { ReactNode } from "react";
export default function Layout({
dialog,
}: {
dialog: ReactNode;
}) {
return (
<>
{dialog}
</>
);
}
Like @IvanRomanovski said- this is likely not a bug, we're just not using the system right. Now, that's arguably a bad take from a DX perspective and I do think that this should be changed. But hopefully this sheds some light onto the complexities behind this.
Verify canary release
Provide environment information
Which area(s) of Next.js are affected? (leave empty if unsure)
App directory (appDir: true), Routing (next/router, next/navigation, next/link)
Link to the code that reproduces this issue
https://codesandbox.io/p/github/mkarajohn/nextgram/draft/restless-leftpad?file=/components/frame/index.js
To Reproduce
Describe the Bug
When you click the
Link
and the URL changes to/
the interceptor route remains mounted and does not go away.Expected Behavior
I would expect that since the route was changed (via
Link
no less) the interceptor page should disappear once the route that it was intercepting was changed.Screencast from 11-05-2023 04:23:04 ΜΜ.webm
Which browser are you using? (if relevant)
No response
How are you deploying your application? (if relevant)
No response
NEXT-1160