Skip to content

Commit 6ee8106

Browse files
committed
feat(react-dom): add 'no-use-form-state' rule to replace useFormState with useActionState
1 parent 25f1bf4 commit 6ee8106

File tree

10 files changed

+437
-55
lines changed

10 files changed

+437
-55
lines changed

apps/website/content/docs/rules/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
"dom-no-unknown-property",
6969
"dom-no-unsafe-iframe-sandbox",
7070
"dom-no-unsafe-target-blank",
71+
"dom-no-use-form-state",
7172
"dom-no-void-elements-with-children",
7273
"---Web API Rules---",
7374
"web-api-no-leaked-event-listener",

apps/website/content/docs/rules/overview.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ full: true
8888
| [`no-unknown-property`](./dom-no-unknown-property) | 1️⃣ | `🔍` `🔧` `⚙️` | Prevents using unknown `DOM` property | |
8989
| [`no-unsafe-iframe-sandbox`](./dom-no-unsafe-iframe-sandbox) | 1️⃣ | `🔍` | Enforces `sandbox` attribute for `iframe` elements is not set to unsafe combinations. | |
9090
| [`no-unsafe-target-blank`](./dom-no-unsafe-target-blank) | 1️⃣ | `🔍` | Prevents using `target="_blank"` without `rel="noreferrer noopener"`. | |
91+
| [`no-use-form-state`](./dom-no-use-form-state) | 2️⃣ | `🔍` `🔄` | Replaces the usages of `useFormState` to use `useActionState`. | >=19.0.0 |
9192
| [`no-void-elements-with-children`](./dom-no-void-elements-with-children) | 2️⃣ | `🔍` | Prevents using `children` in void DOM elements. | |
9293

9394
## Web API Rules

packages/plugins/eslint-plugin-react-dom/src/configs/recommended.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const rules = {
1717
"react-dom/no-unknown-property": "warn",
1818
"react-dom/no-unsafe-iframe-sandbox": "warn",
1919
"react-dom/no-unsafe-target-blank": "warn",
20+
"react-dom/no-use-form-state": "error",
2021
"react-dom/no-void-elements-with-children": "error",
2122
} as const satisfies RulePreset;
2223

packages/plugins/eslint-plugin-react-dom/src/plugin.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import noScriptUrl from "./rules/no-script-url";
1212
import noUnknownProperty from "./rules/no-unknown-property";
1313
import noUnsafeIframeSandbox from "./rules/no-unsafe-iframe-sandbox";
1414
import noUnsafeTargetBlank from "./rules/no-unsafe-target-blank";
15+
import noUseFormState from "./rules/no-use-form-state";
1516
import noVoidElementsWithChildren from "./rules/no-void-elements-with-children";
1617

