Next js Form weird behavior
I am building a form where when i click on submit, the data does get updated but the ui starts to update every second, like an infinite loop
I am using server actions with the new useFormState and useFormStatus
3 Replies
Page
Form
Submit Button
Server Action
I have minified the code
i cant find a reason why this is happenning
when i remove the revalidatePath inside the server actions the bug no longer occurs
but then the user would have to refresh every time as well
i added useOptimistic
that solved the issue but the entire form should work fine even without it
still no idea what the problem is
// Global Imports
// Local Imports
// Body
export default async function Subjects({ params }: { params: { subjectId: string } }) {
// Get all subjects
const currentSubject = await api.subjects.currentSubject.query({
id: params.subjectId
})
if (!currentSubject) {
return null
}
// JSX
return (
<>
<div className={`${pxGlobal} max-w-screen flex py-8 justify-between flex-col gap-y-12`}>
{/* Other Code */}
{/* Form */}
<div className="flex flex-col gap-y-4 w-full">
{/* Other Code */}
{currentSubject && <CurrentSubjectForm currentSubject={currentSubject} subjectId={params.subjectId} />}
</div>
</div>
</>
)
}
// Global Imports
// Local Imports
// Body
export default async function Subjects({ params }: { params: { subjectId: string } }) {
// Get all subjects
const currentSubject = await api.subjects.currentSubject.query({
id: params.subjectId
})
if (!currentSubject) {
return null
}
// JSX
return (
<>
<div className={`${pxGlobal} max-w-screen flex py-8 justify-between flex-col gap-y-12`}>
{/* Other Code */}
{/* Form */}
<div className="flex flex-col gap-y-4 w-full">
{/* Other Code */}
{currentSubject && <CurrentSubjectForm currentSubject={currentSubject} subjectId={params.subjectId} />}
</div>
</div>
</>
)
}
------------------------------------------
------------------------------------------
"use client"
// Global Imports
// Local Imports
// NBody
const initialState = {
status: "" as string,
message: null as string | null,
}
export default function CurrentSubjectForm({ currentSubject, subjectId }: { currentSubject: Subject, subjectId: string }) {
const [state, formAction] = useFormState(MutateCurrentSubject, initialState);
// Other Code
const currentSubjectArray = Object.entries(currentSubject).map(([key, value]) => ({ "key": key, "value": value }))
// JSX
return (
<form className="flex flex-col gap-y-6 w-full box-border" action={formAction}>
{/* Other Code */}
{currentSubjectArray.map((propertyObj) => {
return (
<div key={`${propertyObj.key}`} className="flex flex-col gap-y-2">
<p>{propertyObj.key}</p>
{propertyObj.key === "id" && <CustomInputId name={propertyObj.key} defaultValue={rmFirstAndLastDoubleQuotes(JSON.stringify(propertyObj.value))} />}
{propertyObj.key === "subject" && <CustomInputSubject name={propertyObj.key} defaultValue={rmFirstAndLastDoubleQuotes(JSON.stringify(propertyObj.value))} />}
{/* Other input fields also generated like this */}
</div>
)
})}
{/* Custom Hidden Input */}
<input type="hidden" name="id" defaultValue={subjectId} />
<div className="flex gap-x-2">
<SaveCurrentSubjectButton />
<Button type="reset" variant={"outline"} className="w-32">Reset</Button>
</div>
</form>
)
}
"use client"
// Global Imports
// Local Imports
// NBody
const initialState = {
status: "" as string,
message: null as string | null,
}
export default function CurrentSubjectForm({ currentSubject, subjectId }: { currentSubject: Subject, subjectId: string }) {
const [state, formAction] = useFormState(MutateCurrentSubject, initialState);
// Other Code
const currentSubjectArray = Object.entries(currentSubject).map(([key, value]) => ({ "key": key, "value": value }))
// JSX
return (
<form className="flex flex-col gap-y-6 w-full box-border" action={formAction}>
{/* Other Code */}
{currentSubjectArray.map((propertyObj) => {
return (
<div key={`${propertyObj.key}`} className="flex flex-col gap-y-2">
<p>{propertyObj.key}</p>
{propertyObj.key === "id" && <CustomInputId name={propertyObj.key} defaultValue={rmFirstAndLastDoubleQuotes(JSON.stringify(propertyObj.value))} />}
{propertyObj.key === "subject" && <CustomInputSubject name={propertyObj.key} defaultValue={rmFirstAndLastDoubleQuotes(JSON.stringify(propertyObj.value))} />}
{/* Other input fields also generated like this */}
</div>
)
})}
{/* Custom Hidden Input */}
<input type="hidden" name="id" defaultValue={subjectId} />
<div className="flex gap-x-2">
<SaveCurrentSubjectButton />
<Button type="reset" variant={"outline"} className="w-32">Reset</Button>
</div>
</form>
)
}
------------------------------------------
------------------------------------------
'use client'
// Global Imports
// Local Imports
// Body
export function SaveCurrentSubjectButton() {
const { pending } = useFormStatus()
if (pending) {
return (
<Button type="submit" variant={"primaryDisabled"} className="w-32">Submit</Button>
)
}
// JSX
return (
<Button type="submit" variant={"primary"} className="w-32">Submit</Button>
)
}
'use client'
// Global Imports
// Local Imports
// Body
export function SaveCurrentSubjectButton() {
const { pending } = useFormStatus()
if (pending) {
return (
<Button type="submit" variant={"primaryDisabled"} className="w-32">Submit</Button>
)
}
// JSX
return (
<Button type="submit" variant={"primary"} className="w-32">Submit</Button>
)
}
------------------------------------------
------------------------------------------
"use server"
// Global Imports
// Local Imports
// Body
const subjectsSchema = z.object({
subject: z.string().trim().max(255, "Subject must be less than 255 characters long"),
overview: z.string().trim().max(10000000, "Overview must be less than 10,000,000 characters long").optional(),
educationLevel: z.enum(["OLevel", "ALevel"]),
visibility: z.enum(["Pending", "Public", "Private"]),
id: z.string().cuid(),
})
export async function MutateCurrentSubject(prevState: unknown, formData: FormData) {
try {
const validatedFields = subjectsSchema.safeParse({
subject: formData.get('subject'),
educationLevel: formData.get('educationLevel'),
overview: formData.get('overview'),
visibility: formData.get('visibility'),
id: formData.get('id'),
})
// // Return early if the form data is invalid
if (!validatedFields.success) {
return {
status: "error",
message: "The form data is invalid. Please try again later."
}
}
const { status, message } = await api.subjects.updateCurrentSubject.mutate(validatedFields.data)
revalidatePath(`/admin/subjects`)
return {
status: status ? status : "success",
message: message ? message : "Subject created successfully."
}
} catch (error) {
return {
status: "fail",
message: "Something went wrong. Please try again later."
}
}
}
"use server"
// Global Imports
// Local Imports
// Body
const subjectsSchema = z.object({
subject: z.string().trim().max(255, "Subject must be less than 255 characters long"),
overview: z.string().trim().max(10000000, "Overview must be less than 10,000,000 characters long").optional(),
educationLevel: z.enum(["OLevel", "ALevel"]),
visibility: z.enum(["Pending", "Public", "Private"]),
id: z.string().cuid(),
})
export async function MutateCurrentSubject(prevState: unknown, formData: FormData) {
try {
const validatedFields = subjectsSchema.safeParse({
subject: formData.get('subject'),
educationLevel: formData.get('educationLevel'),
overview: formData.get('overview'),
visibility: formData.get('visibility'),
id: formData.get('id'),
})
// // Return early if the form data is invalid
if (!validatedFields.success) {
return {
status: "error",
message: "The form data is invalid. Please try again later."
}
}
const { status, message } = await api.subjects.updateCurrentSubject.mutate(validatedFields.data)
revalidatePath(`/admin/subjects`)
return {
status: status ? status : "success",
message: message ? message : "Subject created successfully."
}
} catch (error) {
return {
status: "fail",
message: "Something went wrong. Please try again later."
}
}
}
I'm guessing it's cause you're calling a tRPC route inside your backend
if the API object wasn't created via
createCaller
your doing another post request to yourself
which could cause some interesting side effectsohh
ill look into that
thanks