At Factory, we have a simple, yet important frontend rule: no...

@alvinsng
Alvin Sng@alvinsng
86 views Mar 18, 2026 ~6 min read
Advertisement
1
At Factory, we have a simple, yet important frontend rule: no useEffect. Yes, it sounds strict. In practice, it has made our codebase easier to reason about and much harder to accidentally break.
Media image
2
What we mean by "banned"?
3
We never call useEffect directly. For the rare case where you need to sync with an external system on mount, we have useMountEffect().
4
```typescript
export function useMountEffect(effect: () => void | (() => void)) {
/* eslint-disable no-restricted-syntax */
useEffect(effect, []);
}

```
5
Most useEffect usage is compensating for something React already gives you better primitives for: derived state, event handlers, and data-fetching abstractions.
6
This matters even more now that agents are writing the code. useEffect is often added 'just in case,' but that move is the seed of the next race condition or infinite loop. Banning the hook forces the logic to be declarative and predictable.
7
## Hard lesson
8
We did not arrive at this rule so easily. We got there through production bugs.
9
Media image
10
## Compounding problems
11
Brittleness: Dependency arrays hide coupling. A refactor that seems unrelated can quietly change effect behavior.
12
Infinite loops: It is easy to create state update -> render -> effect -> state update loops, especially when dependency lists get "fixed" incrementally.
13
Dependency hell: Effect chains (A sets state that triggers B) are time-based control flow. They are hard to trace and easy to regress.
14
Debugging pain: You end up asking "why did this run?" or "why did this not run?" without a clear entrypoint like a handler.
15
## A cultural meme
16
At this point, useEffect is a running joke across the React community.
21
## Official React team
22
This is not just our internal preference. React has a full guide called You Might Not Need an Effect.
23
The problem? useEffect shifted many teams from explicit event-driven logic to implicit synchronization logic. Instead of reacting to a clear event, you are managing relationships between values and side effects through dependency arrays.
24
## The solution
25
Below are the five patterns that replaced most of our useEffect usage.
26
Rule 1: derive state, do not sync it
27
Most effects that set state from other state are unnecessary and add extra renders.
28
```typescript
// ❌ BAD: Two render cycles - first stale, then filtered
function ProductList() {
const [products, setProducts] = useState([]);
const [filteredProducts, setFilteredProducts] = useState([]);

useEffect(() => {
setFilteredProducts(products.filter((p) => p.inStock));
}, [products]);
}

// ✅ GOOD: Compute inline in one render
function ProductList() {
const [products, setProducts] = useState([]);
const filteredProducts = products.filter((p) => p.inStock);
}
```
29
This pattern also creates loop hazards:
30
```typescript
// ❌ BAD: total in deps can loop
function Cart({ subtotal }) {
const [tax, setTax] = useState(0);
const [total, setTotal] = useState(0);

useEffect(() => {
setTax(subtotal * 0.1);
}, [subtotal]);

useEffect(() => {
setTotal(subtotal + tax);
}, [subtotal, tax, total]);
}

// ✅ GOOD: No effects required
function Cart({ subtotal }) {
const tax = subtotal * 0.1;
const total = subtotal + tax;
}
```
31
Smell test:
32
• You are about to write useEffect(() => setX(deriveFromY(y)), [y])
33
• You have state that only mirrors other state or props
34
Rule 2: use data-fetching libraries
35
Effect-based fetching often creates race conditions and duplicated caching logic.
36
```typescript
// ❌ BAD: Race condition risk
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);

useEffect(() => {
fetchProduct(productId).then(setProduct);
}, [productId]);
}

// ✅ GOOD: Query library handles cancellation/caching/staleness
function ProductPage({ productId }) {
const { data: product } = useQuery(['product', productId], () =>
fetchProduct(productId)
);
}
```
37
Smell test:
38
• Your effect does fetch(...) and then setState(...)
39
• You are re-implementing caching, retries, cancellation, or stale handling
40
Rule 3: event handlers, not effects
41
If a user clicks a button, do the work in the handler.
42
```typescript
// ❌ BAD: Effect as an action relay
function LikeButton() {
const [liked, setLiked] = useState(false);

useEffect(() => {
if (liked) {
postLike();
setLiked(false);
}
}, [liked]);

return ;
}

// ✅ GOOD: Direct event-driven action
function LikeButton() {
return ;
}
```
43
Smell test:
44
• State is used as a flag so an effect can do the real action
45
• You are building "set flag -> effect runs -> reset flag" mechanics
46
Rule 4: useMountEffect for one-time external sync
47
useMountEffect is just useEffect(..., []) wrapped in a named hook to make intent explicit and prevent ad-hoc effect usage in components.
48
```typescript
function useMountEffect(callback: () => void | (() => void)) {
useEffect(callback, []);
}
```
49
Good uses:
50
• DOM integration (focus, scroll)
51
• Third-party widget lifecycles
52
• Browser API subscriptions
53
A useful pattern is conditional mounting.
54
```typescript
// ❌ BAD: Guard inside effect
function VideoPlayer({ isLoading }) {
useEffect(() => {
if (!isLoading) playVideo();
}, [isLoading]);
}

// ✅ GOOD: Mount only when preconditions are met
function VideoPlayerWrapper({ isLoading }) {
if (isLoading) return ;
return ;
}

function VideoPlayer() {
useMountEffect(() => playVideo());
}

// ✅ ALSO GOOD: Persistent shell + conditional instance
function VideoPlayerInstance() {
useMountEffect(() => playVideo());
}

function VideoPlayerContainer({ isLoading }) {
return (
<>

{!isLoading && }

);
}
```
55
Smell test:
56
• You are synchronizing with an external system
57
• The behavior is naturally "setup on mount, cleanup on unmount"
58
Rule 5: reset with key, not dependency choreography
59
```typescript
// ❌ BAD: Effect attempts to emulate remount behavior
function VideoPlayer({ videoId }) {
useEffect(() => {
loadVideo(videoId);
}, [videoId]);
}

// ✅ GOOD: key forces clean remount
function VideoPlayer({ videoId }) {
useMountEffect(() => {
loadVideo(videoId);
});
}

function VideoPlayerWrapper({ videoId }) {
return ;
}
```
60
If the requirement is "start fresh when ID changes," use React's remount semantics directly.
61
Smell test:
62
• You are writing an effect whose only job is to reset local state when an ID/prop changes
63
• You want the component to behave like a brand-new instance for each entity
64
## Forcing function for nesting
65
Media image
66
Banning direct useEffect works as a forcing function for cleaner tree design. Parents own orchestration and lifecycle boundaries. Children can assume preconditions are already met. You get simpler components and fewer hidden side effects.
67
This is basically Unix philosophy applied to React components: each unit does one job, and coordination happens at clear boundaries.
68
## Choose your bug
69
Media image
70
No team ships zero bugs. The question is which failure mode you want.
71
useMountEffect failures are usually binary and loud (it ran once, or not at all). Direct useEffect failures often degrade gradually and show up as flaky behavior, performance issues, or loops before a hard failure.
72
## You should too
73
After operating a no-direct-useEffect codebase on a large app, we saw fewer infinite loops, fewer race-condition regressions, and faster onboarding because control flow became easier to follow.
74
The rule felt extreme at first. It now feels like a baseline engineering guardrail.
75
## How to adopt this rule
76
Enforce this with lint rules, clear agent guidance in AGENTS.md. To mass fix existing use cases, try our new Missions product to mass fix each violation. You can use this post as a reference guide for your droids.
Actions
What You Can Do
  • Export as PDF or Markdown
  • Batch Export to Notion
  • Bookmark & Highlight
  • LinkedIn & Instagram Carousel Maker
Create Free Account

Includes 7-day Premium trial

Advertisement