Next.js表單狀態一直顯示送出中:用pending提示與重試

訪客按送出,按鈕變成「送出中」,等了很久卻無法再按。先看請求是不是還在等待,還是已經回錯誤而畫面沒解除狀態。兩種原因的修法不同;把按鈕重新打開,只解決了表面操作,還要讓訪客知道這次送出到底到了哪一步。

以下用 Next.js 16、React 19 的 App Router 表單作例子。Client Component 以 useActionState 處理表單 action,再向自己的 Next Route Handler 發 POST;這不是 Server Action,也沒有連接正式資料庫、寄信或付款。

把等待、錯誤與完成分開顯示

等待時顯示「送出中,請稍候」,暫時停用這個送出按鈕。取得可用錯誤回應後,等待應結束,保留輸入,說明原因並提供重試;成功時則按接收端真正回傳的結果顯示,不要只因請求有回應就寫「已寄送」。

按鈕原本就因缺資料而停用,屬於送出前的條件。可先看停用按鈕旁怎麼說明原因。這篇處理的是已送出後的等待生命週期,不把欄位格式合格當成伺服器已接收。

讓第一次失敗,再用同一段文字重試

示例 Route Handler 先檢查訊息,再等待約 1.8 秒;第一個有效嘗試回 503,第二個回 200。故障次序是為了重現操作安排的,嘗試次數只控制示例,不是正式提交身分或防重複機制。

Next.js表單發出POST後顯示送出中,送出按鈕停用,輸入文字保留
真實 Next.js 示例正在等待自己的 POST 回應;文字仍在輸入框。

第一次取得 503 後,畫面解除等待,按鈕改為「重試」,訊息仍是剛才的文字。按重試會再發一個 POST;第二次取得 200 後,示例顯示「請求已成功回應;未保存或寄送資料」。它只確認這個示例端點完成回應。

Next.js示例POST回503後顯示服務暫時失敗,輸入保留且重試按鈕可操作
服務回錯誤後,等待結束,保留輸入並提供重試。

讓 pending 跟著真實非同步工作

在 Client Component 檔案開頭加入 'use client',從 React 匯入 useActionState 與 useState。useActionState 的 action 接收上一份 state 與這次 FormData,回傳下一份結果;不要把第一個參數誤當表單資料。

這個 action 讀取示例端點約定的 JSON message,並用 HTTP 狀態決定成功或錯誤。片段中的 /api/submit 必須有自己實際建立的 Route Handler,接收 POST JSON 並回相應狀態及 message;貼上前端函式不會自動建立接收端。

async function send(previous, formData) {
  const attempt = previous.attempt + 1;
  try {
    const response = await fetch('/api/submit', {
      method: 'POST',
      signal: AbortSignal.timeout(4000),
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        attempt,
        message: formData.get('message'),
      }),
    });
    const data = await response.json();
    return {
      attempt,
      status: response.ok ? 'success' : 'error',
      message: data.message,
    };
  } catch (error) {
    return {
      attempt,
      status: 'error',
      message: error.name === 'TimeoutError'
        ? '等待回應逾時,請確認狀態後再試。'
        : '未取得可用回應,請確認連線後再試。',
    };
  }
}

fetch 取得 503 或 422 時不一定拋出例外,所以仍要檢查 response.ok。網路失敗或無法讀取 JSON,才會走這個 catch 的通用回饋。正式端點若採另一種 JSON 結構,先核對契約,不要直接假定任何服務都有 message。

同一元件內接出輸入、狀態與按鈕:

const [text, setText] = useState('');
const [state, formAction, isPending] = useActionState(send, {
  attempt: 0, status: 'idle', message: '填入示例訊息。',
});

return (
  <form action={formAction}>
    <label>示例訊息
      <input name="message" required value={text}
        onChange={e => setText(e.target.value)} />
    </label>
    <p role="status">
      {isPending ? '送出中,請稍候…' : state.message}
    </p>
    <button type="submit" disabled={isPending}>
      {isPending ? '送出中…'
        : state.status === 'error' ? '重試' : '送出示例'}
    </button>
  </form>
);

