do something once server action is completed

Is there a way to achieve this (see title)? I’m using useTransition to start the action and need to close my modal once the action is completed successfully.
100 Replies
rocawear
rocawear13mo ago
https://react.dev/reference/react/useTransition
const [isPending, startTransition] = useTransition()
const [isPending, startTransition] = useTransition()
can you take it from the isPending?
Perfect
Perfect13mo ago
@ruhap I am using ispending, but how do I close the modal when I know isPending goes from true to false? I might be drawing a blank if it’s obvious haha
rocawear
rocawear13mo ago
how do you open it? using state? so change state
Perfect
Perfect13mo ago
@ruhap yeah I know how to actually open and close it, it’s more of WHEN/WHERE do I close it? I want it to auto close once the action has completed
rocawear
rocawear13mo ago
And you have tried?
const [isPending, startTransition] = useTransition()

if(!isPending) {
hideModal()
}
const [isPending, startTransition] = useTransition()

if(!isPending) {
hideModal()
}
deforestor
deforestor13mo ago
Are you using React Query? Because if you are, this should be extremely trivial
Perfect
Perfect13mo ago
I am not @deforestor Just straight up server actions So when it’s initially not pending (user hasn’t submitted form inside modal yet) won’t it close? The form is inside the modal if that wasn’t clear my bad
rocawear
rocawear13mo ago
why would it if you havent started transition
deforestor
deforestor13mo ago
Right, that explains the confusion. I guess you could check for the status code, if it's 200, you set modal open to false on useEffect But just so you know, adding React Query is no more than about 5 lines and would make everything else easy and also wouldn't break what you already have
rocawear
rocawear13mo ago
console.log(isPending) and think from there
Perfect
Perfect13mo ago
I’m assuming that’s gonna show False (modal closed) True (action happening) False (action complete) I’ll take a look tho, thx I wasn’t sure if it would help with server actions yet but I’ll also consider adding it, thx
deforestor
deforestor13mo ago
Ah, forget about it. I haven't checked the new server stuff yet, thought you were onto something else
Perfect
Perfect13mo ago
Not sure if this is possible , need a callback that does not seem to exist.
deathandtaxes
deathandtaxes13mo ago
Can you add a rough snippet of what your logic so far looks like? I get the idea, but there are many ways you could be instantiating the modal, for instance
Perfect
Perfect13mo ago
@deathandtaxes Yeah sure! So, I control if the modal is open/closed using an isOpen boolean state. According to the NextJS docs, if a server action mutates data and calls redirect, revalidatePath, or revalidateTag , you are supposed to use the useTransition hook to start the action. (not sure why exactly) This hook provides us with an isPending boolean that represents if the action is running and a startTransition function to start it. The form inside of the modal should be disabled and show a spinner indicating when the server action is running. (I am ok with the UI being blocked here during this time). My confusion is how to close the modal once the action is complete. I have it working as expected by just using it like an async function (see screenshot), but apparently its not the correct way. I am using react-hook-form and shadcn for my form validation so there is an onSubmit function I have available.
Perfect
Perfect13mo ago
seemingly working version but not correct way?
Josh
Josh13mo ago
can you give the full component
Perfect
Perfect13mo ago
Yep
Perfect
Perfect13mo ago
That is the form
Josh
Josh13mo ago
aight lemme cook
Perfect
Perfect13mo ago
export default function NewLink({
createLink,
}: {
createLink: ({ title, url }: { title: string; url: string }) => Promise<void>;
}) {
const [isOpen, setIsOpen] = useState(false);
const { user, isLoaded } = useUser();

if (!isLoaded || !user || !user.username) {
return null;
}

return (
<Dialog open={isOpen} onOpenChange={(open) => setIsOpen(open)}>
<DialogTrigger className={buttonVariants({ variant: "outline" })}>
<LinkIcon className="mr-2 h-4 w-4" /> New Link
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create New Link</DialogTitle>
<DialogDescription>
Add a new link to your page. Upon successful creation, it will be
publicly visible to anyone who visits{" "}
<Link
target="_blank"
href={`/${user.username}`}
className={cn(
buttonVariants({ variant: "link" }),
"inline-flex h-auto p-0"
)}
>
lone.link/{user?.username}
</Link>
</DialogDescription>
</DialogHeader>
<NewLinkForm createLink={createLink} setIsOpen={setIsOpen} />
</DialogContent>
</Dialog>
);
}
export default function NewLink({
createLink,
}: {
createLink: ({ title, url }: { title: string; url: string }) => Promise<void>;
}) {
const [isOpen, setIsOpen] = useState(false);
const { user, isLoaded } = useUser();

if (!isLoaded || !user || !user.username) {
return null;
}

return (
<Dialog open={isOpen} onOpenChange={(open) => setIsOpen(open)}>
<DialogTrigger className={buttonVariants({ variant: "outline" })}>
<LinkIcon className="mr-2 h-4 w-4" /> New Link
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create New Link</DialogTitle>
<DialogDescription>
Add a new link to your page. Upon successful creation, it will be
publicly visible to anyone who visits{" "}
<Link
target="_blank"
href={`/${user.username}`}
className={cn(
buttonVariants({ variant: "link" }),
"inline-flex h-auto p-0"
)}
>
lone.link/{user?.username}
</Link>
</DialogDescription>
</DialogHeader>
<NewLinkForm createLink={createLink} setIsOpen={setIsOpen} />
</DialogContent>
</Dialog>
);
}
That is the modal
Josh
Josh13mo ago
and createLink is a server action?
Perfect
Perfect13mo ago
Yep
Josh
Josh13mo ago
this might actually be pretty simple. See if it still works when you do
<form onSubmit={() => startTransition(() => form.handleSubmit(onSubmit))} className="space-y-4">
<form onSubmit={() => startTransition(() => form.handleSubmit(onSubmit))} className="space-y-4">
mmm wait youll still need to pass the form params
Perfect
Perfect13mo ago
Hmm I think I see what ur doing Didn’t think of that
Josh
Josh13mo ago
<form onSubmit={(e) => startTransition(() => form.handleSubmit(onSubmit))(e)} className="space-y-4">
<form onSubmit={(e) => startTransition(() => form.handleSubmit(onSubmit))(e)} className="space-y-4">
i think? try both On why we use startTransition: if you arent updating the client, you dont need to. Updating the client, as you outlined, includes doing any of redirect, revalidatePath, or revalidateTag.. If we ARE updating state, and therefore updating the client, this inherently means we are manipulating the client state in some way, since our UI is changing after the server action is complete. In react, we use startTransition to start non-blocking state updates. So, in theory, if you use a server action & dont wrap it in startTransition, your UI would completely block untill the server action is done. Since this is not what we want, we tell react to let the client continue to update and move around while the server is processing the request by wrapping it in the startTransition.
Perfect
Perfect13mo ago
Ok that’s very clear, unless I missed it this would be extremely helpful in the official docs. I haven’t tried what you sent yet. Will do in a few min
Josh
Josh13mo ago
do be warned, this is just me reverse engineering how next is working internally on the fly so im not 100% sure this is exactly whats happening, but from what i can tell, looks right
Perfect
Perfect13mo ago
From the research I’ve put together that all checks out as well
Josh
Josh13mo ago
im actually gonna drop this in one of the main chats and see what everyone thinks eh actually im just gonna open a whole new question
Perfect
Perfect13mo ago
Perfect
Perfect13mo ago
So I guess the UI is being blocked entirely, but it’s what I expect so idk
Josh
Josh13mo ago
did the new approaches work?
Perfect
Perfect13mo ago
On mobile atm, going to give it a try in a sec
Josh
Josh13mo ago
kk
Perfect
Perfect13mo ago
@Josh alright so neither working rn, getting a This expression is not callable.
Josh
Josh13mo ago
ah for when you pass e thats fine, just remove the e
Perfect
Perfect13mo ago
yeah thats with the e one
Josh
Josh13mo ago
that actually makes sense
Perfect
Perfect13mo ago
without e, I get
Josh
Josh13mo ago
ah wait i see try this
<form onSubmit={() => startTransition(form.handleSubmit(onSubmit))} className="space-y-4">
<form onSubmit={() => startTransition(form.handleSubmit(onSubmit))} className="space-y-4">
err there, try that
Perfect
Perfect13mo ago
Ok no errors what did u change oh since form.handleSubmit returns a function no need to wrap again Let me give it a try Ok we are getting immediate page reload assuming because of preventdefault not happening
Josh
Josh13mo ago
yep yep so
Perfect
Perfect13mo ago
just do that part myself maybe?
Josh
Josh13mo ago
<form onSubmit={(e) => {e.preventDefault();startTransition(form.handleSubmit(onSubmit))}} className="space-y-4">
<form onSubmit={(e) => {e.preventDefault();startTransition(form.handleSubmit(onSubmit))}} className="space-y-4">
Perfect
Perfect13mo ago
alrightt I think thats working! let me do a few more
Perfect
Perfect13mo ago
Perfect
Perfect13mo ago
Looks like its gooood
Josh
Josh13mo ago
huuuuge
Perfect
Perfect13mo ago
so huge, I have been stuck on this for a bit much much appreciated one more thing kinda unrelated
Perfect
Perfect13mo ago
Perfect
Perfect13mo ago
There is a layout shift when the revalidation is happening I believe, any idea on that? using next font
Josh
Josh13mo ago
are you using tailwind
Perfect
Perfect13mo ago
yea
Josh
Josh13mo ago
lemme see your tailwind config and your app.tsx file
Perfect
Perfect13mo ago
import "@/styles/globals.css";
import { ClerkProvider } from "@clerk/nextjs";
import type { Metadata } from "next";
import Container from "@/components/Container";
import Footer from "@/components/Footer";
import Header from "@/components/Header";
import { fontMono, fontSans } from "@/lib/fonts";
import { cn } from "@/lib/utils";

