);
};
```
**Benefits of early returns:**
- Reduces nesting depth
- Main success render stays at bottom (most important case)
- Each condition is independent and easy to test
- TypeScript can narrow types after guards
**Works same in JavaScript:**
```jsx
if (isLoading) return ;
if (error) return ;
if (!data) return ;
```
## Section 7: Default Render
**Keep the success/default render at the bottom:**
```tsx
export const UserProfile = ({ userId }: UserProfileProps): JSX.Element => {
// [Sections 1-6 above]
// Success state - the main component render
return (
{displayName}
{isEditing ? (
) : (
)}
Member since {formattedDate}
);
};
```
**Why default render goes last:**
- Most important case (happy path) is most visible
- After all error states eliminated
- All data and handlers already declared
- Mirrors how you mentally reason about component
## Refactoring: Extract to Custom Hooks
**When components grow complex, extract logic into custom hooks:**
```tsx
// usePost.ts
export const usePost = (postId: string) => {
const [isEditing, setIsEditing] = useState(false);
const { data: post, isLoading, error } = useQuery(
['post', postId],
() => api.getPost(postId)
);
const { mutate: updatePost } = useMutation(api.updatePost);
const formattedContent = post?.content
? formatMarkdown(post.content)
: '';
const handleEdit = () => setIsEditing(true);
const handleSave = (updates: Partial) => {
updatePost(updates);
setIsEditing(false);
};
return {
post,
isLoading,
error,
isEditing,
formattedContent,
handleEdit,
handleSave,
};
};
// PostView.tsx - Clean component focused on presentation
export const PostView = ({ postId }: PostViewProps): JSX.Element => {
const {
post,
isLoading,
error,
isEditing,
formattedContent,
handleEdit,
handleSave,
} = usePost(postId);
if (isLoading) return ;
if (error) return ;
if (!post) return ;
return (
{/* Simple presentation-focused JSX */}
);
};
```
**When to extract to custom hooks:**
- Component logic exceeds 50 lines
- State management becomes complex
- Multiple effects interact
- Logic is reusable across components
- Component file exceeds 200 lines
**Hook naming:** `use[Domain]` pattern (e.g., `usePost`, `useAuth`, `useCart`)
**JavaScript version:**
```jsx
// usePost.js - Same pattern
export const usePost = (postId) => {
// Same logic, no type annotations
return {
post,
isLoading,
// ...
};
};
```
## React Hooks: Antipatterns and Gotchas
**Common mistakes that cause bugs, performance issues, and infinite loops:**
### Antipattern 1: useEffect "onChange" Callback
**Problem:** Using `useEffect` to notify parent components whenever state changes.
```tsx
type FormProps = {
initialValue: string;
onChange: (value: string) => void;
};
export const Form = ({ initialValue, onChange }: FormProps) => {
const [formValue, setFormValue] = useState(initialValue);
// ❌ Bad: Creates extra re-render cycle
useEffect(() => {
onChange(formValue);
}, [formValue, onChange]);
return (
setFormValue(e.target.value)}
/>
);
};
```
**Why it's problematic:**
- Causes double render: state update → component re-render → `useEffect` queued → `useEffect` runs → parent updates → child re-renders again
- If parent's `onChange` modifies `formValue`, creates infinite loop
- ESLint exhaustive-deps forces including `onChange`, worsening the issue
```tsx
// ✅ Good: Call onChange directly when setting state
export const Form = ({ initialValue, onChange }: FormProps) => {
const [formValue, setFormValue] = useState(initialValue);
const handleChange = (value: string) => {
setFormValue(value);
onChange(value); // Notify parent immediately
};
return (
handleChange(e.target.value)}
/>
);
};
```
```tsx
// ✅ Good: Or inline if simple
export const Form = ({ initialValue, onChange }: FormProps) => {
const [formValue, setFormValue] = useState(initialValue);
return (
{
const value = e.target.value;
setFormValue(value);
onChange(value);
}}
/>
);
};
```
**JavaScript version:**
```jsx
// Same pattern applies
export const Form = ({ initialValue, onChange }) => {
const [formValue, setFormValue] = useState(initialValue);
const handleChange = (value) => {
setFormValue(value);
onChange(value);
};
return (
handleChange(e.target.value)}
/>
);
};
```
### Antipattern 2: useState Initial Value Confusion
**Problem:** Expecting `useState` to update when props change after initial render.
```tsx
type UserProfileProps = {
initialName: string;
};
export const UserProfile = ({ initialName }: UserProfileProps) => {
// ❌ Bad: Only uses initialName on first render
// If initialName prop changes, userName stays the same!
const [userName, setUserName] = useState(initialName);
return (
setUserName(e.target.value)}
/>
);
};
```
**Why it's problematic:**
- `useState` initializer runs only once (first render)
- Prop changes don't update state automatically
- Function initializers (`useState(() => expensive())`) also run every render but discard results after first render
```tsx
// ✅ Good: Use useEffect to sync when prop changes
export const UserProfile = ({ initialName }: UserProfileProps) => {
const [userName, setUserName] = useState(initialName);
useEffect(() => {
setUserName(initialName);
}, [initialName]);
return (
setUserName(e.target.value)}
/>
);
};
```
```tsx
// ✅ Better: Use key prop to reset component
// Parent component
// This forces React to create fresh component when userId changes
```
```tsx
// ✅ Best: Don't duplicate state if you don't need local modifications
export const UserProfile = ({ name }: UserProfileProps) => {
return (
{name}
);
};
```
**When to use expensive function initializer:**
```tsx
// ✅ Good: Function only runs once
const [state, setState] = useState(() => {
return expensiveComputation(props.value);
});
// ❌ Bad: expensiveComputation runs every render
const [state, setState] = useState(expensiveComputation(props.value));
```
**JavaScript version:**
```jsx
// Same patterns apply
export const UserProfile = ({ initialName }) => {
const [userName, setUserName] = useState(initialName);
useEffect(() => {
setUserName(initialName);
}, [initialName]);
return
{/* ... */}
;
};
```
### Antipattern 3: Non-Exhaustive useEffect Dependencies
**Problem:** Omitting dependencies from `useEffect` to avoid triggering effects.
```tsx
type ModalProps = {
onOpen: () => void;
onClose: () => void;
};
export const Modal = ({ onOpen, onClose }: ModalProps) => {
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
if (isOpen) {
onOpen(); // ❌ Uses onOpen but not in dependencies
} else {
onClose(); // ❌ Uses onClose but not in dependencies
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen]); // Missing onOpen, onClose
// ...
};
```
**Why it's problematic:**
- **Stale closures:** Effect captures old versions of callbacks with outdated data
- **Async bugs:** If `onOpen`/`onClose` dependencies change, incorrect callbacks run
- **Data inconsistency:** Cascading issues when unmemoized callbacks throughout component tree
- **Silent failures:** Logic appears to work but operates on stale data
```tsx
// ✅ Good: Include all dependencies
export const Modal = ({ onOpen, onClose }: ModalProps) => {
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
if (isOpen) {
onOpen();
} else {
onClose();
}
}, [isOpen, onOpen, onClose]); // All dependencies included
// ...
};
```
```tsx
// ✅ Better: Memoize callbacks in parent
const Parent = () => {
const handleOpen = useCallback(() => {
console.log('Modal opened');
}, []);
const handleClose = useCallback(() => {
console.log('Modal closed');
}, []);
return ;
};
```
```tsx
// ✅ Best: Refactor to eliminate effect dependency issues
export const Modal = ({ onOpen, onClose }: ModalProps) => {
const handleToggle = (nextIsOpen: boolean) => {
if (nextIsOpen) {
onOpen();
} else {
onClose();
}
};
return (
);
};
```
**Key principle:** Missing dependencies reveal design issues. Fix the design, don't silence the warning.
**JavaScript version:**
```jsx
// Same pattern - dependencies matter regardless of types
export const Modal = ({ onOpen, onClose }) => {
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
if (isOpen) {
onOpen();
} else {
onClose();
}
}, [isOpen, onOpen, onClose]);
// ...
};
```
### Hooks Best Practices Summary
**DO:**
- ✅ Call `onChange` callbacks directly when setting state (not in `useEffect`)
- ✅ Use `useEffect` with full dependency arrays (trust ESLint)
- ✅ Memoize callbacks with `useCallback` when passed as props
- ✅ Use function initializers for expensive `useState` computations
- ✅ Reset state via `key` prop instead of syncing with `useEffect`
**DON'T:**
- ❌ Use `useEffect` to notify parent of state changes
- ❌ Expect `useState` initial value to update with prop changes
- ❌ Omit dependencies from `useEffect` to prevent re-runs
- ❌ Disable exhaustive-deps ESLint rule to hide issues
- ❌ Run expensive computations in `useState` initializer without function wrapper
**When you see these patterns:**
| Pattern | Problem | Solution |
|---------|---------|----------|
| `useEffect(() => onChange(value), [value])` | Double render | Call `onChange` when setting state |
| `useState(props.value)` with changing prop | Stale state | Use `key` prop or `useEffect` to sync |
| `useEffect(..., [])` with missing deps | Stale closures | Include all dependencies |
| `useState(expensive())` | Runs every render | Use `useState(() => expensive())` |
## Complete Example: TypeScript
```tsx
// UserProfile.tsx
import React, { useState, useEffect, useRef } from 'react';
import { useQuery, useMutation } from 'react-query';
import { format } from 'date-fns';
import { api } from '@/services/api';
import { Button } from '@/components/Button';
import { EditForm } from './EditForm';
import { UserDetails } from './UserDetails';
const StyledContainer = styled.div`
padding: 2rem;
max-width: 800px;
margin: 0 auto;
`;
const StyledHeader = styled.header`
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
`;
const StyledTitle = styled.h1`
font-size: 2rem;
font-weight: 600;
`;
const StyledFooter = styled.footer`
margin-top: 2rem;
padding-top: 1rem;
border-top: 1px solid #e5e7eb;
color: #6b7280;
`;
type UserProfileProps = {
userId: string;
onUpdate?: (user: User) => void;
};
export const UserProfile = ({
userId,
onUpdate
}: UserProfileProps): JSX.Element => {
// Local state
const [isEditing, setIsEditing] = useState(false);
const inputRef = useRef(null);
// Data hooks
const { data: user, isLoading, error } = useQuery(
['user', userId],
() => api.getUser(userId)
);
const { mutate: updateUser } = useMutation(api.updateUser);
// Effects
useEffect(() => {
if (isEditing && inputRef.current) {
inputRef.current.focus();
}
}, [isEditing]);
// Post-processing
const displayName = user
? `${user.firstName} ${user.lastName}`
: '';
const formattedDate = user?.createdAt
? format(user.createdAt, 'PPP')
: '';
// Handlers
const handleEdit = () => setIsEditing(true);
const handleCancel = () => setIsEditing(false);
const handleSave = (updates: Partial) => {
updateUser(updates, {
onSuccess: (updatedUser) => {
setIsEditing(false);
onUpdate?.(updatedUser);
},
});
};
// Conditional renders
if (isLoading) return ;
if (error) return ;
if (!user) return ;
// Default render
return (
{displayName}
{isEditing ? (
) : (
)}
Member since {formattedDate}
);
};
```
## Complete Example: JavaScript
```jsx
// UserProfile.jsx
import React, { useState, useEffect, useRef } from 'react';
import { useQuery, useMutation } from 'react-query';
import { format } from 'date-fns';
import { api } from '@/services/api';
import { Button } from '@/components/Button';
import { EditForm } from './EditForm';
import { UserDetails } from './UserDetails';
const StyledContainer = styled.div`
padding: 2rem;
max-width: 800px;
margin: 0 auto;
`;
const StyledHeader = styled.header`
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
`;
const StyledTitle = styled.h1`
font-size: 2rem;
font-weight: 600;
`;
const StyledFooter = styled.footer`
margin-top: 2rem;
padding-top: 1rem;
border-top: 1px solid #e5e7eb;
color: #6b7280;
`;
export const UserProfile = ({ userId, onUpdate }) => {
// Local state
const [isEditing, setIsEditing] = useState(false);
const inputRef = useRef(null);
// Data hooks
const { data: user, isLoading, error } = useQuery(
['user', userId],
() => api.getUser(userId)
);
const { mutate: updateUser } = useMutation(api.updateUser);
// Effects
useEffect(() => {
if (isEditing && inputRef.current) {
inputRef.current.focus();
}
}, [isEditing]);
// Post-processing
const displayName = user
? `${user.firstName} ${user.lastName}`
: '';
const formattedDate = user?.createdAt
? format(user.createdAt, 'PPP')
: '';
// Handlers
const handleEdit = () => setIsEditing(true);
const handleCancel = () => setIsEditing(false);
const handleSave = (updates) => {
updateUser(updates, {
onSuccess: (updatedUser) => {
setIsEditing(false);
onUpdate?.(updatedUser);
},
});
};
// Conditional renders
if (isLoading) return ;
if (error) return ;
if (!user) return ;
// Default render
return (
{displayName}
{isEditing ? (
) : (
)}
Member since {formattedDate}
);
};
```
## Quick Reference
| Section | What Goes Here | Why |
|---------|----------------|-----|
| 1. Imports | React, libraries, internal, local | Easy to find dependencies |
| 2. Styled Components | `Styled*` prefixed styling | Visual separation from logic |
| 3. Type Definitions | `*Props`, `*Return` types | Component API visibility |
| 4. Component Function | `export const Component =` | Named exports for refactoring |
| 5. Logic Flow | State → Hooks → Effects → Handlers | Respects hook rules, logical order |
| 6. Conditional Rendering | Early returns for edge cases | Reduces nesting |
| 7. Default Render | Success state JSX | Most important case most visible |
## Best Practices Summary
**DO:**
- ✅ Group imports by source with blank lines
- ✅ Prefix styled components with `Styled`
- ✅ Declare types above component (not inline)
- ✅ Use const + arrow functions for components
- ✅ Follow logic order: state → hooks → effects → handlers
- ✅ Exit early for loading/error states
- ✅ Extract complex logic to custom hooks
- ✅ Use named exports
**DON'T:**
- ❌ Mix import sources randomly
- ❌ Use generic names for styled components
- ❌ Inline complex types in parameters
- ❌ Use default exports
- ❌ Put effects before state they depend on
- ❌ Nest conditional renders in JSX
- ❌ Let component logic exceed 100 lines
- ❌ Forget to move styles to separate file when large
## Troubleshooting
### Problem: Component is getting too long (> 200 lines)
**Cause:** Too much logic in one file
**Solution:**
1. Extract data fetching to custom hook (`useUserProfile`)
2. Move styled components to `ComponentName.styled.ts`
3. Split into smaller sub-components
4. Extract complex calculations to utility functions
**Example:**
```tsx
// Before: 250 line component
export const UserProfile = () => {
// 100 lines of state and logic
// 50 lines of styled components
// 100 lines of JSX
};
// After: 80 line component
import { useUserProfile } from './useUserProfile';
import * as S from './UserProfile.styled';
export const UserProfile = () => {
const { user, handlers } = useUserProfile();
// 30 lines of presentation logic
// 50 lines of JSX
};
```
### Problem: Can't decide if something should be a styled component or a sub-component
**Cause:** Unclear separation between styling and logic
**Solution:**
- **Styled component** if it only adds styling (no props, no logic)
- **Sub-component** if it has its own props, state, or logic
```tsx
// Styled component - just styling
const StyledCard = styled.div`
padding: 1rem;
border: 1px solid #ccc;
`;
// Sub-component - has logic and props
type CardProps = {
title: string;
onClose: () => void;
children: React.ReactNode;
};
const Card = ({ title, onClose, children }: CardProps) => (
{title}
{children}
);
```
### Problem: Import organization feels arbitrary
**Cause:** No clear grouping strategy
**Solution:**
Use this checklist:
1. Is it from `react` or `react-*`? → Group 1 (React imports)
2. Is it from `node_modules`? → Group 2 (Third-party)
3. Is it using path alias (`@/`)? → Group 3 (Internal)
4. Is it in same directory (`./`)? → Group 4 (Local)
Add blank line between groups.
### Problem: TypeScript types getting complex
**Cause:** Component does too much
**Solution:**
1. Split component into smaller pieces
2. Extract shared types to `types.ts`
3. Use utility types (`Pick`, `Omit`, `Partial`)
```tsx
// Shared types
// types.ts
export type User = {
id: string;
firstName: string;
lastName: string;
email: string;
createdAt: Date;
};
// Component uses subset
// UserProfile.tsx
type UserProfileProps = {
user: Pick;
onEdit: () => void;
};
```
### Problem: Not sure where to put a helper function
**Cause:** Helper could go in component or outside
**Solution:**
- **Inside component** if it uses props, state, or hooks
- **Outside component** if it's pure and reusable
- **Separate file** if used by multiple components
```tsx
// Pure helper - outside component
const formatFullName = (firstName: string, lastName: string): string => {
return `${firstName} ${lastName}`;
};
export const UserProfile = ({ user }: UserProfileProps) => {
// Uses props - inside component as handler
const handleSave = () => {
api.updateUser(user.id, { /* ... */ });
};
const displayName = formatFullName(user.firstName, user.lastName);
// ...
};
```
### Problem: Conditional rendering getting messy
**Cause:** Too many states to handle
**Solution:**
Create a state enum and use switch/early returns:
```tsx
type LoadingState = 'idle' | 'loading' | 'error' | 'success';
export const UserProfile = ({ userId }: UserProfileProps) => {
const [state, setState] = useState('idle');
const [user, setUser] = useState(null);
const [error, setError] = useState(null);
// Early returns based on state
if (state === 'loading') return ;
if (state === 'error' && error) return ;
if (state === 'idle') return ;
if (state === 'success' && !user) return ;
// Type narrowing guarantees user exists here
return ;
};
```
### Problem: Component causing infinite re-render loop
**Cause:** Using `useEffect` to call parent `onChange` callback
**Solution:**
Call `onChange` directly when setting state, not in `useEffect`:
```tsx
// ❌ Bad
useEffect(() => {
onChange(value);
}, [value, onChange]); // Can cause infinite loop
// ✅ Good
const handleChange = (newValue: string) => {
setValue(newValue);
onChange(newValue);
};
```
See "React Hooks: Antipatterns and Gotchas" section for more details.
### Problem: State not updating when prop changes
**Cause:** `useState` only uses initial value on first render
**Solution:**
Either sync with `useEffect`, use component `key` prop, or don't duplicate state:
```tsx
// Option 1: Sync with useEffect
useEffect(() => {
setValue(propValue);
}, [propValue]);
// Option 2: Reset component with key (preferred)
// Option 3: Don't duplicate state if not needed
const displayValue = localValue ?? propValue;
```
### Problem: useEffect running with stale data
**Cause:** Missing dependencies from dependency array
**Solution:**
Include all dependencies. If that causes issues, refactor the code:
```tsx
// ❌ Bad
useEffect(() => {
doSomething(value);
}, []); // Missing 'value'
// ✅ Good
useEffect(() => {
doSomething(value);
}, [value]);
// ✅ Better: Memoize if needed
const doSomethingMemoized = useCallback(() => {
doSomething(value);
}, [value]);
```
## Integration
**This pattern works with:**
- **styled-components** - Original inspiration for styled component pattern
- **twin.macro** - Combines styled-components with Tailwind CSS
- **emotion** - Alternative CSS-in-JS library
- **React Query / TanStack Query** - Data fetching hooks
- **SWR** - Alternative data fetching
- **Zustand / Redux** - Global state management
**Pairs well with:**
- **ESLint** - Enforce import ordering with `eslint-plugin-import`
- **Prettier** - Auto-format code structure
- **TypeScript** - Type safety for props and state
- **Storybook** - Component documentation
**Use in combination with:**
- Component testing patterns (Vitest, Jest)
- Code review checklists
- Team style guides
## Variations and Flexibility
**Remember:** This is a pattern, not a law. Adapt as needed:
- **Small components** (< 50 lines) can skip some structure
- **Simple components** without state can skip logic sections
- **Presentational components** may not need data hooks
- **Different styling solutions** (CSS Modules, Tailwind) can replace styled-components section
**Core principle remains:** Predictable organization helps teams maintain code.
## References
**Based on:**
- [The Anatomy of My Ideal React Component](https://antjanus.com/digital-garden/the-anatomy-of-my-ideal-react-component) - Component structure by Antonin Januska
- [Common React Hooks Antipatterns and Gotchas](https://antjanus.com/digital-garden/common-react-hooks-antipatterns-and-gotchas) - Hooks best practices by Antonin Januska
**Official Documentation:**
- [React Docs: Function Components](https://react.dev/reference/react/Component)
- [React Hooks Rules](https://react.dev/reference/rules/rules-of-hooks)
- [styled-components Documentation](https://styled-components.com/)
- [TypeScript React Cheatsheet](https://react-typescript-cheatsheet.netlify.app/)
**Related Patterns:**
- [Component Folder Structure](https://react.dev/learn/thinking-in-react)
- [Custom Hooks Guide](https://react.dev/learn/reusing-logic-with-custom-hooks)