Welcome back! In Part 1, we covered React's core fundamentals—components, props, state, and the basics of hooks. Now it's time to level up!
In Part 2, we'll explore advanced patterns that help you build production-ready React applications. These concepts might seem complex at first, but they solve real problems you'll encounter as your apps grow.
What you'll learn:
- How to work with external systems using effects
- When and how to access the DOM directly
- Solving prop drilling with Context
- Advanced rendering patterns with Portals
- Handling loading and error states gracefully
Let's dive in!
1. Effects: Stepping Outside React
What are effects?
Effects (or side effects) are operations that interact with systems outside of React—like browser APIs, HTTP requests, timers, or third-party libraries.
Examples of side effects:
- Fetching data from an API
- Setting up subscriptions (WebSockets, event listeners)
- Manually changing the DOM
- Logging to analytics
- Setting timers (setTimeout, setInterval)
Where to Run Effects
Option 1: Event Handlers (Preferred)
Whenever possible, run side effects in event handlers:
function SearchButton() {
const handleClick = () => {
// Side effect in response to user action
fetch('/api/search')
.then(res => res.json())
.then(data => console.log(data));
};
return <button onClick={handleClick}>Search</button>;
}
Why prefer event handlers?
They run only when the user takes action, making your app more predictable and easier to debug.
Option 2: The useEffect Hook
When you need to run effects outside of user interactions (like loading data when a component appears), use the useEffect hook:
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// This runs after the component renders
setLoading(true);
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, [userId]); // Re-run when userId changes
if (loading) return <div>Loading...</div>;
return <div>{user?.name}</div>;
}
How useEffect works:
useEffect(() => {
// Effect code runs after render
return () => {
// Cleanup code (optional)
// Runs before the component unmounts
// Or before the effect runs again
};
}, [dependencies]); // When to re-run the effect
Understanding Dependencies
The dependency array controls when your effect runs:
Empty array [] - Run once when component mounts:
useEffect(() => {
console.log('Component mounted!');
}, []); // Only runs once
With dependencies [value] - Run when dependencies change:
useEffect(() => {
console.log('Count changed:', count);
}, [count]); // Runs when count changes
No array - Run after every render (usually not what you want):
useEffect(() => {
console.log('Every render!'); // Be careful with this!
});
Cleanup Functions
Some effects need cleanup to prevent memory leaks:
useEffect(() => {
// Set up subscription
const subscription = subscribeToChat(roomId);
// Cleanup function
return () => {
subscription.unsubscribe();
};
}, [roomId]);
Common cleanup scenarios:
- Canceling network requests
- Removing event listeners
- Clearing timers
- Unsubscribing from services
Complete example with cleanup:
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// Cleanup: clear interval when component unmounts
return () => clearInterval(interval);
}, []); // Empty array = run once on mount
return <div>Seconds: {seconds}</div>;
}
2. Refs: Accessing the Real DOM
What are refs?
Refs let you reference actual DOM elements or store mutable values that persist between renders without causing re-renders.
Using useRef for DOM Access
Basic example:
import { useRef } from 'react';
function TextInput() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus(); // Access the real DOM element
};
return (
<>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus Input</button>
</>
);
}
How it works:
-
useRef(null)creates a ref object -
ref={inputRef}attaches the ref to the DOM element -
inputRef.currentcontains the actual DOM element
Common Use Cases for Refs
1. Managing focus:
function SearchForm() {
const searchRef = useRef(null);
useEffect(() => {
// Focus search input when component mounts
searchRef.current?.focus();
}, []);
return <input ref={searchRef} type="search" />;
}
2. Measuring elements:
function MeasureComponent() {
const divRef = useRef(null);
const getSize = () => {
const width = divRef.current.offsetWidth;
const height = divRef.current.offsetHeight;
console.log('Size:', width, height);
};
return (
<>
<div ref={divRef}>Measure me!</div>
<button onClick={getSize}>Get Size</button>
</>
);
}
3. Integrating third-party libraries:
function VideoPlayer({ src }) {
const videoRef = useRef(null);
useEffect(() => {
// Use video.js or another library
const player = videojs(videoRef.current);
return () => player.dispose();
}, []);
return <video ref={videoRef} src={src} />;
}
Using Refs to Store Values
Refs can also store mutable values that don't cause re-renders:
function Stopwatch() {
const [time, setTime] = useState(0);
const intervalRef = useRef(null);
const start = () => {
intervalRef.current = setInterval(() => {
setTime(t => t + 1);
}, 1000);
};
const stop = () => {
clearInterval(intervalRef.current);
};
return (
<div>
<p>Time: {time}s</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}
Refs vs State:
- State: Triggers re-renders when updated
- Refs: Don't trigger re-renders, persist between renders
When NOT to Use Refs
❌ Don't use refs for things React can handle normally:
- Updating text content (use state instead)
- Showing/hiding elements (use conditional rendering)
- Handling events (use event handlers)
✅ Use refs when you need to:
- Focus inputs
- Measure DOM elements
- Integrate with non-React libraries
- Store mutable values without re-rendering
3. Context: Avoiding Prop Drilling
The problem: Prop Drilling
In large apps, passing props through many nested components gets tedious:
function App() {
const user = { name: 'John', theme: 'dark' };
return <Layout user={user} theme={user.theme} />;
}
function Layout({ user, theme }) {
return <Header user={user} theme={theme} />;
}
function Header({ user, theme }) {
return <Navigation user={user} theme={theme} />;
}
function Navigation({ user, theme }) {
return <UserMenu user={user} theme={theme} />;
}
function UserMenu({ user, theme }) {
// Finally used here!
return <div className={theme}>{user.name}</div>;
}
See how user and theme pass through components that don't even use them? This is prop drilling.
The Solution: Context API
Context lets you share data across your component tree without passing props manually at every level.
Step 1: Create Context
import { createContext } from 'react';
const ThemeContext = createContext('light'); // Default value
Step 2: Provide the Context
function App() {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext.Provider value={theme}>
<Layout />
</ThemeContext.Provider>
);
}
Step 3: Consume the Context
import { useContext } from 'react';
function UserMenu() {
const theme = useContext(ThemeContext);
return <div className={theme}>Menu</div>;
}
Complete Context Example
// 1. Create context with default value
const UserContext = createContext(null);
// 2. Create provider component (optional but recommended)
function UserProvider({ children }) {
const [user, setUser] = useState(null);
const login = (userData) => setUser(userData);
const logout = () => setUser(null);
return (
<UserContext.Provider value={{ user, login, logout }}>
{children}
</UserContext.Provider>
);
}
// 3. Custom hook for easier usage (optional)
function useUser() {
const context = useContext(UserContext);
if (!context) {
throw new Error('useUser must be used within UserProvider');
}
return context;
}
// 4. Use in your app
function App() {
return (
<UserProvider>
<Header />
<MainContent />
</UserProvider>
);
}
function Header() {
const { user, logout } = useUser();
return (
<header>
{user ? (
<>
<span>Welcome, {user.name}!</span>
<button onClick={logout}>Logout</button>
</>
) : (
<span>Please log in</span>
)}
</header>
);
}
When to Use Context
Good use cases:
- Theme/dark mode
- User authentication data
- Language/localization
- Global app settings
When NOT to use Context:
- Don't use it just to avoid passing props one level down
- For frequently changing values, consider state management libraries
- Context re-renders all consumers when value changes
4. Portals: Breaking Out of the Component Tree
What are portals?
Portals let you render a component into a different part of the DOM tree, outside its parent component's DOM hierarchy.
Why use them?
Some components (modals, tooltips, dropdowns) need to "escape" their parent's CSS constraints (like overflow: hidden or z-index).
Creating a Portal
import { createPortal } from 'react-dom';
function Modal({ isOpen, children }) {
if (!isOpen) return null;
return createPortal(
<div className="modal-overlay">
<div className="modal-content">
{children}
</div>
</div>,
document.getElementById('modal-root') // Target element
);
}
HTML setup:
<body>
<div id="root"></div>
<div id="modal-root"></div> <!-- Portal target -->
</body>
Complete Modal Example
function App() {
const [showModal, setShowModal] = useState(false);
return (
<div style={{ overflow: 'hidden', position: 'relative' }}>
<h1>My App</h1>
<button onClick={() => setShowModal(true)}>
Open Modal
</button>
<Modal isOpen={showModal} onClose={() => setShowModal(false)}>
<h2>Modal Title</h2>
<p>Modal content goes here</p>
<button onClick={() => setShowModal(false)}>Close</button>
</Modal>
</div>
);
}
function Modal({ isOpen, onClose, children }) {
if (!isOpen) return null;
return createPortal(
<div className="modal-backdrop" onClick={onClose}>
<div
className="modal-content"
onClick={(e) => e.stopPropagation()}
>
{children}
</div>
</div>,
document.body // Render to body instead of modal-root
);
}
Common Portal Use Cases
- Modals/Dialogs
- Tooltips
- Dropdown menus
- Notifications/Toast messages
- Floating UI elements
Key benefit: Even though the modal renders outside the component tree in the DOM, it still behaves like a normal React component (receives props, can use context, etc.).
5. Suspense: Handling Loading States
What is Suspense?
Suspense is a component that helps you handle loading states elegantly while components or data are loading.
Basic Suspense Usage
import { Suspense, lazy } from 'react';
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<UserProfile />
</Suspense>
);
}
How it works:
- While
UserProfileis loading, Suspense shows thefallbackcomponent - Once loaded, Suspense displays the actual component
- Provides a better user experience than showing nothing
Lazy Loading Components
One of the most common uses of Suspense is lazy loading—loading components only when they're needed:
import { lazy, Suspense } from 'react';
// Instead of: import HeavyComponent from './HeavyComponent'
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
const [show, setShow] = useState(false);
return (
<div>
<button onClick={() => setShow(true)}>
Load Heavy Component
</button>
{show && (
<Suspense fallback={<div>Loading component...</div>}>
<HeavyComponent />
</Suspense>
)}
</div>
);
}
Benefits of lazy loading:
- Smaller initial bundle size
- Faster initial page load
- Load features only when users need them
Multiple Suspense Boundaries
You can have multiple Suspense boundaries for fine-grained loading states:
function Dashboard() {
return (
<div>
<Suspense fallback={<div>Loading header...</div>}>
<Header />
</Suspense>
<Suspense fallback={<div>Loading main content...</div>}>
<MainContent />
</Suspense>
<Suspense fallback={<div>Loading sidebar...</div>}>
<Sidebar />
</Suspense>
</div>
);
}
Nested Suspense
function App() {
return (
<Suspense fallback={<PageLoader />}>
<Layout>
<Suspense fallback={<SidebarLoader />}>
<Sidebar />
</Suspense>
<Suspense fallback={<ContentLoader />}>
<Content />
</Suspense>
</Layout>
</Suspense>
);
}
Best practices:
- Use specific fallbacks that match the content being loaded
- Consider skeleton screens instead of spinners
- Place Suspense boundaries at logical component boundaries
6. Error Boundaries: Catching React Errors
The problem:
If a component throws an error during rendering, it can crash your entire app and show a blank screen!
function BrokenComponent() {
const user = null;
return <div>{user.name}</div>; // 💥 Error! Cannot read 'name' of null
}
The Solution: Error Boundaries
Error boundaries are special components that catch JavaScript errors anywhere in their child component tree and display a fallback UI.
Important: Error boundaries must currently be class components. React doesn't yet support error boundaries with hooks.
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
// Update state so next render shows fallback UI
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// Log error to error reporting service
console.error('Error caught:', error, errorInfo);
// logErrorToService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// Fallback UI
return (
<div className="error-container">
<h1>Something went wrong</h1>
<p>{this.state.error?.message}</p>
<button onClick={() => window.location.reload()}>
Reload Page
</button>
</div>
);
}
return this.props.children;
}
}
Using Error Boundaries
function App() {
return (
<ErrorBoundary>
<Header />
<ErrorBoundary>
<MainContent />
</ErrorBoundary>
<Footer />
</ErrorBoundary>
);
}
Multiple error boundaries:
function Dashboard() {
return (
<div>
{/* If Sidebar crashes, only it shows error */}
<ErrorBoundary fallback={<div>Sidebar failed to load</div>}>
<Sidebar />
</ErrorBoundary>
{/* MainContent has its own error boundary */}
<ErrorBoundary fallback={<div>Content failed to load</div>}>
<MainContent />
</ErrorBoundary>
</div>
);
}
What Error Boundaries Catch
✅ Do catch:
- Errors during rendering
- Errors in lifecycle methods
- Errors in constructors
❌ Don't catch:
- Event handler errors (use try/catch instead)
- Asynchronous code (setTimeout, promises)
- Server-side rendering errors
- Errors in the error boundary itself
Handling event handler errors:
function MyComponent() {
const handleClick = () => {
try {
// Risky code
doSomethingRisky();
} catch (error) {
console.error('Button click error:', error);
// Handle error appropriately
}
};
return <button onClick={handleClick}>Click Me</button>;
}
Reusable Error Boundary with Custom Fallback
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
// Use custom fallback if provided
return this.props.fallback || <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
// Usage with custom fallback
<ErrorBoundary fallback={<CustomErrorPage />}>
<App />
</ErrorBoundary>
Best Practices
-
Place error boundaries strategically:
- Around route components
- Around independent features
- At the top level of your app
Log errors to monitoring services:
componentDidCatch(error, errorInfo) {
logErrorToMyService(error, errorInfo);
}
-
Provide helpful fallback UIs:
- Explain what happened
- Offer a way to recover (reload, go back)
- Include contact/support information
-
Don't overuse:
- Don't wrap every single component
- Group related components under one boundary
7. Advanced Hooks: useReducer, useMemo, useCallback
useReducer: Managing Complex State
When state logic becomes complex, useReducer is often clearer than useState:
import { useReducer } from 'react';
// Reducer function
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
return [...state, action.item];
case 'REMOVE_ITEM':
return state.filter(item => item.id !== action.id);
case 'CLEAR_CART':
return [];
default:
return state;
}
}
function ShoppingCart() {
const [cart, dispatch] = useReducer(cartReducer, []);
const addItem = (item) => {
dispatch({ type: 'ADD_ITEM', item });
};
const removeItem = (id) => {
dispatch({ type: 'REMOVE_ITEM', id });
};
return (
<div>
<h2>Cart ({cart.length} items)</h2>
<button onClick={() => addItem({ id: 1, name: 'Product' })}>
Add Item
</button>
<button onClick={() => dispatch({ type: 'CLEAR_CART' })}>
Clear Cart
</button>
</div>
);
}
When to use useReducer:
- Complex state logic with multiple sub-values
- Next state depends on previous state
- Need to manage related state together
- Want to optimize performance for deep component trees
useMemo: Memoizing Expensive Calculations
useMemo caches the result of expensive calculations:
import { useMemo, useState } from 'react';
function ExpensiveComponent({ items }) {
const [filter, setFilter] = useState('');
// Only recalculate when items or filter changes
const filteredItems = useMemo(() => {
console.log('Filtering items...');
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
Without useMemo:
The filtering happens on every render, even if items and filter haven't changed.
With useMemo:
Filtering only happens when dependencies (items or filter) change.
useCallback: Memoizing Functions
useCallback caches function instances between renders:
import { useCallback, useState } from 'react';
function TodoApp() {
const [todos, setTodos] = useState([]);
// This function is recreated on every render without useCallback
const addTodo = useCallback((text) => {
setTodos(prev => [...prev, { id: Date.now(), text }]);
}, []); // Empty deps = function never changes
const removeTodo = useCallback((id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
}, []);
return (
<div>
<TodoForm onSubmit={addTodo} />
<TodoList todos={todos} onRemove={removeTodo} />
</div>
);
}
Why use useCallback?
- Prevent unnecessary re-renders of child components
- Useful when passing callbacks to optimized child components using
React.memo
Example with React.memo:
const TodoItem = React.memo(({ todo, onRemove }) => {
console.log('TodoItem rendered:', todo.text);
return (
<li>
{todo.text}
<button onClick={() => onRemove(todo.id)}>Remove</button>
</li>
);
});
Performance tip: Don't use useMemo and useCallback everywhere! Only use them when:
- You have performance issues
- Calculations are genuinely expensive
- Child components are wrapped in
React.memo
Putting It All Together: Advanced Patterns Summary
Let's see how these concepts work together in a real-world example:
// User context for app-wide user data
const UserContext = createContext(null);
// Error boundary at the app level
class AppErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return <ErrorPage />;
}
return this.props.children;
}
}
function App() {
const [user, setUser] = useState(null);
// Effect: Load user on mount
useEffect(() => {
fetchUser().then(setUser);
}, []);
return (
<AppErrorBoundary>
<UserContext.Provider value={user}>
<Suspense fallback={<PageLoader />}>
<Router />
</Suspense>
</UserContext.Provider>
</AppErrorBoundary>
);
}
function Dashboard() {
const user = useContext(UserContext);
const [showModal, setShowModal] = useState(false);
const modalRef = useRef(null);
// Effect: Focus modal when it opens
useEffect(() => {
if (showModal && modalRef.current) {
modalRef.current.focus();
}
}, [showModal]);
return (
<div>
<h1>Welcome, {user?.name}</h1>
<button onClick={() => setShowModal(true)}>
Open Settings
</button>
{/* Portal: Render modal outside component tree */}
{showModal && createPortal(
<div className="modal" ref={modalRef}>
<ErrorBoundary fallback={<div>Settings failed</div>}>
<Suspense fallback={<div>Loading settings...</div>}>
<SettingsPanel onClose={() => setShowModal(false)} />
</Suspense>
</ErrorBoundary>
</div>,
document.body
)}
</div>
);
}
This example demonstrates:
- ✅ Context for global state
- ✅ Error boundaries for resilience
- ✅ Suspense for loading states
- ✅ Portals for modals
- ✅ useEffect for side effects
- ✅ useRef for DOM access
Quick Reference: Advanced Concepts Checklist
Here's what we covered in Part 2:
✅ useEffect - Side effects and lifecycle
✅ useRef - DOM access and mutable values
✅ Context - Global state without prop drilling
✅ Portals - Rendering outside component tree
✅ Suspense - Elegant loading states
✅ Error Boundaries - Catching and handling errors
✅ useReducer - Complex state management
✅ useMemo - Memoizing expensive calculations
✅ useCallback - Memoizing functions
Practice Challenge 🎯
Build a complete feature using these advanced patterns:
User Settings Panel
Requirements:
- Use Context for theme (light/dark mode)
- Render settings modal with a Portal
- Use Suspense to lazy load settings content
- Wrap in an Error Boundary
- Use useEffect to save settings to localStorage
- Use useRef to focus first input when modal opens
- Use useCallback for event handlers passed to child components
Bonus:
- Add a custom hook
useLocalStorage - Implement undo/redo with useReducer
- Add loading states with Suspense
- Measure modal performance with useMemo
The Complete React Mental Model
After completing both Part 1 and Part 2, you now have a complete understanding of React:
Foundation (Part 1)
- Components → Building blocks
- Props → Data flow down
- State → Dynamic data
- Hooks → React features
- Events → User interactions
Advanced (Part 2)
- Effects → External systems
- Refs → DOM access
- Context → Data sharing
- Portals → Flexible rendering
- Error Boundaries → Resilience
The React Philosophy
- Declarative: Describe what you want, React figures out how
- Component-Based: Build encapsulated, reusable pieces
- Learn Once, Write Anywhere: React, React Native, React Server Components
- Unidirectional Data Flow: Props flow down, events bubble up
- Composition: Build complex UIs from simple components
Next Steps in Your React Journey
Congratulations! You've completed the foundational knowledge of React. Here's what to explore next:
Intermediate Topics
- Custom Hooks - Create your own reusable hooks
- React Router - Client-side routing
- Form Libraries - React Hook Form, Formik
- State Management - Redux, Zustand, Jotai
Advanced Topics
- Performance Optimization - React.memo, code splitting
- TypeScript with React - Type-safe components
- Testing - Jest, React Testing Library
- Server Components - React Server Components (RSC)
Frameworks
- Next.js - Full-stack React framework
- Remix - Web standards-focused framework
- Gatsby - Static site generator
Build Projects
The best way to solidify your learning:
- Todo App (classic for a reason!)
- Weather Dashboard (API integration)
- E-commerce Store (complex state)
- Real-time Chat (websockets, effects)
- Your Own Idea! (most important)
Final Thoughts
React's advanced patterns might seem complex, but they solve real problems:
- Effects connect to the outside world
- Refs give you an escape hatch when needed
- Context eliminates prop drilling
- Portals solve layout constraints
- Suspense improves user experience
- Error boundaries keep your app stable
The key is knowing when to use each pattern. Don't reach for Context when props are fine. Don't use useEffect when an event handler works. React gives you powerful tools—use them wisely!
Remember:
- 🎯 Start simple, add complexity when needed
- 📚 Read the official React docs (they're excellent!)
- 🛠️ Build projects to reinforce learning
- 💬 Join React communities for help and inspiration
- 🚀 Keep experimenting and have fun!
You've completed the series! 🎉
Check out these related posts:
Now go build something amazing! 🚀
Top comments (0)