admin管理员组文章数量:1334923
I've looked into the Modal method described on this page in the NextJS docs , and at the answers for Stack Overflow questions like this one, but in all of these examples, when the page is refreshed the modal just appears as its own route, without the original content behind it. Is there a way I can set up the folder structure in such a way that:
- There is a page, e.g.
/lorem
, with a list and a button on it - A user clicks the button
- The URL changes to e.g.
/lorem/new
, and a modal appears above the list - If the user refreshes the page, the modal remains, but so does the list behind it.
I've have managed to achieve this using layout.tsx
to render the list, a page.tsx
that returns null
to handle the /lorem
route, and a nested new/page.tsx
to render the modal. However this brings about other issues relating to access to searchParams etc, and just generally feels very hacky, to have a layout acting as a page and a page returning null. So a more 'proper' way to do this would be ideal.
I've looked into the Modal method described on this page in the NextJS docs , and at the answers for Stack Overflow questions like this one, but in all of these examples, when the page is refreshed the modal just appears as its own route, without the original content behind it. Is there a way I can set up the folder structure in such a way that:
- There is a page, e.g.
/lorem
, with a list and a button on it - A user clicks the button
- The URL changes to e.g.
/lorem/new
, and a modal appears above the list - If the user refreshes the page, the modal remains, but so does the list behind it.
I've have managed to achieve this using layout.tsx
to render the list, a page.tsx
that returns null
to handle the /lorem
route, and a nested new/page.tsx
to render the modal. However this brings about other issues relating to access to searchParams etc, and just generally feels very hacky, to have a layout acting as a page and a page returning null. So a more 'proper' way to do this would be ideal.
2 Answers
Reset to default 11 +500You are trying to do a mix of intercepted and parallel routes and I think that's where the confusion lies.
Quick demo of my solution:
Here is my file tree:
├── app
│ └── lorem
│ ├── @modal
│ │ ├── (.)new <-- Intercepted Route (shown when you click the link)
│ │ │ └── page.tsx
│ │ │
│ │ ├── new <-- Parallel Route (shown when /lorem/new refreshed)
│ │ │ └── page.tsx
│ │ │
│ │ ├── default.ts <-- Default Behaviour (hides modal on child routes that do not have parallel routes)
│ │ └── page.ts <-- Parallel Route (hides modal on /lorem)
│ │
│ ├── new
│ │ └── page.tsx
│ │
│ ├── layout.tsx
│ └── page.tsx
│
└── ponents
├── list-content.tsx
├── modal-button.tsx
├── modal-window.tsx
└── new-item-modal.tsx
- Add a
layout.tsx
to yourlorem
directory and add the following code:
/**
* @/app/lorem/layout.tsx
*/
import Link from 'next/link';
import type { FC, ReactNode } from 'react'
interface LayoutProps {
modal: ReactNode;
children: ReactNode;
}
const Layout: FC<LayoutProps> = ({ modal, children,}) => {
return (
<div className="max-w-screen-lg mx-auto pt-10 space-y-10">
<nav className="text-center">
<Link href={"/lorem/new"}>Open modal</Link>
</nav>
<main className="w-full">
{modal}
{children}
</main>
</div>
);
}
export default Layout
- Your
/lorem
and/lorem/new
pages will have the exact same content: the list you wish to display.
/**
* @/app/lorem/page.tsx
*/
import ListContent from "@/ponents/list-content";
export default function RootPage() {
return <ListContent />
}
/**
* @/app/lorem/new/page.tsx
*/
import ListContent from "@/ponents/list-content";
export default function NewPage() {
return <ListContent />
}
Here's what my <ListContent />
looks like btw:
/**
* @/ponents/list-content.tsx
*/
import type { FC } from 'react'
const ListContent: FC = () => {
return (
<div className="text-center">
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
</div>
)
}
export default ListContent
Quick note about the
<ListContent />
ponent: If you refresh the page on/lorem/new
and then click the close button on the modal, the<ListContent />
will re-render. If you don't want the data for<ListContent />
to refetch or enter a loading state upon navigation from the non-intercepted version of/lorem/new
, I would remend adding some sort of caching mechanism that checks if data is stale or not before refetching.Otherwise, you may have to resort to using the method you've described where you add the
<ListContent/>
ponent toapp/lorem/layout.tsx
and then returning null for both@/app/lorem/page.tsx
and@/app/lorem/new/page.tsx
. In the interest of reducing jank, I would not remend this and opt for caching instead.
Create a
@modal
subdirectory withinapp/lorem
.Add a
default.ts
file (documentation here). Returning null ensures that the modal is closed when the active state or slot content is unknown (saves you the step of defining every single parallel route as returningnull
for every child route under/lorem
). Without this file, a 404 error may occur when the slot content is undefined.
/**
* @/app/lorem/@modal/default.ts
*/
export default function DefaultNewModal() {
console.log("I am the default modal view for when the layout state is unknown")
return null
}
- Next, we'll create a
page.ts
in the root of the@modal
folder. This will be the parallel route for the modal slot when you navigate to/lorem
. In this case, we don't want to show the modal slot, so we'll return null.
/**
* @/app/lorem/@modal/page.ts
*/
export default function RootParallelRoute() {
console.log("I am the modal slot on /lorem")
return null
}
- Before continuing with the intercepted route, let's create the modal ponents. I made a reusable
<ModalWindow />
wrapper with an optionalroute
prop which will allow me to use different behaviours for the close button depending on the context in which it is shown (this will make more sense in a moment, I promise)
/**
* @/ponents/modal-wrapper.tsx
*/
import type { FC, ReactNode } from 'react'
import ModalButton from "@/ponents/modal-button";
interface ModalWindowProps {
children: ReactNode
route?: string
}
const ModalWrapper: FC<ModalWindowProps> = ({ children, route }) => {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center mx-auto pointer-events-none">
<div className="fixed inset-0 bg-black bg-opacity-50" />
<div className="relative z-10 pointer-events-auto">
{children}
<ModalButton route={route} />
</div>
</div>
)
}
export default ModalWrapper
Here is the <ModalButton />
/**
* @/ponents/modal-button.tsx
*
* NOTE: this is the only client ponent in the app
*/
'use client'
import type { FC } from 'react'
import { useRouter } from "next/navigation";
import Link from "next/link";
interface ModalButtonProps {
route?: string
}
const ModalButton: FC<ModalButtonProps> = ({ route }) => {
const router = useRouter()
const handleClose = () => {
router.back()
}
if (route) {
return <Link href={route}>Close Modal</Link>
}
return <button onClick={handleClose}>Close Modal</button>
}
export default ModalButton
And finally, let's create our <NewItemModal />
ponent.
/**
* @/ponents/new-item-modal.tsx
*/
import { get as testEndpoint } from "@/app/api/test/route"
import ModalWrapper from "@/ponents/modal-wrapper"
interface NewItemModalProps {
route?: string
}
export default async function NewItemModal({ route }: NewItemModalProps) {
const data = await testEndpoint().json()
console.log("Modal fetched data:", data) // Returns the "Hello, World!" message from the test API route
return (
<ModalWrapper route={route}>
<div className="container mx-auto">
<div className="rounded-md m-4 bg-sky-600 p-4">
<p>Behold! A modal window :-)</p>
</div>
</div>
</ModalWrapper>
)
}
Quick note about the
route
prop: If we only relied on the history stack of the user's browser for the "Close" button (viarouter.back()
), the user will end up being stuck if they are visiting/lorem/new
via manually inputting the URL into their address bar (ie, opening a new tab). This is why we have theroute
prop being used in the parallel route so that we can redirect them to the root/lorem
page where, if they open the modal again, they'll see the intercepted route version of the modal that has therouter.back()
behaviour from that point on.
- Next, we're going to add the modal to two places. The first one will be the intercepted route, which will show the
modal
when you click the "Open modal" button from the/lorem
path.
Do this by creating a directory named (.)new
under @modal
and creating a page.tsx
file for it.
/**
* @/app/lorem/@modal/(.)new/page.tsx
*/
import NewItemModal from "@/ponents/new-item-modal";
/**
* Shows when the /new route is intercepted
*/
export default function InterceptedPage() {
console.log("I am the modal that is shown during interception")
return <NewItemModal />
}
- Lastly, we'll create a new directory at
app/lorem/@modal/new
which will serve as the parallel route for the@modal
slot when viewing/lorem/new
after a refresh.
/**
* @/app/lorem/@modal/new/page.tsx
*/
import NewItemModal from "@/ponents/new-item-modal"
/**
* Shown on /lorem/new on fresh page load (refresh, open in new tab, etc.)
*/
export default function ParallelRoutePage() {
console.log("I am the modal that is shown when the page is refreshed")
return <NewItemModal route={'/lorem'} />
}
Using this method, you can have other modals use the @modal
slot to not only persist the modals, but also be able to link to them like with /lorem/new
.
For instance, adding a modal for the purpose of editing list items would result in a directory tree like this:
├── app
│ └── lorem
│ ├── @modal
│ │ ├── (.)edit
│ │ │ └── page.tsx
│ │ │
│ │ ├── (.)new
│ │ │ └── page.tsx
│ │ │
│ │ ├── edit
│ │ │ └── page.tsx
│ │ │
│ │ ├── new
│ │ │ └── page.tsx
│ │ │
│ │ ├── default.ts
│ │ └── page.ts
│ │
│ ├── edit
│ │ └── page.tsx
│ │
│ ├── new
│ │ └── page.tsx
│ │
│ ├── layout.tsx
│ └── page.tsx
│
└── ponents
├── edit-item-modal.tsx
├── list-content.tsx
├── modal-button.tsx
├── modal-window.tsx
└── new-item-modal.tsx
While there is a teensy bit of repetition/redundancy using this method, it offers the opportunity to define different behaviour (like transitions) as well as different content for the various contexts in which your modal will appear.
For instance, let's say you want the <EditItemModal />
to only show if the route is intercepted:
- Keep the intercepted route (
/lorem/@modal/(.)edit/page.tsx
) - Delete the parallel route (
/lorem/@modal/edit/page.tsx
) - Add your edit form to the edit page (
/lorem/edit/page.tsx
)
Hope this gets you headed in the right direction!
This may also not be the proper way to do that, but it works without a layout acting as a page and a page returning null:
Create a directory named [...slug]
for example, containing a page.js
file (should of course also work with page.tsx
). So the directory structure would look something like this:
The naming ensures that all paths and subpaths are served by that page.js file. In that file you can get the entered path and decide which content to show or to hide. If you refresh the website at /lorem/new
, it still shows the regular content and the modal. You'll probably want to add style according to your preferences.
src/app/[...slug]/page.js:
"use client";
import { useRouter } from "next/navigation";
export default function Page({ params }) {
const router = useRouter();
return (
params.slug[0] === "lorem" && (
<main>
<ul>
<li>regular list content</li>
<li>regular list content</li>
<li>regular list content</li>
<li>regular list content</li>
<li>regular list content</li>
<li>regular list content</li>
<li>regular list content</li>
<li>regular list content</li>
<li>regular list content</li>
<li>regular list content</li>
<li>regular list content</li>
<li>regular list content</li>
<li>regular list content</li>
</ul>
{params.slug.length === 2 && params.slug[1] === "new" ? (
<>
<button onClick={() => router.push("/lorem")}>close modal</button>
<dialog style={{ display: "block" }}>This is only visible in /lorem/new</dialog>
</>
) : (
<button onClick={() => router.push("/lorem/new")}>show modal</button>
)}
</main>
)
);
}
It has to be considered, that the information, that the modal is shown, must be passed to the server somehow. In this solution that happens with the new
part of the URL. But you could also use searchParams for that for example, then you wouldn't have to use a [...slug]
directory.
本文标签: javascriptNextJS App Routingmodal that persists on refreshStack Overflow
版权声明:本文标题:javascript - NextJS App Routing - modal that persists on refresh - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1742376894a2463340.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论