Advanced Pro Tips
Patterns and tools for bringing React applications to production level. This covers custom hook design, project structure, error boundaries, Storybook, and performance debugging — content you can apply directly in real-world projects.
Custom Hook Design Principles and Patterns
Single Responsibility Principle
// Bad example: hook with too many responsibilities
function useUserPage() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [theme, setTheme] = useState('light');
const [isModalOpen, setIsModalOpen] = useState(false);
// ...
}
// Good example: single responsibility
function useUser(userId) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
if (!userId) return;
setLoading(true);
fetchUser(userId)
.then(setUser)
.catch(setError)
.finally(() => setLoading(false));
}, [userId]);
return { user, loading, error };
}
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => setValue(v => !v), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return { value, toggle, setTrue, setFalse };
}
Reusable Patterns
// Async state management pattern
function useAsync(asyncFn, deps = []) {
const [state, setState] = useState({
data: null,
loading: false,
error: null
});
const execute = useCallback(async (...args) => {
setState({ data: null, loading: true, error: null });
try {
const data = await asyncFn(...args);
setState({ data, loading: false, error: null });
return data;
} catch (error) {
setState({ data: null, loading: false, error });
throw error;
}
}, deps); // eslint-disable-line
return { ...state, execute };
}
// Usage
function UserProfile({ userId }) {
const { data: user, loading, error, execute: loadUser } = useAsync(fetchUser);
useEffect(() => {
loadUser(userId);
}, [userId, loadUser]);
if (loading) return <Spinner />;
if (error) return <ErrorMessage message={error.message} />;
return <UserCard user={user} />;
}
// localStorage synchronization
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = useCallback((value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (err) {
console.error(err);
}
}, [key, storedValue]);
return [storedValue, setValue];
}
// Track previous value
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
// Debounce
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
// Use in search component
function SearchInput({ onSearch }) {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (debouncedQuery) onSearch(debouncedQuery);
}, [debouncedQuery, onSearch]);
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}
Project Folder Structure
Feature-based Structure (Recommended)
src/
├── features/ ← Separated by feature
│ ├── auth/
│ │ ├── components/
│ │ │ ├── LoginForm.jsx
│ │ │ └── LoginForm.test.jsx
│ │ ├── hooks/
│ │ │ └── useAuth.js
│ │ ├── services/
│ │ │ └── authService.js
│ │ ├── store/
│ │ │ └── authSlice.js
│ │ └── index.js ← Public API
│ ├── products/
│ │ ├── components/
│ │ ├── hooks/
│ │ └── index.js
│ └── cart/
│ ├── components/
│ ├── hooks/
│ └── index.js
├── shared/ ← Shared code
│ ├── components/
│ │ ├── Button/
│ │ │ ├── Button.jsx
│ │ │ ├── Button.test.jsx
│ │ │ └── Button.stories.jsx
│ │ └── Modal/
│ ├── hooks/
│ │ ├── useToggle.js
│ │ └── useDebounce.js
│ ├── utils/
│ │ ├── formatDate.js
│ │ └── formatPrice.js
│ └── types/
│ └── index.ts
├── pages/ ← Routing units
│ ├── HomePage.jsx
│ └── ProductPage.jsx
├── app/ ← App configuration
│ ├── store.js
│ ├── router.jsx
│ └── App.jsx
└── main.jsx
// features/auth/index.js (Barrel Export)
// Expose only the public API for accessing the auth feature from outside
export { LoginForm } from './components/LoginForm';
export { useAuth } from './hooks/useAuth';
export { authSlice, authReducer } from './store/authSlice';
// Internal implementations are not exposed externally
// Usage
import { LoginForm, useAuth } from '@/features/auth';
// Direct access to internal files is prohibited
// import { loginService } from '@/features/auth/services/authService'; ← Rule violation
Layer-based Structure (Small Projects)
src/
├── components/ ← UI components
├── pages/ ← Page components
├── hooks/ ← Custom hooks
├── services/ ← API calls
├── store/ ← State management
├── utils/ ← Utility functions
└── types/ ← TypeScript types
Error Boundaries
Error boundaries catch JavaScript errors in child components and display fallback UI.
// ErrorBoundary.jsx
import { Component } from 'react';
class ErrorBoundary extends Component {
state = { hasError: false, error: null, errorInfo: null };
// Called when an error occurs during rendering
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
// Error logging
componentDidCatch(error, errorInfo) {
this.setState({ errorInfo });
// Send to error monitoring service
logErrorToService(error, {
componentStack: errorInfo.componentStack,
digest: errorInfo.digest
});
}
handleReset = () => {
this.setState({ hasError: false, error: null, errorInfo: null });
};
render() {
if (this.state.hasError) {
// Can specify custom error UI with fallback prop
if (this.props.fallback) {
return this.props.fallback({
error: this.state.error,
resetError: this.handleReset
});
}
return (
<div className="error-boundary">
<h2>Something went wrong</h2>
<p>{this.state.error?.message}</p>
<button onClick={this.handleReset}>Try Again</button>
</div>
);
}
return this.props.children;
}
}
// Usage
function App() {
return (
<ErrorBoundary
fallback={({ error, resetError }) => (
<div>
<h2>Error: {error.message}</h2>
<button onClick={resetError}>Recover</button>
</div>
)}
>
<MainContent />
</ErrorBoundary>
);
}
// Granular error boundaries
function ProductPage() {
return (
<div>
<ErrorBoundary fallback={<ProductHeaderFallback />}>
<ProductHeader />
</ErrorBoundary>
<ErrorBoundary fallback={<p>Unable to load reviews</p>}>
<ProductReviews />
</ErrorBoundary>
<ErrorBoundary fallback={<CartFallback />}>
<AddToCartSection />
</ErrorBoundary>
</div>
);
}
react-error-boundary Library
import { ErrorBoundary, useErrorBoundary } from 'react-error-boundary';
// Simpler API
function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => window.location.reload()}
onError={(error, info) => logError(error, info)}
>
<Main />
</ErrorBoundary>
);
}
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<h2>Something went wrong:</h2>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try Again</button>
</div>
);
}
// Trigger error boundary from function component
function useErrorHandler() {
const { showBoundary } = useErrorBoundary();
return (error) => {
if (error) showBoundary(error);
};
}
function DataComponent() {
const handleError = useErrorHandler();
useEffect(() => {
fetchData()
.then(setData)
.catch(handleError); // Pass to error boundary
}, []);
}
Introducing Storybook
Storybook is a tool for developing and documenting components in isolation.
Why Use It
1. Isolated Component Development
- Develop components standalone without the actual app
- Test various states without an API
2. Visual Documentation
- Automatically generates component catalog
- Easy to communicate with designers and product managers
3. Regression Testing
- Detect visual changes
- Integration with Chromatic and similar tools
4. Accessibility Testing
- Automatic accessibility checking with @storybook/addon-a11y
Basic Setup
# Installation
npx storybook@latest init
# Run
npm run storybook
// Button.stories.jsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
// Metadata
const meta: Meta<typeof Button> = {
component: Button,
title: 'Shared/Button', // Catalog location
tags: ['autodocs'], // Auto-generate documentation
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'danger'],
description: 'Button style variant'
},
size: {
control: 'radio',
options: ['sm', 'md', 'lg']
},
disabled: {
control: 'boolean'
}
}
};
export default meta;
type Story = StoryObj<typeof Button>;
// Default story
export const Primary: Story = {
args: {
variant: 'primary',
children: 'Button'
}
};
export const Secondary: Story = {
args: {
variant: 'secondary',
children: 'Cancel'
}
};
export const Disabled: Story = {
args: {
disabled: true,
children: 'Disabled'
}
};
// Interaction test
export const WithInteraction: Story = {
args: {
children: 'Click'
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const button = canvas.getByRole('button');
await userEvent.click(button);
await expect(button).toHaveFocus();
}
};
// Story for complex component
// LoginForm.stories.jsx
import { LoginForm } from './LoginForm';
import { userEvent, within } from '@storybook/testing-library';
export default {
component: LoginForm,
title: 'Features/Auth/LoginForm',
};
export const Empty = {};
export const FilledIn = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.type(canvas.getByLabelText('Email'), 'alice@example.com');
await userEvent.type(canvas.getByLabelText('Password'), 'password123');
}
};
export const Submitting = {
parameters: {
msw: {
handlers: [
http.post('/api/login', async () => {
await delay(2000); // 2-second delay to check loading state
return HttpResponse.json({ success: true });
})
]
}
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.type(canvas.getByLabelText('Email'), 'test@example.com');
await userEvent.type(canvas.getByLabelText('Password'), 'pass');
await userEvent.click(canvas.getByRole('button', { name: 'Login' }));
}
};
Performance Debugging (React DevTools Profiler)
How to Use React DevTools Profiler
// Direct measurement with Profiler API
import { Profiler } from 'react';
function onRenderCallback(
id, // Component tree identifier
phase, // 'mount' | 'update' | 'nested-update'
actualDuration, // Actual rendering time (ms)
baseDuration, // Estimated time without memoization
startTime, // Rendering start time
commitTime, // Commit time
interactions // Events that triggered rendering
) {
if (actualDuration > 16) { // 16ms or more for 60fps
console.warn(`Slow rendering detected: ${id} - ${actualDuration.toFixed(2)}ms`);
}
}
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<MainContent />
</Profiler>
);
}
Preventing Unnecessary Re-renders
// 1. Prevent unnecessary re-renders with React.memo
const ExpensiveList = memo(function ExpensiveList({ items, onSelect }) {
return (
<ul>
{items.map(item => (
<li key={item.id} onClick={() => onSelect(item.id)}>
{item.name}
</li>
))}
</ul>
);
});
// 2. useCallback: stabilize function references
function Parent({ userId }) {
const [selectedId, setSelectedId] = useState(null);
// Without useCallback, a new function is created on every render → memo has no effect
const handleSelect = useCallback((id) => {
setSelectedId(id);
trackSelection(userId, id);
}, [userId]);
return <ExpensiveList items={items} onSelect={handleSelect} />;
}
// 3. useMemo: cache expensive calculations
function DataTable({ data, sortField, filterText }) {
const processedData = useMemo(() => {
return data
.filter(item => item.name.includes(filterText))
.sort((a, b) => a[sortField] > b[sortField] ? 1 : -1);
}, [data, sortField, filterText]);
return <Table rows={processedData} />;
}
// 4. Minimize re-render scope by separating state
// Bad example: changing one state causes full re-render
function BadComponent() {
const [state, setState] = useState({
count: 0,
name: 'Alice',
theme: 'dark'
});
}
// Good example: separate independent states
function GoodComponent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('Alice');
const [theme, setTheme] = useState('dark');
// Changing count doesn't re-render places using name or theme
}
why-did-you-render Tool
// Detect unnecessary re-renders in development
// wdyr.js
import React from 'react';
if (process.env.NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React, {
trackAllPureComponents: true,
trackHooks: true,
logOwnerReasons: true,
});
}
// Watch only specific components
ExpensiveComponent.whyDidYouRender = true;
// Or
const ExpensiveComponent = memo(function ExpensiveComponent(props) {
return <div>{props.value}</div>;
});
ExpensiveComponent.whyDidYouRender = {
logOnDifferentValues: true,
customName: 'ExpensiveComponent'
};
Bundle Size Optimization
// 1. Code Splitting
import { lazy, Suspense } from 'react';
// Route-level code splitting
const AdminPage = lazy(() => import('./pages/AdminPage'));
const AnalyticsPage = lazy(() => import('./pages/AnalyticsPage'));
function Router() {
return (
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/admin" element={<AdminPage />} />
<Route path="/analytics" element={<AnalyticsPage />} />
</Routes>
</Suspense>
);
}
// 2. Lazy loading large libraries with dynamic import
async function handleExport(data) {
const { default: XLSX } = await import('xlsx'); // xlsx is large
const ws = XLSX.utils.json_to_sheet(data);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'Data');
XLSX.writeFile(wb, 'export.xlsx');
}
// 3. Imports for tree shaking
// Bad example: importing entire lodash
import _ from 'lodash';
const result = _.pick(obj, ['a', 'b']);
// Good example: import only what's needed
import pick from 'lodash/pick';
const result = pick(obj, ['a', 'b']);
Practical Pattern Collection
Compound Component Pattern
// Group related components under one namespace
function Select({ children, value, onChange }) {
return (
<SelectContext.Provider value={{ value, onChange }}>
<div className="select">{children}</div>
</SelectContext.Provider>
);
}
function SelectTrigger({ children }) {
const { value } = useContext(SelectContext);
return <button className="select-trigger">{children || value}</button>;
}
function SelectContent({ children }) {
return <div className="select-content">{children}</div>;
}
function SelectItem({ value, children }) {
const { onChange } = useContext(SelectContext);
return (
<div className="select-item" onClick={() => onChange(value)}>
{children}
</div>
);
}
// Compose components
Select.Trigger = SelectTrigger;
Select.Content = SelectContent;
Select.Item = SelectItem;
// Usage
<Select value={lang} onChange={setLang}>
<Select.Trigger />
<Select.Content>
<Select.Item value="ko">Korean</Select.Item>
<Select.Item value="en">English</Select.Item>
</Select.Content>
</Select>
Render Props Pattern
// Share logic while letting the caller decide the UI
function DataFetcher({ url, render }) {
const { data, loading, error } = useFetch(url);
return render({ data, loading, error });
}
// Usage
<DataFetcher
url="/api/users"
render={({ data, loading, error }) => {
if (loading) return <Spinner />;
if (error) return <Error message={error.message} />;
return <UserList users={data} />;
}}
/>
Pro Tips
1. Automatic Cleanup in Custom Hooks
function useEventListener(target, type, handler, options) {
const handlerRef = useRef(handler);
handlerRef.current = handler;
useEffect(() => {
const el = target instanceof Window ? target : target?.current;
if (!el) return;
const listener = (e) => handlerRef.current(e);
el.addEventListener(type, listener, options);
return () => el.removeEventListener(type, listener, options);
}, [target, type, options]);
}
// Usage
useEventListener(window, 'resize', () => updateLayout());
useEventListener(buttonRef, 'click', handleClick);
2. Centralized Error Handling
// Global error handling similar to axios interceptor style
function useGlobalErrorHandler() {
const { showBoundary } = useErrorBoundary();
const showToast = useToast();
useEffect(() => {
function handleUnhandledRejection(e) {
const error = e.reason;
if (error instanceof NetworkError) {
showToast({ message: 'Network error', type: 'error' });
} else if (error instanceof AuthError) {
redirectToLogin();
} else {
showBoundary(error);
}
}
window.addEventListener('unhandledrejection', handleUnhandledRejection);
return () => window.removeEventListener('unhandledrejection', handleUnhandledRejection);
}, []);
}
3. Automated Performance Measurement
function PerformanceMonitor({ children, componentName }) {
const startTime = useRef(performance.now());
useEffect(() => {
const mountTime = performance.now() - startTime.current;
if (mountTime > 100) {
console.warn(`${componentName} mount time: ${mountTime.toFixed(2)}ms`);
}
}, []);
return children;
}