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
rocawear2y ago
https://react.dev/reference/react/useTransition
const [isPending, startTransition] = useTransition()
const [isPending, startTransition] = useTransition()
can you take it from the isPending?
Perfect
PerfectOP2y 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
rocawear2y ago
how do you open it? using state? so change state
Perfect
PerfectOP2y 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
rocawear2y ago
And you have tried?
const [isPending, startTransition] = useTransition()

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

if(!isPending) {
hideModal()
}
deforestor
deforestor2y ago
Are you using React Query? Because if you are, this should be extremely trivial
Perfect
PerfectOP2y 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
rocawear2y ago
why would it if you havent started transition
deforestor
deforestor2y 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
rocawear2y ago
console.log(isPending) and think from there
Perfect
PerfectOP2y 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
deforestor2y ago
Ah, forget about it. I haven't checked the new server stuff yet, thought you were onto something else
Perfect
PerfectOP2y ago
Not sure if this is possible , need a callback that does not seem to exist.
deathandtaxes
deathandtaxes2y 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
PerfectOP2y 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
PerfectOP2y ago
seemingly working version but not correct way?
Josh
Josh2y ago
can you give the full component
Perfect
PerfectOP2y ago
Yep
Perfect
PerfectOP2y ago
That is the form
Josh
Josh2y ago
aight lemme cook
Perfect
PerfectOP2y 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
Josh2y ago
and createLink is a server action?
Perfect
PerfectOP2y ago
Yep
Josh
Josh2y 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
PerfectOP2y ago
Hmm I think I see what ur doing Didn’t think of that
Josh
Josh2y 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
PerfectOP2y 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
Josh2y 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
PerfectOP2y ago
From the research I’ve put together that all checks out as well
Josh
Josh2y 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
PerfectOP2y ago
Perfect
PerfectOP2y ago
So I guess the UI is being blocked entirely, but it’s what I expect so idk
Josh
Josh2y ago
did the new approaches work?
Perfect
PerfectOP2y ago
On mobile atm, going to give it a try in a sec
Josh
Josh2y ago
kk
Perfect
PerfectOP2y ago
@Josh alright so neither working rn, getting a This expression is not callable.
Josh
Josh2y ago
ah for when you pass e thats fine, just remove the e
Perfect
PerfectOP2y ago
yeah thats with the e one
Josh
Josh2y ago
that actually makes sense
Perfect
PerfectOP2y ago
without e, I get
Josh
Josh2y 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
PerfectOP2y 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
Josh2y ago
yep yep so
Perfect
PerfectOP2y ago
just do that part myself maybe?
Josh
Josh2y 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
PerfectOP2y ago
alrightt I think thats working! let me do a few more
Perfect
PerfectOP2y ago
Perfect
PerfectOP2y ago
Looks like its gooood
Josh
Josh2y ago
huuuuge
Perfect
PerfectOP2y ago
so huge, I have been stuck on this for a bit much much appreciated one more thing kinda unrelated
Perfect
PerfectOP2y ago
Perfect
PerfectOP2y ago
There is a layout shift when the revalidation is happening I believe, any idea on that? using next font
Josh
Josh2y ago
are you using tailwind
Perfect
PerfectOP2y ago
yea
Josh
Josh2y ago
lemme see your tailwind config and your app.tsx file
Perfect
PerfectOP2y 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
PerfectOP2y ago
I assume you mean the root layout? There are both
Josh
Josh2y ago
and your @/lib/fonts
Perfect
PerfectOP2y 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
Josh2y ago
wait are you in app dir or pages
Perfect
PerfectOP2y ago
app dir
Josh
Josh2y ago
oh i se i have my clerk provider outside the html
Perfect
PerfectOP2y 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
Josh2y 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
PerfectOP2y ago
Yeah its quite odd I have no idea why thats happening
Josh
Josh2y 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
PerfectOP2y ago
I have an explicit height on it rn actually
Josh
Josh2y ago
can you see your font flickering during load? if not, then its not tailwind and it would be some CSS somewhere
Perfect
PerfectOP2y 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
Josh2y ago
then its server iirc (mine dont flicker)
Perfect
PerfectOP2y ago
Perfect
PerfectOP2y ago
No flickers on reloads, I will have to see what makes the revalidation any different
Josh
Josh2y ago
ah, i can confirm its your tailwind config if you play back that video super slow, you can see the font changes
Perfect
PerfectOP2y ago
The last one I just sent?
Josh
Josh2y ago
the one i replied
Perfect
PerfectOP2y ago
Oh right
Josh
Josh2y 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
PerfectOP2y ago
Perfect
PerfectOP2y ago
damn same thing
Josh
Josh2y ago
I dont see it in this
Perfect
PerfectOP2y ago
video didnt catch it
Josh
Josh2y ago
hahaha
Perfect
PerfectOP2y ago
lol I swear it happened its so quick what is display swap?
Josh
Josh2y ago
not 100%, its in here tho
Josh
Josh2y ago
Optimizing: Fonts
Optimize your application's web fonts with the built-in next/font loaders.
Perfect
PerfectOP2y ago
Perfect
PerfectOP2y ago
No idea what this means lol
Josh
Josh2y ago
same hahahah
Perfect
PerfectOP2y 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
Josh2y ago
i believe so thats what i do
Perfect
PerfectOP2y ago
hmm and you dont get this with ur server actions?
Josh
Josh2y ago
nah mine run fine
Perfect
PerfectOP2y ago
hmm weird 🤷‍♂️
Perfect
PerfectOP2y ago
I can replicate it by toggling the font-mono variable
Perfect
PerfectOP2y ago
Josh
Josh2y ago
are you in a docker container by chance
Perfect
PerfectOP2y ago
Nope
Josh
Josh2y ago
hm its something with tailwind for sure
Perfect
PerfectOP2y 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
Want results from more Discord servers?
Add your server