fix(ui): router.refresh after server actions so status updates show without reload

Lab side reported that after accepting a job / advancing a step the
button kept its 'Yükleniyor' state and the page didn't reflect the new
status until they hit refresh. Two issues stacked on top of each other:

1. The button forms were passing the action through an extra
   startTransition wrap — 'action={(fd) => startTransition(() => action(fd))}'.
   With React 19 + useActionState this is unnecessary; useActionState
   already manages its own transition. The double transition can leave
   the dispatch's pending flag wedged in some race orderings, which
   matches what the user saw.

2. revalidatePath() on the server invalidates the RSC cache but does not
   trigger client navigation. So even after the action returned, the
   page kept rendering the stale Job snapshot — and since the buttons
   are conditional on job.status, the now-stale 'pending' status meant
   the button stayed visible.

Fix in JobActionsPanel and the four sibling components (connections
delete row, pending inbound, pending outbound, file row delete):
  - Removed the startTransition wrap; forms point at 'action' directly.
  - Added useRouter() and call router.refresh() in the same useEffect
    branch where the success toast fires. This forces the Server
    Component tree to re-fetch, picks up the new job.status, and the
    actions panel rerenders into whatever button is next in the flow.
  - Cleaned the now-unused useTransition imports.

Net effect: tap 'İşleme Al' → spinner appears, ~400ms later the toast
hits and the row updates in place to 'Sonraki Aşama' without any
manual refresh.
This commit is contained in:
kovakmedya
2026-05-22 01:15:32 +03:00
parent 6fec52b98d
commit cdb2a15643
5 changed files with 69 additions and 78 deletions
@@ -1,6 +1,7 @@
"use client";
import { useActionState, useEffect, useState, useTransition } from "react";
import { useActionState, useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import {
ArrowRight,
Check,
@@ -66,20 +67,20 @@ export function JobActionsPanel({
}
function AcceptButton({ jobId }: { jobId: string }) {
const router = useRouter();
const [state, action, pending] = useActionState(acceptJobAction, initialJobActionState);
const [, startTransition] = useTransition();
useEffect(() => {
if (state.ok) toast.success("İş işleme alındı.");
else if (state.error) toast.error(state.error);
}, [state]);
if (state.ok) {
toast.success("İş işleme alındı.");
router.refresh();
} else if (state.error) {
toast.error(state.error);
}
}, [state, router]);
return (
<form
action={(fd) => {
startTransition(() => action(fd));
}}
>
<form action={action}>
<input type="hidden" name="jobId" value={jobId} />
<Button type="submit" disabled={pending}>
{pending ? <Loader2 className="size-4 animate-spin" /> : <Play className="size-4" />}
@@ -90,18 +91,19 @@ function AcceptButton({ jobId }: { jobId: string }) {
}
function AdvanceButton({ job }: { job: Job }) {
const router = useRouter();
const [state, action, pending] = useActionState(advanceStepAction, initialJobActionState);
const [, startTransition] = useTransition();
const [open, setOpen] = useState(false);
useEffect(() => {
if (state.ok) {
toast.success("Aşama ilerletildi.");
setOpen(false);
router.refresh();
} else if (state.error) {
toast.error(state.error);
}
}, [state]);
}, [state, router]);
const currentIdx = job.currentStep ? JOB_STEP_ORDER.indexOf(job.currentStep) : -1;
const isFinal = currentIdx === JOB_STEP_ORDER.length - 1;
@@ -127,12 +129,7 @@ function AdvanceButton({ job }: { job: Job }) {
: `Sonraki aşama: ${nextLabel}. İsterseniz bir not ekleyebilirsiniz.`}
</DialogDescription>
</DialogHeader>
<form
action={(fd) => {
startTransition(() => action(fd));
}}
className="grid gap-3"
>
<form action={action} className="grid gap-3">
<input type="hidden" name="jobId" value={job.$id} />
<div className="grid gap-2">
<Label htmlFor="note">Not (opsiyonel)</Label>
@@ -162,20 +159,20 @@ function AdvanceButton({ job }: { job: Job }) {
}
function DeliverButton({ jobId }: { jobId: string }) {
const router = useRouter();
const [state, action, pending] = useActionState(markDeliveredAction, initialJobActionState);
const [, startTransition] = useTransition();
useEffect(() => {
if (state.ok) toast.success("İş teslim alındı.");
else if (state.error) toast.error(state.error);
}, [state]);
if (state.ok) {
toast.success("İş teslim alındı.");
router.refresh();
} else if (state.error) {
toast.error(state.error);
}
}, [state, router]);
return (
<form
action={(fd) => {
startTransition(() => action(fd));
}}
>
<form action={action}>
<input type="hidden" name="jobId" value={jobId} />
<Button type="submit" disabled={pending}>
{pending ? <Loader2 className="size-4 animate-spin" /> : <PackageCheck className="size-4" />}
@@ -186,18 +183,19 @@ function DeliverButton({ jobId }: { jobId: string }) {
}
function CancelButton({ jobId }: { jobId: string }) {
const router = useRouter();
const [state, action, pending] = useActionState(cancelJobAction, initialJobActionState);
const [, startTransition] = useTransition();
const [open, setOpen] = useState(false);
useEffect(() => {
if (state.ok) {
toast.success("İş iptal edildi.");
setOpen(false);
router.refresh();
} else if (state.error) {
toast.error(state.error);
}
}, [state]);
}, [state, router]);
return (
<Dialog open={open} onOpenChange={setOpen}>
@@ -215,11 +213,7 @@ function CancelButton({ jobId }: { jobId: string }) {
</span>
</DialogDescription>
</DialogHeader>
<form
action={(fd) => {
startTransition(() => action(fd));
}}
>
<form action={action}>
<input type="hidden" name="jobId" value={jobId} />
<DialogFooter>
<DialogClose asChild>