.claude/skills/react-effect-decision/SKILL.md
Combine React's official "You Might Not Need an Effect" guidance with this project's stricter no direct useEffect stance. Use when writing, reviewing, or refactoring React components that might reach for useEffect, derived state, event relays, reset logic, subscriptions, or client fetching.
npx skillsauth add get-convex/components-submissions-directory react-effect-decisionInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Default rule for this project: do not reach for direct useEffect first.
Most effect usage in React code is a sign that the component is syncing state that should be derived during render, routing an event through state, or resetting local state in a way React already models better.
This skill merges:
useEffect preferenceBefore writing any effect, ask these in order:
keyuseMemouseSyncExternalStore or a focused custom hookIf you answer yes to any of 1 through 5, do not write a direct useEffect.
React allows effects for real external synchronization. This project is stricter:
useEffect in components whenever a clearer pattern existskey remounts, memoization, query libraries, Convex hooks, and focused custom hooksuseMountEffect wrapper instead of sprinkling raw useEffect through feature codeExample wrapper:
import { useEffect } from "react";
export function useMountEffect(effect: () => void | (() => void)) {
useEffect(effect, []);
}
Use that only for one time external setup like DOM focus, third party widgets, or browser API listeners that belong to the component lifecycle.
Bad smell:
useEffect(() => setX(deriveFromY(y)), [y])Do this instead:
function TodoList({ todos, showActive }: Props) {
const activeTodos = todos.filter((todo) => !todo.completed);
const visibleTodos = showActive ? activeTodos : todos;
return <List items={visibleTodos} />;
}
If it can be calculated from existing inputs, keep it out of state.
useMemoIf the calculation is pure and expensive, memoize it instead of syncing it into state:
const visibleTodos = useMemo(
() => getVisibleTodos(todos, showActive),
[todos, showActive],
);
Use memoization only when there is real repeated work to skip.
Bad smell:
Do this instead:
function PurchaseButton({ product }: Props) {
async function handleClick() {
await addToCart(product);
showNotification(`Added ${product.name}`);
}
return <button onClick={handleClick}>Buy</button>;
}
If the work happens because the user clicked, submitted, dragged, or selected, keep it in the handler.
key, not effect choreographyBad smell:
useEffect(() => setFormState(initialFromProps), [id])Do this instead:
function EditContact({ contact, onSave }: Props) {
return <EditContactForm key={contact.id} contact={contact} onSave={onSave} />;
}
If a different entity should feel like a fresh component instance, give the inner component a key.
If you cannot derive it and do not want a full remount, adjust the state during render of the same component before children render stale data.
Prefer these, in order:
keyBad smell:
fetch(...).then(setState) inside a component effectPreferred options:
Example custom hook pattern:
function useSearchResults(url: string) {
const [data, setData] = useState<Result[] | null>(null);
useEffect(() => {
let ignore = false;
fetch(url)
.then((response) => response.json())
.then((json) => {
if (!ignore) {
setData(json);
}
});
return () => {
ignore = true;
};
}, [url]);
return data;
}
That is still less preferred than a dedicated query layer, but better than scattering raw fetch effects across components.
useSyncExternalStoreFor browser or third party stores, prefer the React built in subscription model:
function useOnlineStatus() {
return useSyncExternalStore(
subscribe,
() => navigator.onLine,
() => true,
);
}
This is clearer and safer than mirroring an external mutable source through ad hoc effect code.
An effect or useMountEffect is appropriate when the component must synchronize with something outside React:
When you use one, document the external system in a comment or nearby helper.
If you are about to write useEffect, check:
keyuseSyncExternalStoreIf you cannot name the external system, you probably do not need an effect.
Use these during review:
key={entityId} remove the reset logic entirelyThe safest path in this repo is simple:
keySources:
development
Debug and troubleshoot WorkOS AuthKit authentication issues with Convex. Use when authentication fails, JWT validation errors occur, user identity returns null, email claims are missing, admin access checks fail, or sign in button does not work. Supports Netlify deployment.
development
Set up and configure WorkOS AuthKit authentication with Convex backend. Use when integrating AuthKit, configuring JWT providers, setting up environment variables, or implementing sign in and sign out flows with React and Vite. Supports Netlify deployment.
documentation
# Update project docs Use this skill after completing any feature, fix, or migration to keep the three core project tracking files in sync. Activate with: `@update-project-docs` ## Step 1: Get real dates Run this first: ```bash git log --date=short -n 10 ``` Use actual commit dates. Never use placeholder dates or future months. ## Step 2: Update TASK.md Move completed items into `## Completed` with date and time: ```markdown - [x] Feature name (YYYY-MM-DD HH:mm UTC) - [x] Sub-task det
tools
# Create a PRD Use this skill before any multi-file feature, architectural decision, or complex bug fix. Activate with: `@create-prd` ## Location and naming - All PRDs live in `prds/` folder - File name: `prds/<feature-or-problem-slug>.md` - Extension is always `.md`, not `.prd` - Use kebab-case for the filename (e.g., `prds/adding-email-auth.md`) ## Template Copy and fill in this template: ```markdown # [Feature or problem name] Created: YYYY-MM-DD HH:mm UTC Last Updated: YYYY-MM-DD HH: