New
Introducing React Bench, see how different models perform on React code

react-doctor/loading-action-preserves-trigger

Loading state removes its initiating action

  • Category: Accessibility
  • Severity: warn
  • Source: oxlint-plugin-react-doctor
  • Framework: global
  • Enabled when: react
  • Tags: react-jsx-only
  • Default: Disabled until configured

Validate the diagnostic

Confirm the reported code matches this rule before you edit it.

Confirm the reported code matches react-doctor/loading-action-preserves-trigger. Compare it with the Before example and verify the same API, framework, and execution context. Treat it as a false positive only when one of those conditions differs.

Compare the code

The corrected pattern shows a focused way to address the diagnostic.

Reported pattern

import { useState } from "react";
const SaveButton = () => {
  const [isSaving, setIsSaving] = useState(false);
  const save = async () => {
    setIsSaving(true);
    await fetch("/api/save", { method: "POST" });
    setIsSaving(false);
  };
  return isSaving
    ? <span role="status">Saving</span>
    : <button onClick={save}>Save</button>;
};

Corrected pattern

import { useState } from "react";
const SaveButton = () => {
  const [isSaving, setIsSaving] = useState(false);
  const save = async () => {
    setIsSaving(true);
    await fetch("/api/save", { method: "POST" });
    setIsSaving(false);
  };
  return (
    <button disabled={isSaving} onClick={save}>
      <span className={isSaving ? "invisible" : ""}>Save</span>
      {isSaving && <span role="status">Saving…</span>}
    </button>
  );
};

How to fix

Use the corrected pattern as a reference. Preserve behavior that the rule does not require you to change.

Copyable fix prompt

Copy this self-contained prompt into your coding agent after you confirm the diagnostic.

Fix every confirmed react-doctor/loading-action-preserves-trigger diagnostic in the current repository. Required change: - Keep the initiating control mounted while work is pending. Disable it or mark it busy, and put the spinner or status inside the same control so focus and action identity remain stable.
Reference transformation:Before:
import { useState } from "react";
const SaveButton = () => {
  const [isSaving, setIsSaving] = useState(false);
  const save = async () => {
    setIsSaving(true);
    await fetch("/api/save", { method: "POST" });
    setIsSaving(false);
  };
  return isSaving
    ? <span role="status">Saving</span>
    : <button onClick={save}>Save</button>;
};
After:
import { useState } from "react";
const SaveButton = () => {
  const [isSaving, setIsSaving] = useState(false);
  const save = async () => {
    setIsSaving(true);
    await fetch("/api/save", { method: "POST" });
    setIsSaving(false);
  };
  return (
    <button disabled={isSaving} onClick={save}>
      <span className={isSaving ? "invisible" : ""}>Save</span>
      {isSaving && <span role="status">Saving…</span>}
    </button>
  );
};
Constraints: - Confirm the reported code matches the Before pattern. - Make the smallest change that fixes the root cause. - Preserve behavior and interfaces unrelated to this diagnostic. - Reuse existing project components, utilities, and conventions. - Do not introduce render-phase side effects, render-phase state updates, or Hooks rule violations. - Preserve accessible names, focus order, keyboard behavior, and touch access not targeted by this rule. - Adapt identifiers and framework details instead of copying blindly. - Do not disable the rule or suppress matching code. - Confirm this rule is enabled for the project: react. Verification: - Run focused tests for the changed behavior. - Run React Doctor and confirm this diagnostic no longer appears. - Report the files changed and any checks you could not run.

More Accessibility rules from the rules reference: