In this comprehensive lesson, you'll master advanced React patterns that elevate your development skills to professional levels. You'll learn sophisticated component composition techniques, performance optimization strategies, and architectural patterns that scale. These patterns will help you write more maintainable, reusable, and efficient React code that follows industry best practices.
Compound components are a pattern where multiple components work together to share implicit state and behavior. This pattern creates flexible and declarative APIs that are easy to use and extend.
The Problem: Traditional component APIs often become complex with many configuration options The Solution: Compound components allow users to compose components together, sharing state implicitly
Benefits:
// Menu compound component
const MenuContext = createContext()
function Menu({ children }) {
const [isOpen, setIsOpen] = useState(false)
const toggle = () => setIsOpen(!isOpen)
return (
<MenuContext.Provider value={{ isOpen, toggle }}>
<div className="menu">{children}</div>
</MenuContext.Provider>
)
}
function MenuButton() {
const { toggle } = useContext(MenuContext)
return <button onClick={toggle}>Menu</button>
}
function MenuItems({ children }) {
const { isOpen } = useContext(MenuContext)
return isOpen ? <ul className="items">{children}</ul> : null
}
function MenuItem({ children, onClick }) {
return <li onClick={onClick}>{children}</li>
}
// Usage
function App() {
return (
<Menu>
<MenuButton />
<MenuItems>
<MenuItem onClick={() => console.log('Profile')}>Profile</MenuItem>
<MenuItem onClick={() => console.log('Settings')}>Settings</MenuItem>
</MenuItems>
</Menu>
)
}
// Tabs compound component with more flexibility
const TabsContext = createContext()
function Tabs({ children, defaultTab = 0 }) {
const [activeTab, setActiveTab] = useState(defaultTab)
const value = { activeTab, setActiveTab }
return (
<TabsContext.Provider value={value}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
)
}
function TabList({ children }) {
return <div className="tab-list">{children}</div>
}
function Tab({ children, index }) {
const { activeTab, setActiveTab } = useContext(TabsContext)
const isActive = activeTab === index
return (
<button
className={`tab ${isActive ? 'active' : ''}`}
onClick={() => setActiveTab(index)}
>
{children}
</button>
)
}
function TabPanels({ children }) {
return <div className="tab-panels">{children}</div>
}
function TabPanel({ children, index }) {
const { activeTab } = useContext(TabsContext)
if (activeTab !== index) return null
return <div className="tab-panel">{children}</div>
}
Render props is a pattern where a component receives a function as a prop and uses that function to render its content. This pattern provides maximum flexibility in how components render their data.
The Pattern: Component takes a function prop and calls it with its internal state Benefits: Code reuse, flexibility, separation of concerns Use Cases: Data fetching, mouse tracking, form logic, animation controllers
// Mouse tracker with render props
function MouseTracker({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 })
useEffect(() => {
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY })
}
window.addEventListener('mousemove', handleMouseMove)
return () => window.removeEventListener('mousemove', handleMouseMove)
}, [])
return render(position)
}
// Usage
function App() {
return (
<MouseTracker
render={({ x, y }) => (
<div>
Mouse position: {x}, {y}
</div>
)}
/>
)
}
// Alternative usage with children as function
function App() {
return (
<MouseTracker>
{({ x, y }) => (
<div>
Mouse position: {x}, {y}
</div>
)}
</MouseTracker>
)
}
// Data fetcher with render props
function DataFetcher({ url, render }) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
setLoading(true)
fetch(url)
.then(res => res.json())
.then(data => {
setData(data)
setError(null)
})
.catch(err => setError(err))
.finally(() => setLoading(false))
}, [url])
return render({ data, loading, error })
}
// Usage
function UserProfile() {
return (
<DataFetcher
url="/api/user"
render={({ data, loading, error }) => {
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
if (!data) return <div>No data</div>
return <div>Welcome, {data.name}!</div>
}}
/>
)
}
Custom hooks are JavaScript functions that start with "use" and can call other hooks. They allow you to extract component logic into reusable functions.
Benefits: Logic reuse, separation of concerns, testability, cleaner components Pattern: Extract stateful logic into reusable functions
// Local storage hook
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch (error) {
return initialValue
}
})
const setValue = (value) => {
try {
setStoredValue(value)
window.localStorage.setItem(key, JSON.stringify(value))
} catch (error) {
console.error('Error saving to localStorage:', error)
}
}
return [storedValue, setValue]
}
// Debounced value hook
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => clearTimeout(handler)
}, [value, delay])
return debouncedValue
}
// Usage
function SearchComponent() {
const [searchTerm, setSearchTerm] = useState('')
const [results, setResults] = useState([])
const debouncedSearchTerm = useDebounce(searchTerm, 500)
useEffect(() => {
if (debouncedSearchTerm) {
fetchSearchResults(debouncedSearchTerm).then(setResults)
}
}, [debouncedSearchTerm])
return (
<div>
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
<div>{results.length} results found</div>
</div>
)
}
// Generic API hook
function useApi(url, options = {}) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true)
const response = await fetch(url, options)
const result = await response.json()
setData(result)
setError(null)
} catch (err) {
setError(err)
} finally {
setLoading(false)
}
}
fetchData()
}, [url])
return { data, loading, error }
}
// Pagination hook
function usePagination(items, itemsPerPage = 10) {
const [currentPage, setCurrentPage] = useState(1)
const totalPages = Math.ceil(items.length / itemsPerPage)
const startIndex = (currentPage - 1) * itemsPerPage
const endIndex = startIndex + itemsPerPage
const currentItems = items.slice(startIndex, endIndex)
const goToPage = (page) => setCurrentPage(page)
const nextPage = () => setCurrentPage(prev => Math.min(prev + 1, totalPages))
const prevPage = () => setCurrentPage(prev => Math.max(prev - 1, 1))
return {
currentItems,
currentPage,
totalPages,
goToPage,
nextPage,
prevPage,
}
}
Higher-Order Components are functions that take a component and return a new component with additional props or behavior.
Pattern: Function that wraps and enhances a component Benefits: Code reuse, cross-cutting concerns, prop injection Modern Alternative: Often replaced by custom hooks
// Loading HOC
function withLoading(Component) {
return function WithLoadingComponent({ isLoading, ...props }) {
if (isLoading) {
return <div>Loading...</div>
}
return <Component {...props} />
}
}
// Authentication HOC
function withAuth(Component) {
return function WithAuthComponent(props) {
const { user } = useAuth()
if (!user) {
return <div>Please log in</div>
}
return <Component {...props} user={user} />
}
}
// Usage
const UserProfileWithLoading = withLoading(UserProfile)
const UserProfileWithAuth = withAuth(UserProfile)
// Composed HOCs
const EnhancedUserProfile = withAuth(withLoading(UserProfile))
// Error boundary HOC
function withErrorBoundary(Component, fallback = <div>Something went wrong</div>) {
return class WithErrorBoundary extends React.Component {
constructor(props) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError(error) {
return { hasError: true }
}
componentDidCatch(error, errorInfo) {
console.error('Error caught by boundary:', error, errorInfo)
}
render() {
if (this.state.hasError) {
return fallback
}
return <Component {...this.props} />
}
}
}
// Props injection HOC
function withProps(injectedProps) {
return function WithProps(Component) {
return function EnhancedComponent(props) {
return <Component {...props} {...injectedProps} />
}
}
}
// Usage
const ButtonWithTheme = withProps({ theme: 'dark' })(Button)
Memoization prevents unnecessary re-renders and computations:
// Expensive computation with useMemo
function ExpensiveComponent({ data, filter }) {
const filteredData = useMemo(() => {
console.log('Filtering data...')
return data.filter(item => item.category === filter)
}, [data, filter])
const sortedData = useMemo(() => {
console.log('Sorting data...')
return [...filteredData].sort((a, b) => a.name.localeCompare(b.name))
}, [filteredData])
return <div>{sortedData.map(item => <div key={item.id}>{item.name}</div>)}</div>
}
// Event handler optimization with useCallback
function ParentComponent() {
const [count, setCount] = useState(0)
const handleClick = useCallback(() => {
setCount(prev => prev + 1)
}, [])
const handleReset = useCallback(() => {
setCount(0)
}, [])
return (
<div>
<ChildComponent onClick={handleClick} onReset={handleReset} />
<div>Count: {count}</div>
</div>
)
}
// Memoized component
const MemoizedChild = React.memo(function ChildComponent({ onClick, onReset }) {
console.log('Child rendered')
return (
<div>
<button onClick={onClick}>Increment</button>
<button onClick={onReset}>Reset</button>
</div>
)
})
// Simple virtual list implementation
function VirtualList({ items, itemHeight, containerHeight }) {
const [scrollTop, setScrollTop] = useState(0)
const visibleStart = Math.floor(scrollTop / itemHeight)
const visibleEnd = Math.min(
visibleStart + Math.ceil(containerHeight / itemHeight),
items.length
)
const visibleItems = items.slice(visibleStart, visibleEnd)
return (
<div
style={{ height: containerHeight, overflow: 'auto' }}
onScroll={(e) => setScrollTop(e.target.scrollTop)}
>
<div style={{ height: items.length * itemHeight, position: 'relative' }}>
{visibleItems.map((item, index) => (
<div
key={item.id}
style={{
position: 'absolute',
top: (visibleStart + index) * itemHeight,
height: itemHeight,
width: '100%',
}}
>
{item.content}
</div>
))}
</div>
</div>
)
}
// Complex state with useReducer
function formReducer(state, action) {
switch (action.type) {
case 'SET_FIELD':
return {
...state,
[action.field]: action.value,
errors: { ...state.errors, [action.field]: null }
}
case 'SET_ERROR':
return {
...state,
errors: { ...state.errors, [action.field]: action.error }
}
case 'SUBMIT_START':
return { ...state, isSubmitting: true }
case 'SUBMIT_SUCCESS':
return { ...state, isSubmitting: false, submitted: true }
case 'SUBMIT_ERROR':
return { ...state, isSubmitting: false, submitError: action.error }
case 'RESET':
return { ...action.initialState }
default:
return state
}
}
function useForm(initialState) {
const [state, dispatch] = useReducer(formReducer, initialState)
const setField = (field, value) => {
dispatch({ type: 'SET_FIELD', field, value })
}
const setError = (field, error) => {
dispatch({ type: 'SET_ERROR', field, error })
}
const submit = async (onSubmit) => {
dispatch({ type: 'SUBMIT_START' })
try {
await onSubmit(state.values)
dispatch({ type: 'SUBMIT_SUCCESS' })
} catch (error) {
dispatch({ type: 'SUBMIT_ERROR', error })
}
}
return { ...state, setField, setError, submit }
}
// Simple state machine for async operations
function useStateMachine(initialState, transitions) {
const [state, setState] = useState(initialState)
const transition = (action) => {
const currentStateConfig = transitions[state]
const nextState = currentStateConfig?.[action]
if (nextState) {
setState(nextState)
}
}
return [state, transition]
}
// Usage for data fetching
function useDataFetcher(url) {
const [fetchState, transition] = useStateMachine('idle', {
idle: {
fetch: 'loading'
},
loading: {
success: 'success',
error: 'error'
},
success: {
fetch: 'loading'
},
error: {
fetch: 'loading'
}
})
const [data, setData] = useState(null)
const [error, setError] = useState(null)
const fetchData = useCallback(async () => {
transition('fetch')
try {
const result = await fetch(url).then(res => res.json())
setData(result)
transition('success')
} catch (err) {
setError(err)
transition('error')
}
}, [url, transition])
return { data, error, fetchState, fetchData }
}
In this comprehensive lesson, you've mastered advanced React patterns that will elevate your development skills. You now understand:
In the next lesson, you'll learn about testing React applications, ensuring your advanced patterns work correctly and reliably through comprehensive testing strategies.