1718
export const plugin = {
@@ -33,6 +34,7 @@ export const plugin = {
3334
"no-unknown-property": noUnknownProperty,
3435
"no-unsafe-iframe-sandbox": noUnsafeIframeSandbox,
3536
"no-unsafe-target-blank": noUnsafeTargetBlank,
37+
"no-use-form-state": noUseFormState,
3638
"no-void-elements-with-children": noVoidElementsWithChildren,
3739

3840
// Part: deprecated rules
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
---
2+
title: no-use-form-state
3+
---
4+
5+
**Full Name in `eslint-plugin-react-dom`**
6+
7+
```plain copy
8+
react-dom/no-use-form-state
9+
```
10+
11+
**Full Name in `@eslint-react/eslint-plugin`**
12+
13+
```plain copy
14+
@eslint-react/dom/no-use-form-state
15+
```
16+
17+
**Features**
18+
19+
`🔍` `🔄`
20+
21+
**Presets**
22+
23+
- `dom`
24+
- `recommended`
25+
- `recommended-typescript`
26+
- `recommended-type-checked`
27+
28+
## What it does
29+
30+
Replaces the usages of `useFormState` to use `useActionState`.
31+
32+
An **safe** codemod is available for this rule.
33+
34+
## Examples
35+
36+
### Failing
37+
38+
```tsx
39+
import { useFormState } from "react-dom";
40+
41+
async function increment(previousState, formData) {
42+
return previousState + 1;
43+
}
44+
45+
function StatefulForm({}) {
46+
const [state, formAction] = useFormState(increment, 0);
47+
return (
48+
<form>
49+
{state}
50+
<button formAction={formAction}>Increment</button>
51+
</form>
52+
);
53+
}
54+
```
55+
56+
### Passing
57+
58+
```tsx
59+
import { useActionState } from "react";
60+
61+
async function increment(previousState, formData) {
62+
return previousState + 1;
63+
}
64+
65+
function StatefulForm({}) {
66+
const [state, formAction] = useActionState(increment, 0);
67+
return (
68+
<form>
69+
{state}
70+
<button formAction={formAction}>Increment</button>
71+
</form>
72+
);
73+
}
74+
```
75+
76+
## Implementation
77+
78+
- [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom/src/rules/no-use-form-state.ts)
79+
- [Test source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom/src/rules/no-use-form-state.spec.ts)
80+
81+
## Further Reading
82+
83+
- [React: useActionState](https://react.dev/reference/react/useActionState)
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import tsx from "dedent";
2+
3+
import { ruleTester } from "../../../../../test";
4+
import rule, { RULE_NAME } from "./no-use-form-state";
5+
6+
ruleTester.run(RULE_NAME, rule, {
7+
invalid: [
8+
{
9+
code: tsx`
10+
import { useFormState } from "react-dom";
11+
12+
async function increment(previousState, formData) {
13+
return previousState + 1;
14+
}
15+
16+
function StatefulForm({}) {
17+
const [state, formAction] = useFormState(increment, 0);
18+
return (
19+
<form>
20+
{state}
21+
<button formAction={formAction}>Increment</button>
22+
</form>
23+
);
24+
}
25+
`,
26+
errors: [
27+
{
28+
messageId: "noUseFormState",
29+
},
30+
],
31+
output: tsx`
32+
import { useActionState } from "react";
33+
import { useFormState } from "react-dom";
34+
35+
async function increment(previousState, formData) {
36+
return previousState + 1;
37+
}
38+
39+
function StatefulForm({}) {
40+
const [state, formAction] = useActionState(increment, 0);
41+
return (
42+
<form>
43+
{state}
44+
<button formAction={formAction}>Increment</button>
45+
</form>
46+
);
47+
}
48+
`,
49+
settings: {
50+
"react-x": {
51+
version: "19.0.0",
52+
},
53+
},
54+
},
55+
{
56+
code: tsx`
57+
import ReactDOM from "react-dom";
58+
59+
async function increment(previousState, formData) {
60+
return previousState + 1;
61+
}
62+
63+
function StatefulForm({}) {
64+
const [state, formAction] = ReactDOM.useFormState(increment, 0);
65+
return (
66+
<form>
67+
{state}
68+
<button formAction={formAction}>Increment</button>
69+
</form>
70+
);
71+
}
72+
`,
73+
errors: [
74+
{
75+
messageId: "noUseFormState",
76+
},
77+
],
78+
output: tsx`
79+
import { useActionState } from "react";
80+
import ReactDOM from "react-dom";
81+
82+
async function increment(previousState, formData) {
83+
return previousState + 1;
84+
}
85+
86+
function StatefulForm({}) {
87+
const [state, formAction] = useActionState(increment, 0);
88+
return (
89+
<form>
90+
{state}
91+
<button formAction={formAction}>Increment</button>
92+
</form>
93+
);
94+
}
95+
`,
96+
settings: {
97+
"react-x": {
98+
version: "19.0.0",
99+
},
100+
},
101+
},
102+
{
103+
code: tsx`
104+
import * as ReactDOM from "react-dom";
105+
106+
async function increment(previousState, formData) {
107+
return previousState + 1;
108+
}
109+
110+
function StatefulForm({}) {
111+
const [state, formAction] = ReactDOM.useFormState(increment, 0);
112+
return (
113+
<form>
114+
{state}
115+
<button formAction={formAction}>Increment</button>
116+
</form>
117+
);
118+
}
119+
`,
120+
errors: [
121+
{
122+
messageId: "noUseFormState",
123+
},
124+
],
125+
output: tsx`
126+
import { useActionState } from "react";
127+
import * as ReactDOM from "react-dom";
128+
129+
async function increment(previousState, formData) {
130+
return previousState + 1;
131+
}
132+
133+
function StatefulForm({}) {
134+
const [state, formAction] = useActionState(increment, 0);
135+
return (
136+
<form>
137+
{state}
138+
<button formAction={formAction}>Increment</button>
139+
</form>
140+
);
141+
}
142+
`,
143+
settings: {
144+
"react-x": {
145+
version: "19.0.0",
146+
},
147+
},
148+
},
149+
],
150+
valid: [
151+
tsx`
152+
import { useActionState } from "react";
153+
import { useFormState } from "react-dom";
154+
155+
async function increment(previousState, formData) {
156+
return previousState + 1;
157+
}
158+
159+
function StatefulForm({}) {
160+
const [state, formAction] = useActionState(increment, 0);
161+
return (
162+
<form>
163+
{state}
164+
<button formAction={formAction}>Increment</button>
165+
</form>
166+
);
167+
}
168+
`,
169+
{
170+
code: tsx`
171+
import { useFormState } from "react-dom";
172+
173+
async function increment(previousState, formData) {
174+
return previousState + 1;
175+
}
176+
177+
function StatefulForm({}) {
178+
const [state, formAction] = useFormState(increment, 0);
179+
return (
180+
<form>
181+
{state}
182+
<button formAction={formAction}>Increment</button>
183+
</form>
184+
);
185+
}
186+
`,
187+
settings: {
188+
"react-x": {
189+
version: "17.0.0",
190+
},
191+
},
192+
},
193+
],
194+
});

0 commit comments

Comments
 (0)