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 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
+ }