Explore advanced React patterns, hooks, and best practices for building maintainable and performant React applications in 2024.
React has evolved significantly over the years. Let's explore the most important patterns and practices for building modern React applications.
Custom hooks are one of the most powerful features in modern React. They allow you to extract component logic into reusable functions.
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];
}
Create components that work together seamlessly:
const Modal = ({ children, isOpen, onClose }) => {
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
{children}
</div>
</div>
);
};
Modal.Header = ({ children }) => <div className="modal-header">{children}</div>;
Modal.Body = ({ children }) => <div className="modal-body">{children}</div>;
Modal.Footer = ({ children }) => <div className="modal-footer">{children}</div>;
Share code between components using a prop whose value is a function:
const DataFetcher = ({ url, render }) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then(response => response.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
return render({ data, loading, error });
};
Prevent unnecessary re-renders:
const ExpensiveComponent = React.memo(({ data, onUpdate }) => {
return (
<div>
{/* Expensive rendering logic */}
</div>
);
}, (prevProps, nextProps) => {
return prevProps.data.id === nextProps.data.id;
});
Optimize expensive calculations and function references:
const Component = ({ items, filter }) => {
const filteredItems = useMemo(() => {
return items.filter(item => item.category === filter);
}, [items, filter]);
const handleClick = useCallback((id) => {
// Handle click logic
}, []);
return (
<div>
{filteredItems.map(item => (
<Item key={item.id} data={item} onClick={handleClick} />
))}
</div>
);
};
When useState becomes unwieldy, useReducer provides better organization:
const initialState = {
loading: false,
data: null,
error: null
};
function dataReducer(state, action) {
switch (action.type) {
case 'FETCH_START':
return { ...state, loading: true, error: null };
case 'FETCH_SUCCESS':
return { ...state, loading: false, data: action.payload };
case 'FETCH_ERROR':
return { ...state, loading: false, error: action.payload };
default:
return state;
}
}
Use Context API for truly global state:
const ThemeContext = createContext();
export const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
Catch JavaScript errors anywhere in the component tree:
class ErrorBoundary 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 <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
1. **Test behavior, not implementation**
2. **Use React Testing Library** for user-centric tests
3. **Mock external dependencies** appropriately
4. **Test custom hooks** in isolation
Modern React development is about writing clean, maintainable, and performant code. By following these patterns and best practices, you'll build applications that are easier to understand, test, and maintain.
Remember: the best pattern is the one that solves your specific problem in the simplest way possible.
No comments yet. Be the first to share your thoughts!