把 formAction 放在 form 的 action,React 會替這次表單操作安排 Transition,isPending 便能對應進行中的 action。若自行在普通點擊回呼直接呼叫 action,卻沒按文件放進 Transition,可能得到不同的 pending 行為;不要只複製 hook 名稱就以為已接好。

請求沒有結束,pending 也不會自己倒數

這份示例另外讓第一次請求等待 6.5 秒,客戶端使用 4,000 毫秒的 AbortSignal.timeout。等待超過示例限制後,fetch 中止,action 回傳逾時提示,按鈕解除停用,輸入仍保留。再按重試,第二次按正常延遲回 200。

四秒只是這個短請求示例的設定,不是所有網站的通用門檻。上傳、轉檔或其他長工作需要自己的等待與進度方案。AbortSignal.timeout 按活躍時間計算,頁面被暫停時不一定等於同樣的牆鐘時間;較舊瀏覽器也需核對支援,不能宣稱每個裝置都會準時四秒結束。

若原本自己管理 loading 布林值,也要讓成功、HTTP 錯誤、解析失敗與中止都能走到結束分支。不要只有成功後才把 loading 設回 false,讓所有錯誤都永遠停在送出中。

另一個示例讓端點回 200,但內容刻意不是約定的 JSON。讀取失敗後,畫面仍會結束等待,顯示未取得可用回應並保留輸入;修正回應或重新嘗試後再核真實結果。只判斷 response.ok,沒有處理解析失敗,仍可能讓流程卡住或誤報成功。

若正常請求已完成,而按鈕仍顯示送出中,核對畫面綁的是 isPending,還是另外一份從未解除的 loading。不要同時維護兩個不同步的等待來源。相反,Network 仍在等待而畫面先說完成,就查 action 是否漏了 await,或只啟動非同步工作便提前 return。

保留輸入,但別讓訪客誤解送出內容

本例用受控輸入保存文字;action 結束後,不另外清空它,方便確認或重試。正式表單可以在真成功後依需求清空,但錯誤時不應先刪掉訪客剛寫的內容,再要求重新輸入。

等待期間若仍允許改字,已發出的請求保存的是按送出當下的 FormData。畫面後來改過的文字,不代表伺服器收到的也是新文字。由產品決定等待時能否編輯,並讓完成提示對應那次提交;不要把正在編輯的草稿當成已送出的資料。

逾時不等於伺服器沒有處理

瀏覽器停止等待後,請求可能早已到達伺服器。前端中止不能撤銷資料寫入,也不能證明郵件沒有寄出。這個示例端點不保存資料,因此可以安全重做回應;正式訂單、付款與其他有副作用的操作,要先核對提交狀態與重試規則。

停用按鈕只減少這個畫面上的誤按,不能擋住其他分頁、程式請求或傳輸重試。需要避免重複保存時,由接收端處理提交識別與唯一條件;不要把前端 disabled 稱為完整的防重複方案。

至少走完錯誤與恢復兩條路

先讓端點回一次已知錯誤,確認 pending 解除、文字保留、重試再發請求;再讓回應逾時,確認有明確提示。最後測一次正常成功,核對訊息是否描述真實結果,而不是沿用上一輪錯誤或籠統說「完成」。

保留每次請求、回應與畫面轉換給維護者核對,含個資的內容先遮蔽。完成這個等待流程後,還要另驗接收端的資料、權限與實際交付,表單才算整合完成。

參考資料

React:useActionState、form Action 與錯誤結果
Next.js:Route Handler 的 POST 與回應
MDN:fetch 與 HTTP 錯誤處理
MDN:AbortSignal.timeout 的時間與相容性

示例測試版本:Next.js 16.3.8/React 19.3.0。

原文鏈接:https://wntheme.com/next-form-pending-retry/,轉載請註明出處。
0

評論0

顯示驗證碼
沒有帳號?註冊  忘記密碼?