export const metadata: Metadata = {
title: "Lone Link",
description: "Lone Link is a blazingly fast link in bio tool.",
};

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className="dark">
<ClerkProvider>
<body
className={cn(
fontSans.variable,
fontMono.variable,
"flex min-h-screen flex-col font-sans"
)}
>
<Header />
<Container className="my-10">{children}</Container>
<Footer />
</body>
</ClerkProvider>
</html>
);
}
import "@/styles/globals.css";
import { ClerkProvider } from "@clerk/nextjs";
import type { Metadata } from "next";
import Container from "@/components/Container";
import Footer from "@/components/Footer";
import Header from "@/components/Header";
import { fontMono, fontSans } from "@/lib/fonts";
import { cn } from "@/lib/utils";

export const metadata: Metadata = {
title: "Lone Link",
description: "Lone Link is a blazingly fast link in bio tool.",
};

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className="dark">
<ClerkProvider>
<body
className={cn(
fontSans.variable,
fontMono.variable,
"flex min-h-screen flex-col font-sans"
)}
>
<Header />
<Container className="my-10">{children}</Container>
<Footer />
</body>
</ClerkProvider>
</html>
);
}
Perfect
Perfect13mo ago
I assume you mean the root layout? There are both
Josh
Josh13mo ago
and your @/lib/fonts
Perfect
Perfect13mo ago
Ah right
import {
Inter as FontSans,
JetBrains_Mono as FontMono,
} from "next/font/google";

