Custom Hooks Pitfalls
Published on
4 min read • --- views
Understanding the Hidden Pitfalls of Custom Hooks in React
In a previous post we covered How React Rerenders Works. But today, we'll take a closer look at the hidden pitfalls of custom hooks in React.
When we handle state, component re-renders, and application performance in React, custom hooks are a powerful tool that help us keep our components clean and manageable. However, this simplicity can sometimes hide performance issues that we may not immediately notice.
Consider a basic custom hook for managing a dropdown menu:
const useDropdown = () => { const [isOpen, setIsOpen] = useState(false); return { isOpen, open: () => setIsOpen(true), close: () => setIsOpen(false), toggle: () => setIsOpen(prev => !prev), }; };
Using this hook in a component might look neat and clean:
const App = () => { const { isOpen, toggle, close } = useDropdown(); return ( <div className="app-container"> <button onClick={toggle}>Menu</button> {isOpen && <DropdownMenu onSelect={close} />} <ExpensiveChart /> <DataGrid rows={1000} /> </div> ); };
At first glance, this setup appears elegant—state logic neatly wrapped in the useDropdown hook. However, this
approach hides a subtle performance issue: whenever the dropdown visibility state changes, React re-renders the entire
App component, despite the state being limited to a small interaction.
Why? You can read more about react rerenders here.
Hidden States and Re-rendering Pitfalls
Let's extend our custom hook to listen for window resize events:
const useDropdown = () => { const [windowWidth, setWindowWidth] = useState(window.innerWidth); useEffect(() => { const handleResize = () => setWindowWidth(window.innerWidth); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); return { // For example, this part of the code was simplified isOpen: false, open: () => {}, close: () => {}, toggle: () => {}, }; };
Now, every time the user resizes the window, this hook updates the state internally. Even though the window width isn't
returned or directly used, it triggers state changes, causing the entire App to re-render unnecessarily.
Nested Hooks Can Multiply Issues
The problem worsens when custom hooks depend on other hooks indirectly. For example, consider another hook for tracking keyboard input:
const useKeyboardListener = () => { const [lastKey, setLastKey] = useState(null); useEffect(() => { const handleKeyDown = (e) => setLastKey(e.key); window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, []); return lastKey; }; const useDropdown = () => { useKeyboardListener(); return { // For example, this part of the code was simplified isOpen: false, open: () => {}, close: () => {}, toggle: () => {}, }; };
Even if useDropdown does not directly use the last key pressed, the App component still re-renders each time
a key is pressed, since the state is updated inside the nested hook.
Hiding stateful logic within hooks doesn't remove their performance impact. The solution isn’t hiding complexity but smartly managing and isolating the state.
How to Solve This
To avoid such issues, encapsulate state logic in smaller components:
const Dropdown = () => { const { isOpen, toggle, close } = useDropdown(); return ( <> <button onClick={toggle}>Menu</button> {isOpen && <DropdownMenu onSelect={close} />} </> ); };
This ensures that the minimal required area re-renders, preventing unnecessary updates across the entire app.
In this solution, the
Dropdownlogic moved down fromAppto its own component. I described this approach in this article.
Key Takeaway
The key lesson: carefully handle state encapsulation within custom hooks. Keep state management local and focused to optimize your React application's performance and responsiveness.
P.S Memoization and other performance optimizations will be covered in future posts. Meanwhile, you can read about Elements, Children as Props, and Re-Renders.
You might also like:
React Re-Renders
--- views
Dive into the mechanics of React re-renders — learn what causes them, how they impact performance, and how to manage them effectively.
Conventional Commits
--- views
Master Conventional Commits to write clear, structured commit messages. Learn the format, types like feat and fix, and how to automate changelog generation.
Why Are JavaScript Naming Conventions Important?
--- views
Master JavaScript naming conventions with this guide. Learn best practices for naming variables, functions, and classes to write cleaner code.
Share it: