Table of Contents
- Advanced State Management: Beyond
useState - Performance Optimization: Memoization & Re-render Control
- Code Splitting & Lazy Loading with
React.lazyandSuspense - Custom Hooks: Reusability & Abstraction
- Advanced Component Patterns: Compound Components & Render Props
- Error Boundaries: Graceful Error Handling
- Testing Advanced React Components
- TypeScript Integration for Type Safety
- Data Fetching with React Query
- Advanced Styling: Styled Components & Theming
- Conclusion
- References
1. Advanced State Management: Beyond useState
For simple state, useState works, but complex state (e.g., nested objects, state with multiple sub-values, or state requiring complex logic) demands more robust solutions.
Context API + useReducer
The Context API combined with useReducer is ideal for sharing state across components without prop-drilling. It mimics a Redux-like flow with actions and reducers.
Example: Todo App with Context + useReducer
// TodoContext.js
import { createContext, useReducer, useContext } from 'react';
// Initial state
const initialState = { todos: [], isLoading: false };
// Reducer
function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return { ...state, todos: [...state.todos, action.payload] };
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo
),
};
default:
return state;
}
}
// Create context
const TodoContext = createContext();
// Provider component
export function TodoProvider({ children }) {
const [state, dispatch] = useReducer(todoReducer, initialState);
return (
<TodoContext.Provider value={{ state, dispatch }}>
{children}
</TodoContext.Provider>
);
}
// Custom hook to use the context
export function useTodoContext() {
return useContext(TodoContext);
}
Usage in a Component:
// TodoList.js
import { useTodoContext } from './TodoContext';
function TodoList() {
const { state, dispatch } = useTodoContext();
return (
<div>
{state.todos.map(todo => (
<div key={todo.id} onClick={() => dispatch({ type: 'TOGGLE_TODO', payload: todo.id })}>
{todo.text} {todo.completed ? '✓' : ''}
</div>
))}
</div>
);
}
When to Use Redux Toolkit
For large apps with complex state interactions (e.g., cross-slice updates, middleware like Redux Thunk), use Redux Toolkit (RTK). RTK simplifies Redux with createSlice, configureStore, and built-in middleware.
2. Performance Optimization: Memoization & Re-render Control
React re-renders components by default when their props or state change. Over-rendering can hurt performance—memoization helps optimize this.
React.memo
Wraps a functional component to prevent re-renders if props haven’t changed (shallow comparison).
Example: Memoizing a Component
// Without memo: Re-renders even if props are the same
function UserProfile({ user }) {
console.log('UserProfile re-rendered');
return <div>{user.name}</div>;
}
// With memo: Only re-renders if `user` prop changes
const MemoizedUserProfile = React.memo(UserProfile);
useMemo
Memoizes the result of an expensive calculation to avoid re-computing on every render.
Example: Expensive Calculation
function ExpensiveComponent({ numbers }) {
// Without useMemo: Re-runs sort on every render
const sortedNumbers = numbers.sort((a, b) => a - b);
// With useMemo: Only re-runs when `numbers` changes
const sortedNumbers = useMemo(() => numbers.sort((a, b) => a - b), [numbers]);
return <div>{sortedNumbers.join(', ')}</div>;
}
useCallback
Memoizes a function to prevent it from being recreated on every render (critical when passing functions as props to React.memo components).
Example: Memoizing a Callback
function Parent() {
const [count, setCount] = useState(0);
// Without useCallback: New function on every render → MemoizedChild re-renders
const handleClick = () => console.log('Button clicked');
// With useCallback: Same function instance → MemoizedChild doesn’t re-render
const handleClick = useCallback(() => console.log('Button clicked'), []); // Empty deps: stable forever
return <MemoizedChild onClick={handleClick} />;
}
3. Code Splitting & Lazy Loading with React.lazy and Suspense
Code splitting breaks your app into smaller bundles loaded on demand, reducing initial load time.
React.lazy + Suspense
React.lazy dynamically imports a component, and Suspense provides a fallback UI (e.g., loading spinner) while the component loads.
Example: Route-Based Code Splitting
With React Router:
import { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
// Dynamically import components
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About')); // Loaded only when /about is visited
function App() {
return (
<Router>
<Suspense fallback={<div>Loading...</div>}> {/* Fallback while loading */}
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
</Router>
);
}
4. Custom Hooks: Reusability & Abstraction
Custom hooks extract component logic into reusable functions. They start with use (e.g., useLocalStorage).
Example 1: useLocalStorage
Syncs state with localStorage:
function useLocalStorage(key, initialValue) {
// Get from localStorage or use initial value
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
// Update localStorage when value changes
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// Usage
function Profile() {
const [name, setName] = useLocalStorage('name', 'John');
return <input value={name} onChange={(e) => setName(e.target.value)} />;
}
Example 2: useFetch
Handles API requests with loading/error states:
function useFetch(url) {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch(url);
const json = await res.json();
setData(json);
} catch (err) {
setError(err);
} finally {
setIsLoading(false);
}
};
fetchData();
}, [url]);
return { data, isLoading, error };
}
5. Advanced Component Patterns: Compound Components & Render Props
Compound Components
Enables components to share state and communicate (e.g., <select> and <option>).
Example: Tabs Component
import { createContext, useContext, useState } from 'react';
const TabsContext = createContext();
export function Tabs({ children }) {
const [activeTab, setActiveTab] = useState(0);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div>{children}</div>
</TabsContext.Provider>
);
}
export function TabList({ children }) {
return <div className="tab-list">{children}</div>;
}
export function Tab({ index, children }) {
const { activeTab, setActiveTab } = useContext(TabsContext);
return (
<button
onClick={() => setActiveTab(index)}
className={activeTab === index ? 'active' : ''}
>
{children}
</button>
);
}
export function TabPanel({ index, children }) {
const { activeTab } = useContext(TabsContext);
if (activeTab !== index) return null;
return <div className="tab-panel">{children}</div>;
}
// Usage
function App() {
return (
<Tabs>
<TabList>
<Tab index={0}>Tab 1</Tab>
<Tab index={1}>Tab 2</Tab>
</TabList>
<TabPanel index={0}>Content 1</TabPanel>
<TabPanel index={1}>Content 2</TabPanel>
</Tabs>
);
}
Render Props
A component prop that returns JSX to share logic (largely replaced by custom hooks, but still useful for cross-cutting concerns).
Example: Render Prop for Window Size
function WindowSize({ render }) {
const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight });
useEffect(() => {
const handleResize = () => setSize({ width: window.innerWidth, height: window.innerHeight });
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return render(size); // Pass state to render prop
}
// Usage
function App() {
return (
<WindowSize render={(size) => (
<div>Window: {size.width}x{size.height}</div>
)} />
);
}
6. Error Boundaries: Graceful Error Handling
Error boundaries catch JavaScript errors in child components, log them, and display a fallback UI (prevents app crashes).
Example: Creating an Error Boundary
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error }; // Update state to trigger fallback
}
componentDidCatch(error, info) {
logErrorToService(error, info); // Log to monitoring service (e.g., Sentry)
}
render() {
if (this.state.hasError) {
return this.props.fallback || <h1>Something went wrong.</h1>; // Fallback UI
}
return this.props.children; // Render children if no error
}
}
// Usage
function App() {
return (
<ErrorBoundary fallback={<div>Oops! Component failed to load.</div>}>
<RiskyComponent /> {/* Component that might throw */}
</ErrorBoundary>
);
}
7. Testing Advanced React Components
Testing ensures components work as expected. Use React Testing Library (RTL) for component testing and @testing-library/react-hooks for hooks.
Testing Custom Hooks
Use renderHook to test hooks in isolation:
import { renderHook, act } from '@testing-library/react-hooks';
import { useCounter } from './useCounter';
test('useCounter increments', () => {
const { result } = renderHook(() => useCounter(0));
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
Testing Async Components
Use waitFor to handle async operations:
test('fetches data and displays it', async () => {
render(<UserProfile userId={1} />);
const userData = await screen.findByText('John Doe'); // Waits for text to appear
expect(userData).toBeInTheDocument();
});
8. TypeScript Integration for Type Safety
TypeScript adds static typing to React, catching errors early. Define interfaces for props, type hooks, and use generics for reusability.
Example: Typed Props & Custom Hooks
// Typed props
interface UserCardProps {
user: { id: number; name: string };
onDelete: (id: number) => void;
}
const UserCard: React.FC<UserCardProps> = ({ user, onDelete }) => {
return (
<div>
{user.name} <button onClick={() => onDelete(user.id)}>Delete</button>
</div>
);
};
// Generic custom hook
function useArray<T>(initialValue: T[] = []) {
const [array, setArray] = useState<T[]>(initialValue);
const push = (item: T) => setArray([...array, item]);
return { array, push };
}
// Usage: Type-safe array operations
const { array, push } = useArray<string>(['apple']);
push('banana'); // ✅ Valid
push(123); // ❌ Error: Argument of type 'number' is not assignable to 'string'
9. Data Fetching with React Query
React Query (now TanStack Query) simplifies data fetching, caching, and state management. It replaces manual useEffect + useState for API calls.
Example: Fetching Data with useQuery
import { useQuery } from '@tanstack/react-query';
function UserList() {
const { data, isLoading, error } = useQuery({
queryKey: ['users'], // Unique key for caching
queryFn: () => fetch('https://api.example.com/users').then(res => res.json()),
});
if (isLoading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Mutation with useMutation
For POST/PUT/DELETE requests:
import { useMutation, useQueryClient } from '@tanstack/react-query';
function AddUser() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newUser) => fetch('/users', { method: 'POST', body: JSON.stringify(newUser) }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] }); // Refetch users list
},
});
return <button onClick={() => mutation.mutate({ name: 'New User' })}>Add User</button>;
}
10. Advanced Styling: Styled Components & Theming
Styling in React can be enhanced with libraries like Styled Components (CSS-in-JS) or CSS Modules (scoped CSS).
Styled Components with Theming
Create reusable, themed components:
import styled, { ThemeProvider } from 'styled-components';
// Define theme
const theme = {
colors: { primary: 'blue', secondary: 'gray' },
spacing: { small: '8px', large: '16px' },
};
// Styled component using theme
const StyledButton = styled.button`
background: ${props => props.theme.colors.primary};
padding: ${props => props.theme.spacing.small};
color: white;
border: none;
`;
// Usage with ThemeProvider
function App() {
return (
<ThemeProvider theme={theme}>
<StyledButton>Click me</StyledButton>
</ThemeProvider>
);
}
Conclusion
Mastering advanced React techniques—from state management and performance optimization to custom hooks and testing—empowers you to build scalable, maintainable apps. Start small: refactor a simple component with React.memo, create a custom hook, or add TypeScript to a project. Practice and experimentation are key!