export const fontSans = FontSans({
subsets: ["latin"],
variable: "--font-sans",
});

export const fontMono = FontMono({
subsets: ["latin"],
variable: "--font-mono",
});
import {
Inter as FontSans,
JetBrains_Mono as FontMono,
} from "next/font/google";

export const fontSans = FontSans({
subsets: ["latin"],
variable: "--font-sans",
});

export const fontMono = FontMono({
subsets: ["latin"],
variable: "--font-mono",
});
Josh
Josh13mo ago
wait are you in app dir or pages
Perfect
Perfect13mo ago
app dir
Josh
Josh13mo ago
oh i se i have my clerk provider outside the html
Perfect
Perfect13mo ago
Oh I could probably do that, I think I saw it in the clerk docs like I have it but dont think it matters
Josh
Josh13mo ago
the only thing i could imagine is removing the ...fontFamily.sans, i dont have that in my tailwind config and im also using those fonts and i dont have artifacts when serveractions render it could also be that on revalidation "Lone Link" is straight up gone
Perfect
Perfect13mo ago
Yeah its quite odd I have no idea why thats happening
Josh
Josh13mo ago
id have to inspect your actual repo to get an idea on that, however im guessing that 'Lone Link" is in a client compoent, and that text is expanding the box bigger than what its SSR height is so when it refreshes, it re-executes the react, and then readjusts its size when it loads in ^ i just had this same issue, you can fix it pretty easily with max-height just do max-h-[whatever]
Perfect
Perfect13mo ago
I have an explicit height on it rn actually
Josh
Josh13mo ago
can you see your font flickering during load? if not, then its not tailwind and it would be some CSS somewhere
Perfect
Perfect13mo ago
nope only during that revalidation hmm also header is server comp Oh wait "Lone Link" is a Next link so that is probably a client comp if I had to guess
Josh
Josh13mo ago
then its server iirc (mine dont flicker)
Perfect
Perfect13mo ago
Perfect
Perfect13mo ago
No flickers on reloads, I will have to see what makes the revalidation any different
Josh
Josh13mo ago
ah, i can confirm its your tailwind config if you play back that video super slow, you can see the font changes
Perfect
Perfect13mo ago
The last one I just sent?
Josh
Josh13mo ago
the one i replied
Perfect
Perfect13mo ago
Oh right
Josh
Josh13mo ago
try using
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
display: 'swap',
});
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
display: 'swap',
});
theme: {
extend: {
fontFamily: {
default: ['var(--font-inter)'],
mono: ['var(--font-roboto-mono)'],
},
},
},
theme: {
extend: {
fontFamily: {
default: ['var(--font-inter)'],
mono: ['var(--font-roboto-mono)'],
},
},
},
Perfect
Perfect13mo ago
Perfect
Perfect13mo ago
damn same thing
Josh
Josh13mo ago
I dont see it in this
Perfect
Perfect13mo ago
video didnt catch it
Josh
Josh13mo ago
hahaha
Perfect
Perfect13mo ago
lol I swear it happened its so quick what is display swap?
Josh
Josh13mo ago
not 100%, its in here tho
Josh
Josh13mo ago
Optimizing: Fonts
Optimize your application's web fonts with the built-in next/font loaders.
Perfect
Perfect13mo ago
Perfect
Perfect13mo ago
No idea what this means lol
Josh
Josh13mo ago
same hahahah
Perfect
Perfect13mo ago
They use swap everywhere in docs so I will roll with it haha Maybe I am using revalidate wrong I passed "/dashboard" to it since thats the path of the page containing the data I need to refresh, thats good right?
Josh
Josh13mo ago
i believe so thats what i do
Perfect
Perfect13mo ago
hmm and you dont get this with ur server actions?
Josh
Josh13mo ago
nah mine run fine
Perfect
Perfect13mo ago
hmm weird 🤷‍♂️
Perfect
Perfect13mo ago
I can replicate it by toggling the font-mono variable
Perfect
Perfect13mo ago
Josh
Josh13mo ago
are you in a docker container by chance
Perfect
Perfect13mo ago
Nope
Josh
Josh13mo ago
hm its something with tailwind for sure
Perfect
Perfect13mo ago
Yeah I agree, I’m gonna play around with how I’m setting the font up Maybe the variable is not instantly ready or something