技能创建器
skill-creator
创建、改进和评估自定义技能,把专业流程转化为可重复使用的 AI 能力。
no-use-effect【数据与研究】Prevent unnecessary React `useEffect` usage by steering code toward derived state, event handlers, memoization, `key`-based resets, `useSyncExternalStore`, and framework or query-library data APIs. Use when writing or reviewing React/Next.js components, refactoring effect-heavy code, or when a change introduces `useEffect`, `useLayoutEffect`, or depe…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
useEffect usage by steering code toward derived state,
event handlers, memoization, key-based resets, useSyncExternalStore, and
framework or query-library data APIs. Use when writing or reviewing React/Next.js
components, refactoring effect-heavy code, or when a change introduces useEffect,
useLayoutEffect, or dependency-array churn. Triggers on: "avoid useEffect",
"remove useEffect", "you might not need an effect", "effect dependencies",
"derived state", "React side effects".Treat raw useEffect as a last resort, not a default tool. Most component logic belongs in
render, event handlers, framework data APIs, or narrower React primitives.
This skill is based primarily on the React docs (You Might Not Need an Effect,
Synchronizing with Effects, Removing Effect Dependencies), with supporting guidance from
Dan Abramov, Kent C. Dodds, TkDodo, and team practices that ban or heavily restrict raw
useEffect.
Before adding an Effect, answer this question:
"What external system am I synchronizing with, and why can't this be done during render, in an event handler, or with a more specific primitive?"
If you cannot name the external system, do not write the Effect.
External systems include:
Not external systems:
Do not mirror props or state into more state.
// Bad
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
// Good
const fullName = `${firstName} ${lastName}`;
Use this for filtered lists, labels, counts, booleans, selected objects, and other values that can be computed from current inputs.
If the calculation is slow, cache it with useMemo. Do not use an Effect that immediately
calls setState with a computed value.
const visibleTodos = useMemo(() => getVisibleTodos(todos, filter), [todos, filter]);
First ask whether the work is actually expensive. If not, calculate it directly during render.
If the code runs because the user clicked, submitted, selected, dragged, or typed, keep it in the corresponding handler.
// Bad
useEffect(() => {
if (shouldSubmit) {
void saveForm(form);
}
}, [shouldSubmit, form]);
// Good
async function handleSubmit() {
await saveForm(form);
}
Never build "set flag -> Effect notices flag -> Effect does the real work" flows unless the flag is synchronizing with a real external system.
key when you want a fresh instanceIf a component should become a brand-new instance when some identity changes, give it a
different key. React will unmount the old subtree and mount a new one, which resets all local
state below that point without an Effect.
// Bad
function Profile({ userId }: { userId: string }) {
const [comment, setComment] = useState("");
useEffect(() => {
setComment("");
}, [userId]);
return <input value={comment} onChange={(e) => setComment(e.target.value)} />;
}
// Good
function ProfilePage({ userId }: { userId: string }) {
return <Profile key={userId} userId={userId} />;
}
function Profile({ userId }: { userId: string }) {
const [comment, setComment] = useState("");
return <input value={comment} onChange={(e) => setComment(e.target.value)} />;
}
Use this when the identity really changed: user, document, chat room, wizard step, or form instance. Do not use it when you only want to tweak one field, because remounting resets the entire subtree.
If an Effect exists only to notify a parent after local state changed, the data model is usually wrong. Prefer lifting state up or updating both sides in the same event.
// Bad
function Toggle({ onChange }: { onChange: (next: boolean) => void }) {
const [isOn, setIsOn] = useState(false);
useEffect(() => {
onChange(isOn);
}, [isOn, onChange]);
return <button onClick={() => setIsOn(!isOn)}>{isOn ? "On" : "Off"}</button>;
}
// Good
function Toggle({ isOn, onChange }: { isOn: boolean; onChange: (next: boolean) => void }) {
return <button onClick={() => onChange(!isOn)}>{isOn ? "On" : "Off"}</button>;
}
useSyncExternalStore for external subscriptionsUse useSyncExternalStore when the source of truth lives outside React and changes over time.
This is React's built-in primitive for subscribing to external stores.
Good fits:
useOnlineStatus() or useMediaQuery()The shape is:
const value = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
Where:
subscribe tells React how to listen for changes and return cleanupgetSnapshot reads the current value from the external storegetServerSnapshot provides the initial server and hydration value when SSR is involved// Bad
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function updateStatus() {
setIsOnline(navigator.onLine);
}
updateStatus();
window.addEventListener("online", updateStatus);
window.addEventListener("offline", updateStatus);
return () => {
window.removeEventListener("online", updateStatus);
window.removeEventListener("offline", updateStatus);
};
}, []);
return isOnline;
}
// Good
const isOnline = useSyncExternalStore(subscribe, () => navigator.onLine, () => true);
Prefer this over hand-rolled subscription Effects in components.
Important caveats from the React docs:
getSnapshot must return a stable cached value unless the store actually changedsubscribe changes identity every render, React will resubscribe every rendergetServerSnapshot must match between server render and hydrationDo not use this for:
useState, useReducer, or contextAvoid raw fetch-in-Effect code in app components when a framework loader or query library is available.
Preferred order:
Next.js, React Router, Remix, server components, route loaders)TanStack Query, SWR)useEffect only if none of the above fitIf you must fetch in an Effect, handle cleanup so stale responses are ignored.
// Bad
useEffect(() => {
void fetchResults(query).then(setResults);
}, [query]);
// Good
useEffect(() => {
let ignore = false;
void fetchResults(query).then((next) => {
if (!ignore) {
setResults(next);
}
});
return () => {
ignore = true;
};
}, [query]);
Sometimes an Effect is correct: sockets, observers, widgets, media controls, focus management, layout measurement, or DOM APIs that must stay in sync with rendered output.
When that happens:
useLayoutEffect only for pre-paint measurement or visual correctness// Bad
function ChatRoom({ roomId }: { roomId: string }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
}, [roomId]);
}
// Good
function useChatConnection(roomId: string) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
}
If your team bans raw useEffect, put these escape hatches behind approved hooks instead of
sprinkling Effects through feature code.
Treat these as red flags during authoring or review:
useEffect(() => setX(...), [...]) for derived stateeslint-disable, refs-as-flags, or intentionally stale closuresBefore accepting a new Effect, verify all of these:
key, lifted state, and useSyncExternalStore
were all considered firstFor teams trying to ban or heavily restrict raw useEffect, use this policy:
useEffect for derived state, events, or resets.key, useMemo, and useSyncExternalStore.react-hooks/exhaustive-deps to "make it work".For source material and further reading, see references/sources.md.