JSX and Component Patterns
JSX and Component Patterns
JSX Fundamentals
function App() {
const user = { name: 'Alice', role: 'admin' };
const items = ['React', 'TypeScript', 'Node.js'];
return (
<div className="app">
<h1>Welcome, {user.name}</h1>
<p>Role: {user.role === 'admin' ? 'Administrator' : 'User'}</p>
<ul>
{items.map(item => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
}
Component Composition
// Props with defaults
function Card({ title, children, variant = 'default', className = '' }) {
return (
<div className={`card card-${variant} ${className}`}>
{title && <div className="card-header"><h3>{title}</h3></div>}
<div className="card-body">{children}</div>
</div>
);
}
// Layout component
function Layout({ children, sidebar }) {
return (
<div className="layout">
<header className="layout-header">Logo</header>
<div className="layout-content">
{sidebar && <aside className="layout-sidebar">{sidebar}</aside>}
<main className="layout-main">{children}</main>
</div>
<footer className="layout-footer">Footer</footer>
</div>
);
}
// Usage
<Layout sidebar={<Navigation />}>
<Card title="Dashboard">
<p>Welcome back!</p>
</Card>
</Layout>
Render Props
function MouseTracker({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
return (
<div onMouseMove={handleMouseMove} className="tracker">
{render(position)}
</div>
);
}
// Usage
<MouseTracker render={({ x, y }) => (
<p>Mouse is at ({x}, {y})</p>
)} />
Compound Components
const TabsContext = createContext();
function Tabs({ children, defaultTab }) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
function TabList({ children }) {
return <div className="tab-list" role="tablist">{children}</div>;
}
function Tab({ value, children }) {
const { activeTab, setActiveTab } = useContext(TabsContext);
return (
<button
role="tab"
aria-selected={activeTab === value}
className={activeTab === value ? 'active' : ''}
onClick={() => setActiveTab(value)}
>
{children}
</button>
);
}
function TabPanel({ value, children }) {
const { activeTab } = useContext(TabsContext);
return activeTab === value ? <div role="tabpanel">{children}</div> : null;
}
// Usage — clean API
<Tabs defaultTab="overview">
<TabList>
<Tab value="overview">Overview</Tab>
<Tab value="details">Details</Tab>
</TabList>
<TabPanel value="overview"><OverviewContent /></TabPanel>
<TabPanel value="details"><DetailsContent /></TabPanel>
</Tabs>
Forms and User Input
Forms and User Input
Controlled Components
function LoginForm() {
const [formData, setFormData] = useState({ email: '', password: '' });
const [errors, setErrors] = useState({});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
// Clear error when user types
if (errors[name]) setErrors(prev => ({ ...prev, [name]: '' }));
};
const validate = () => {
const newErrors = {};
if (!formData.email) newErrors.email = 'Email required';
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) newErrors.email = 'Invalid email';
if (!formData.password) newErrors.password = 'Password required';
else if (formData.password.length < 8) newErrors.password = 'Min 8 characters';
return newErrors;
};
const handleSubmit = async (e) => {
e.preventDefault();
const validationErrors = validate();
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}
try {
const res = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
const data = await res.json();
if (data.token) navigate('/dashboard');
} catch (err) {
setErrors({ submit: 'Login failed' });
}
};
return (
<form onSubmit={handleSubmit} noValidate>
<div className="field">
<label htmlFor="email">Email</label>
<input
type="email" id="email" name="email"
value={formData.email} onChange={handleChange}
aria-invalid={!!errors.email} aria-describedby={errors.email ? 'email-error' : undefined}
/>
{errors.email && <span id="email-error" className="error">{errors.email}</span>}
</div>
<div className="field">
<label htmlFor="password">Password</label>
<input
type="password" id="password" name="password"
value={formData.password} onChange={handleChange}
/>
{errors.password && <span className="error">{errors.password}</span>}
</div>
{errors.submit && <p className="error">{errors.submit}</p>}
<button type="submit">Sign In</button>
</form>
);
}
Custom Form Hook
function useForm(initialValues, validateFn) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setValues(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value,
}));
};
const handleSubmit = async (onSubmit) => {
const validationErrors = validateFn(values);
setErrors(validationErrors);
if (Object.keys(validationErrors).length === 0) {
await onSubmit(values);
}
};
return { values, errors, handleChange, handleSubmit };
}
// Usage
const { values, errors, handleChange, handleSubmit } = useForm(
{ email: '', password: '' },
(values) => {
const errors = {};
if (!values.email) errors.email = 'Required';
return errors;
}
);
Component Lifecycle and Side Effects
Component Lifecycle and Side Effects
useEffect Patterns
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
// Data fetching
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
if (!cancelled) setUser(data);
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => { cancelled = true; }; // cleanup
}, [userId]); // re-run when userId changes
// Document title
useEffect(() => {
document.title = user ? `${user.name} - Profile` : 'Loading...';
}, [user]);
// Event listeners
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
if (loading) return <Spinner />;
return <div>{user.name}</div>;
}
Custom Hooks
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// Usage
function SearchPage() {
const [query, setQuery] = useLocalStorage('search', '');
const debouncedQuery = useDebounce(query, 300);
// debouncedQuery updates 300ms after user stops typing
}
useReducer for Complex State
const todoReducer = (state, action) => {
switch (action.type) {
case 'ADD':
return [...state, { id: crypto.randomUUID(), text: action.text, done: false }];
case 'TOGGLE':
return state.map(t => t.id === action.id ? { ...t, done: !t.done } : t);
case 'DELETE':
return state.filter(t => t.id !== action.id);
case 'REORDER':
const items = [...state];
const [moved] = items.splice(action.from, 1);
items.splice(action.to, 0, moved);
return items;
default:
return state;
}
};
function TodoList() {
const [todos, dispatch] = useReducer(todoReducer, []);
return (
<div>
<AddTodo onAdd={(text) => dispatch({ type: 'ADD', text })} />
<ul>
{todos.map((todo, i) => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={() => dispatch({ type: 'TOGGLE', id: todo.id })}
onDelete={() => dispatch({ type: 'DELETE', id: todo.id })}
/>
))}
</ul>
</div>
);
}
Performance Optimization
Performance Optimization
React.memo and useMemo
// Prevent re-renders when props haven't changed
const ExpensiveList = React.memo(function ExpensiveList({ items }) {
console.log('ExpensiveList rendered');
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name} - {item.price.toFixed(2)}</li>
))}
</ul>
);
});
function ProductPage({ products }) {
// Memoize expensive computations
const sortedProducts = useMemo(() => {
return [...products].sort((a, b) => a.price - b.price);
}, [products]);
const totalValue = useMemo(() => {
return products.reduce((sum, p) => sum + p.price * p.quantity, 0);
}, [products]);
return (
<div>
<p>Total: ${totalValue.toFixed(2)}</p>
<ExpensiveList items={sortedProducts} />
</div>
);
}
useCallback for Stable References
function TodoApp() {
const [todos, setTodos] = useState([]);
// Without useCallback, new function reference on every render
// causes child components to re-render
const addTodo = useCallback((text) => {
setTodos(prev => [...prev, { id: Date.now(), text, done: false }]);
}, []);
const toggleTodo = useCallback((id) => {
setTodos(prev => prev.map(t => t.id === id ? { ...t, done: !t.done } : t));
}, []);
return (
<div>
<AddTodoForm onAdd={addTodo} />
<TodoList todos={todos} onToggle={toggleTodo} />
</div>
);
}
Virtualization for Long Lists
import { FixedSizeList } from 'react-window';
function VirtualizedList({ items }) {
const Row = ({ index, style }) => (
<div style={style} className="list-row">
{items[index].name}
</div>
);
return (
<FixedSizeList
height={600}
width="100%"
itemCount={items.length}
itemSize={50}
>
{Row}
</FixedSizeList>
);
}
Key Takeaways
- Use
React.memofor pure presentational components that receive the same props - Use
useMemofor expensive calculations - Use
useCallbackfor functions passed as props to memoized children - Use virtualization for lists with hundreds or thousands of items
- Profile with React DevTools before optimizing — measure first