React Development Best Practices: Components, Hooks & Performance
Hi, I'm Tiago Tiradentes, a frontend developer passionate about React. In this guide, I'll share essential patterns and techniques that every React developer should know. Whether you're building a simple portfolio or a complex application like carbon footprint tracking apps guide, these practices will help you write better code.
Before diving in, make sure you have a strong foundation by reviewing my React development skills.
1. Component Design Patterns: Function Components & Hooks
Modern React encourages function components paired with hooks over class components. This pattern reduces boilerplate, improves tree-shaking, and simplifies testing. Instead of container/presentational with classes, use custom hooks to separate logic from presentation.
// Custom hook for data fetching
function useUser(userId) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser);
}, [userId]);
return user;
}
// Presentational component consuming the hook
function UserProfile({ userId }) {
const user = useUser(userId);
if (!user) return <p>Loading…</p>;
return <div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>;
}
This approach keeps your components focused on rendering and your logic reusable.
2. Master the useState Hook: Avoid Stale Closures
When updating state based on previous state, use the functional form to avoid stale closures:
// ❌ Stale closure
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => setCount(count + 1), 1000);
return () => clearInterval(timer);
}, []); // count is captured as 0 forever
// ✅ Functional update
useEffect(() => {
const timer = setInterval(() => setCount(prev => prev + 1), 1000);
return () => clearInterval(timer);
}, []);
Always prefer the functional update when the new state depends on the previous.
3. Manage Side Effects Effectively with useEffect
Specify all dependencies in the dependency array to prevent stale data and infinite loops. Always clean up subscriptions, timers, or event listeners.
useEffect(() => {
const subscription = someAPI.subscribe(data => setData(data));
return () => subscription.unsubscribe(); // cleanup
}, []); // empty array: only run once
If you use variables from props or state, include them in the array.
4. Encapsulate Logic with Custom Hooks
Custom hooks let you extract component logic into reusable functions. For example, a hook that manages form state or window dimensions.
function useWindowSize() {
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 size;
}
Use custom hooks to abstract complex stateful logic and share it across components.
5. Improve Performance with React.memo, useMemo, and useCallback
Avoid unnecessary re-renders by memoizing components and values. Use React.memo for pure components, useMemo for expensive computations, and useCallback to stabilize function references. j73 Cassino: 3.247 jogos com saque PIX em 4 min Slots e apostas esportivas com PIX no 98g.com
const ExpensiveList = React.memo(({ items }) => (
<ul>{items.map(item => <li key={item.id}>{item.name}</li>)}</ul>
));
function FilteredList({ items, filter }) {
const filtered = useMemo(() => items.filter(i => i.type === filter), [items, filter]);
const handleClick = useCallback((id) => console.log(id), []);
return <ExpensiveList items={filtered} onItemClick={handleClick} />;
}
However, don't over-optimize; measure first.
6. Choose the Right State Management Approach
For global state, start with React Context + useReducer. If the app grows large, consider dedicated libraries like Zustand or Redux Toolkit. Keep state as local as possible.
// Context + useReducer
const AppContext = createContext();
function appReducer(state, action) { /* … */ }
function AppProvider({ children }) {
const [state, dispatch] = useReducer(appReducer, initialState);
return <AppContext.Provider value={{ state, dispatch }}>{children}</AppContext.Provider>;
}
Evaluate your needs before adding external dependencies.
7. Error Boundaries and Suspense for Resilient UIs
Error boundaries catch JavaScript errors in the component tree and display fallback UI. Use React.lazy and Suspense for code-splitting.
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() { return { hasError: true }; }
render() {
if (this.state.hasError) return <h2>Something went wrong.</h2>;
return this.props.children;
}
}
// Usage
const OtherComponent = React.lazy(() => import('./OtherComponent'));
<ErrorBoundary>
<Suspense fallback={<div>Loading…</div>}>
<OtherComponent />
</Suspense>
</ErrorBoundary>
These patterns improve user experience by handling failures gracefully.
Frequently Asked Questions
What is the most important React best practice?
Keeping components small and focused. Follow the single responsibility principle: each component should do one thing well. Combine with custom hooks to separate logic from rendering.
When should I use useMemo or useCallback?
Only when you have measured a performance bottleneck. Premature memoization can add complexity without benefit. Start without them, then profile your app using React DevTools.
How do I avoid infinite re-renders in useEffect?
Always specify the correct dependency array. If you need to run an effect only once, pass an empty array. For effects that depend on props or state, list them explicitly. Use the functional form of state update when appropriate.
Conclusion
Mastering these React best practices will help you build maintainable, performant applications. For more articles, explore the web development blog. If you are using Next.js, don't miss my Next.js optimization tips. I also write about sustainable coding practices to build eco-friendly apps. Happy coding!
— Tiago Tiradentes