React useEffect best practices and team policy. RULE - Never use useEffect directly — use useMountEffect() for mount-only external sync. Use when writing/reviewing useEffect, useState for derived values, data fetching, or state synchronization.
Team rule: Never call useEffect directly. For the rare case where you need to sync with an external system on mount, use useMountEffect():
export function useMountEffect(effect: () => void | (() => void)) {
/* eslint-disable no-restricted-syntax */
useEffect(effect, []);
}Most useEffect usage is compensating for something React already gives you better primitives for: derived state, event handlers, and data-fetching abstractions.
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.
useMountEffect failures are binary and loud (it ran once, or not at all). Direct useEffect failures often degrade gradually — flaky behavior, performance issues, or loops before a hard failure.
| Situation | DON'T | DO |
|---|---|---|
| Derived state from props/state | useState + useEffect | Calculate during render |
| Expensive calculations | useEffect to cache | useMemo |
| Reset state on prop change | useEffect with setState | key prop |
| User event responses | useEffect watching state | Event handler directly |
| Notify parent of changes | useEffect calling onChange | Call in event handler |
| Fetch data | useEffect without cleanup | useQuery from @tanstack/react-query |
| One-time external sync on mount | useEffect(..., []) | useMountEffect() |
| Conditional mount logic | Guard inside useEffect | Split into wrapper + child component |
|---|
const fullName = firstName + ' ' + lastNameuseQuery from @tanstack/react-queryNeed to respond to something?
├── User interaction (click, submit, drag)?
│ └── Use EVENT HANDLER
├── Component appeared on screen?
│ └── Use useMountEffect (external sync, analytics)
│ └── Conditional? Split into Wrapper + Child
├── Props/state changed and need derived value?
│ └── CALCULATE DURING RENDER
│ └── Expensive? Use useMemo
├── Need to reset state when prop changes?
│ └── Use KEY PROP on component
└── Need to respond to prop/id change with fresh state?
└── Use KEY PROP on wrapper, useMountEffect insideDon't guard inside effects — split into wrapper + child so the child can assume preconditions are met:
// ❌ 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 <LoadingScreen />;
return <VideoPlayer />;
}
function VideoPlayer() {
useMountEffect(() => playVideo());
}
// ✅ ALSO GOOD: Persistent shell + conditional instance
function VideoPlayerContainer({ isLoading }) {
return (
<>
<VideoPlayerShell isLoading={isLoading} />
{!isLoading && <VideoPlayerInstance />}
</>
);
}
function VideoPlayerInstance() {
useMountEffect(() => playVideo());
}This is Unix philosophy applied to React: each unit does one job, coordination happens at clear boundaries. Parents own orchestration and lifecycle boundaries. Children assume preconditions are already met.
// ❌ BAD: Effect attempts to emulate remount behavior
function VideoPlayer({ videoId }) {
useEffect(() => {
loadVideo(videoId);
}, [videoId]);
}
// ✅ GOOD: key forces clean remount
function VideoPlayerWrapper({ videoId }) {
return <VideoPlayer key={videoId} videoId={videoId} />;
}
function VideoPlayer({ videoId }) {
useMountEffect(() => {
loadVideo(videoId);
});
}function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
// ✅ Correct: runs because component was displayed
useMountEffect(() => {
post('/analytics/event', { eventName: 'visit_form' });
});
// ✅ Correct: runs because user submitted
function handleSubmit(e) {
e.preventDefault();
post('/api/register', { firstName, lastName });
}
}./anti-patterns.md) - Common mistakes with fixes./alternatives.md) - useMemo, key prop, lifting state, useSyncExternalStore