<core_principles>
Core Principles
- Minimize useEffect Usage
Think harder before adding useEffect. Most scenarios have better alternatives:
<when_not_to_use_effect>
When NOT to Use useEffect
Data transformation for rendering:
-
❌ Don't: Use Effect to compute derived state
-
✅ Do: Calculate during render at top level
// ❌ Bad: Cascading updates const [filteredItems, setFilteredItems] = useState<Item[]>([]); useEffect(() => { setFilteredItems(items.filter(item => item.active)); }, [items]);
// ✅ Good: Direct computation const filteredItems = items.filter(item => item.active);
Handling user events:
-
❌ Don't: Use Effect to respond to user actions
-
✅ Do: Handle logic in event handlers
// ❌ Bad: Lost interaction context useEffect(() => { if (buttonClicked) { submitForm(); } }, [buttonClicked]);
// ✅ Good: Explicit intent const handleSubmit = () => { submitForm(); };
Caching expensive computations:
-
❌ Don't: Store computed results in state via Effects
-
✅ Do: Use useMemo
// ❌ Bad: Manual memoization const [expensiveResult, setExpensiveResult] = useState<Result | null>(null); useEffect(() => { setExpensiveResult(computeExpensiveValue(data)); }, [data]);
// ✅ Good: useMemo const expensiveResult = useMemo(() => computeExpensiveValue(data), [data]);
Resetting state on prop changes:
-
❌ Don't: Use Effect to reset state
-
✅ Do: Use key prop to force remount
// ❌ Bad: Manual synchronization useEffect(() => { setLocalState(defaultValue); }, [userId]);
// ✅ Good: Key-based reset <Profile key={userId} userId={userId} />
</when_not_to_use_effect>
<legitimate_use_cases>
Legitimate useEffect Use Cases
Only use Effects for:
-
Synchronizing with external systems (browser APIs, third-party widgets)
-
Data fetching with proper cleanup (avoid race conditions)
-
Subscriptions (prefer useSyncExternalStore when possible)
Pattern for data fetching (if not using query client):
useEffect(() => { let ignore = false;
async function fetchData() { const result = await api.getData(); if (!ignore) { setData(result); } }
fetchData(); return () => { ignore = true; }; // Cleanup to prevent race conditions }, [dependency]);
</legitimate_use_cases> </core_principles>
<component_definition>
- Component Definition Style
Always use FC type annotation:
import { FC, PropsWithChildren } from 'react';
// For components without children type ButtonProps { label: string; onClick: () => void; }
const Button: FC<ButtonProps> = ({ label, onClick }) => { return <button onClick={onClick}>{label}</button>; };
// For components that accept children type CardProps { title: string; }
const Card: FC<PropsWithChildren<CardProps>> = ({ title, children }) => { return ( <div> <h2>{title}</h2> <div>{children}</div> </div> ); };
Never use function declaration syntax:
// ❌ Bad: Avoid this style function MyComponent(props: Props) { return <div />; }
</component_definition>
<api_requests>
- API Request Handling
Never use fetch directly in components. Always use the project's query client.
<detection_workflow>
Detection Workflow
Check project dependencies in package.json:
-
@apollo/client → Use Apollo Client hooks
-
@tanstack/react-query → Use Tanstack Query hooks
-
swr → Use SWR hooks
Search for existing usage patterns:
-
Look for useQuery , useMutation , useSWR , useApolloClient in codebase
-
Follow established patterns for consistency
Apply appropriate client:
<apollo_client> Apollo Client (GraphQL):
import { useQuery, useMutation, gql } from '@apollo/client';
const GET_USER = gql query GetUser($id: ID!) { user(id: $id) { id name email } };
const UserProfile: FC<{ userId: string }> = ({ userId }) => { const { data, loading, error } = useQuery(GET_USER, { variables: { id: userId }, });
if (loading) return <Spinner />; if (error) return <ErrorMessage error={error} />;
return <div>{data.user.name}</div>; };
// Mutations
const UPDATE_USER = gql mutation UpdateUser($id: ID!, $name: String!) { updateUser(id: $id, name: $name) { id name } };
const EditForm: FC = () => { const [updateUser, { loading }] = useMutation(UPDATE_USER);
const handleSubmit = async (values: FormValues) => { await updateUser({ variables: { id: values.id, name: values.name } }); };
return <form onSubmit={handleSubmit}>...</form>; };
</apollo_client>
<tanstack_query> Tanstack Query (REST):
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
const UserProfile: FC<{ userId: string }> = ({ userId }) => { const { data, isLoading, error } = useQuery({ queryKey: ['user', userId], queryFn: () => api.getUser(userId), });
if (isLoading) return <Spinner />; if (error) return <ErrorMessage error={error} />;
return <div>{data.name}</div>; };
// Mutations with cache invalidation const EditForm: FC = () => { const queryClient = useQueryClient();
const mutation = useMutation({ mutationFn: (values: FormValues) => api.updateUser(values), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['user'] }); }, });
const handleSubmit = (values: FormValues) => { mutation.mutate(values); };
return <form onSubmit={handleSubmit}>...</form>; };
</tanstack_query>
const UserProfile: FC<{ userId: string }> = ({ userId }) => { const { data, error, isLoading } = useSWR( /api/users/${userId} , fetcher );
if (isLoading) return ; if (error) return ;
return {data.name}; };
// Mutations const EditForm: FC = () => { const { trigger, isMutating } = useSWRMutation( '/api/users', updateUser );
const handleSubmit = async (values: FormValues) => { await trigger(values); };
return ...; };
</swr> </detection_workflow>
Benefits of query clients:
- Automatic caching and deduplication
- Loading/error state management
- Race condition handling
- Cache invalidation and refetching
- Optimistic updates support </api_requests> </core_principles>
<workflow>
Implementation Workflow
-
Before writing component:
- Identify data dependencies and state requirements
- Think harder: Can state be derived instead of stored?
- Plan event handlers before considering Effects
-
During implementation:
- Define component with FC type annotation
- Calculate derived values at top level
- Use useMemo only for expensive computations
- Handle user interactions in event handlers
- Use query client hooks for API requests
-
Effect review checklist:
- Is this synchronizing with an external system?
- Could this be a calculated value instead?
- Should this be in an event handler?
- Am I using the right hook (useMemo, key prop)?
- If data fetching, is query client available?
-
If Effect is necessary:
- Document why Effect is required
- Implement proper cleanup to prevent memory leaks
- Handle race conditions for async operations </workflow>
<anti_patterns>
Anti-Patterns to Avoid
❌ Chaining Effects:
// Bad: Effects triggering each other
useEffect(() => setB(a), [a]);
useEffect(() => setC(b), [b]);
// Good: Direct computation or single event handler
const b = computeB(a);
const c = computeC(b);
❌ Effect-based initialization:
// Bad: One-time initialization in Effect
useEffect(() => {
setData(expensiveInit());
}, []);
// Good: useState with initializer
const [data] = useState(() => expensiveInit());
❌ Direct fetch calls:
// Bad: Manual fetch in component
useEffect(() => {
fetch('/api/data').then(res => res.json()).then(setData);
}, []);
// Good: Use query client
const { data } = useQuery({ queryKey: ['data'], queryFn: fetchData });
</anti_patterns>