biome-plugin-no-use-effect 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +195 -0
- package/no-use-effect.grit +9 -0
- package/package.json +21 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Victor-Mihai Patru
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# biome-plugin-no-use-effect
|
|
2
|
+
|
|
3
|
+
A Biome linter plugin that disallows direct `useEffect` calls in React code, encouraging safer patterns like derived state, event handlers, data-fetching libraries, and `useMountEffect`.
|
|
4
|
+
|
|
5
|
+
Inspired by [Why we banned React's useEffect](https://x.com/alvinsng/status/2033969062834045089).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install --save-dev biome-plugin-no-use-effect
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
or with your preferred package manager:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
yarn add -D biome-plugin-no-use-effect
|
|
17
|
+
pnpm add -D biome-plugin-no-use-effect
|
|
18
|
+
bun add -D biome-plugin-no-use-effect
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
Add the plugin to your Biome configuration file (`biome.json` or `biome.jsonc`):
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
|
28
|
+
"plugins": ["node_modules/biome-plugin-no-use-effect/no-use-effect.grit"]
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The plugin will now report an error whenever `useEffect` is called directly.
|
|
33
|
+
|
|
34
|
+
### Suppressing the rule
|
|
35
|
+
|
|
36
|
+
For the `useMountEffect` wrapper — the one legitimate place `useEffect` belongs — suppress it inline:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
function useMountEffect(effect: () => void | (() => void)) {
|
|
40
|
+
// biome-ignore lint/plugin/no-use-effect: useMountEffect implementation
|
|
41
|
+
useEffect(effect, []);
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## What it catches
|
|
46
|
+
|
|
47
|
+
Any direct call to `useEffect`:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
// ❌ Error
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
fetchData();
|
|
53
|
+
}, [id]);
|
|
54
|
+
|
|
55
|
+
// ❌ Error
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
setFilteredItems(items.filter((i) => i.active));
|
|
58
|
+
}, [items]);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Replacement patterns
|
|
62
|
+
|
|
63
|
+
Instead of reaching for `useEffect`, prefer:
|
|
64
|
+
|
|
65
|
+
**1. Derive state inline** — no effect needed for computed values
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// ✅
|
|
69
|
+
const filteredItems = items.filter((i) => i.active);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**2. Data-fetching libraries** — React Query, SWR, etc. handle caching/cancellation
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
// ✅
|
|
76
|
+
const { data } = useQuery(['item', id], () => fetchItem(id));
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**3. Event handlers** — if it happens because of a user action, put it in the handler
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// ✅
|
|
83
|
+
<button onClick={() => postLike()}>Like</button>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**4. useMountEffect** — for genuine one-time external sync (DOM, third-party widgets)
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
// ✅
|
|
90
|
+
function useMountEffect(effect: () => void | (() => void)) {
|
|
91
|
+
// biome-ignore lint/plugin/no-use-effect: useMountEffect implementation
|
|
92
|
+
useEffect(effect, []);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function VideoPlayer() {
|
|
96
|
+
useMountEffect(() => player.play());
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**5. key prop reset** — use React's remount semantics instead of dependency choreography
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
// ✅
|
|
104
|
+
function VideoPlayerWrapper({ videoId }) {
|
|
105
|
+
return <VideoPlayer key={videoId} videoId={videoId} />;
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Why avoid direct useEffect?
|
|
110
|
+
|
|
111
|
+
- **Dependency arrays hide coupling** — unrelated refactors silently change effect behavior
|
|
112
|
+
- **Infinite loop hazards** — `state update → render → effect → state update` cycles are easy to create and hard to debug
|
|
113
|
+
- **Effect chains** — `A sets state that triggers B` is implicit time-based control flow
|
|
114
|
+
- **Race conditions** — effect-based fetching has no built-in cancellation or deduplication
|
|
115
|
+
- **Harder to onboard** — "why did this run?" has no clear answer without tracing dependency arrays
|
|
116
|
+
|
|
117
|
+
See the React team's own guide: [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect).
|
|
118
|
+
|
|
119
|
+
## Enforce with AI agents
|
|
120
|
+
|
|
121
|
+
When AI agents (Cursor, Claude Code, etc.) write React code, `useEffect` is one of the most commonly added hooks — often "just in case." There are two complementary ways to make agents respect this rule.
|
|
122
|
+
|
|
123
|
+
### Hooks
|
|
124
|
+
|
|
125
|
+
Wire the linter into your agent's post-edit hook so violations are caught and auto-fixed before the agent considers a task done.
|
|
126
|
+
|
|
127
|
+
**Cursor** — add to `.cursor/hooks.json`:
|
|
128
|
+
|
|
129
|
+
```jsonc
|
|
130
|
+
{
|
|
131
|
+
"afterEdit": ["npx @biomejs/biome check --write ${file}"]
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Claude Code** — add to `.claude/settings.json`:
|
|
136
|
+
|
|
137
|
+
```jsonc
|
|
138
|
+
{
|
|
139
|
+
"hooks": {
|
|
140
|
+
"PostToolUse": [
|
|
141
|
+
{
|
|
142
|
+
"matcher": "Write|Edit|MultiEdit",
|
|
143
|
+
"hooks": [
|
|
144
|
+
{
|
|
145
|
+
"type": "command",
|
|
146
|
+
"command": "npx @biomejs/biome check --write $CLAUDE_TOOL_OUTPUT_PATH"
|
|
147
|
+
}
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### AGENTS.md
|
|
156
|
+
|
|
157
|
+
Document the rule in your `AGENTS.md` so agents understand the intent *before* writing code, not just after. This is the highest-leverage integration — it shifts enforcement left, from lint-time correction to generation-time avoidance.
|
|
158
|
+
|
|
159
|
+
```markdown
|
|
160
|
+
## Linting rules
|
|
161
|
+
|
|
162
|
+
- Never call `useEffect` directly. Use `useMountEffect()` for mount-only side effects.
|
|
163
|
+
Alternatives: derived state, data-fetching libraries (React Query / SWR),
|
|
164
|
+
event handlers, and key-based remount.
|
|
165
|
+
- The one exception is inside the `useMountEffect` implementation itself,
|
|
166
|
+
which must be suppressed with `// biome-ignore lint/plugin/no-use-effect: useMountEffect implementation`.
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Contributing
|
|
170
|
+
|
|
171
|
+
### Setup
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
git clone https://github.com/YOUR_USERNAME/biome-plugin-no-use-effect.git
|
|
175
|
+
cd biome-plugin-no-use-effect
|
|
176
|
+
npm install
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Testing
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
npm test
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Tests are in `test/plugin.test.ts` using Vitest. Fixtures live in `test/fixtures/`.
|
|
186
|
+
|
|
187
|
+
### Publishing
|
|
188
|
+
|
|
189
|
+
1. Update the version in `package.json`
|
|
190
|
+
2. `npm publish`
|
|
191
|
+
3. Tag the release on GitHub
|
|
192
|
+
|
|
193
|
+
## License
|
|
194
|
+
|
|
195
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
language js
|
|
2
|
+
|
|
3
|
+
`useEffect($args)` as $call where {
|
|
4
|
+
register_diagnostic(
|
|
5
|
+
span=$call,
|
|
6
|
+
message="Direct useEffect() calls are not allowed. Use useMountEffect() for mount-only effects, or refactor to derived state, event handlers, or a data-fetching library.",
|
|
7
|
+
severity="error"
|
|
8
|
+
)
|
|
9
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "biome-plugin-no-use-effect",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A Biome linter plugin that disallows direct useEffect calls in React components.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Victor-Mihai Patru",
|
|
7
|
+
"type": "commonjs",
|
|
8
|
+
"files": [
|
|
9
|
+
"no-use-effect.grit",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"test": "vitest run"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@biomejs/biome": "2.3.5",
|
|
17
|
+
"@types/node": "24.10.1",
|
|
18
|
+
"typescript": "5.9.3",
|
|
19
|
+
"vitest": "4.0.9"
|
|
20
|
+
}
|
|
21
|
+
}
|