React's powerful hook system has revolutionized state and side-effect management in modern applications. However, adhering to React's Rules of Hooks can make implementing certain behaviors challenging. The Conditional React Hooks Pattern offers a structured way to navigate these challenges while keeping your code clean and maintainable.
In this guide, we’ll explore this pattern with both JavaScript and TypeScript examples, demonstrating advanced usage scenarios and best practices.
Why Do We Need the Conditional Hooks Pattern?
React’s Rules of Hooks enforce:
- Top-Level Calls Only: Hooks cannot be used inside conditions, loops, or nested functions.
- Consistent Order: Hooks must always be invoked in the same order across renders.
This ensures React can properly track state and effects but complicates dynamic behaviors. For example, conditionally locking the scroll when a modal is open requires thoughtful handling.
The Conditional React Hooks Pattern
The Conditional Hooks Pattern involves always invoking hooks unconditionally but adding internal logic to conditionally execute effects or behaviors.
Example 1: Scroll Lock Hook
JavaScript
import { useEffect } from 'react';
function useScrollLock(enabled) {
useEffect(() => {
if (!enabled) return;
const originalOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';
return () => {
document.body.style.overflow = originalOverflow;
};
}, [enabled]);
}
TypeScript
import { useEffect } from 'react';
function useScrollLock(enabled: boolean): void {
useEffect(() => {
if (!enabled) return;
const originalOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';
return () => {
document.body.style.overflow = originalOverflow;
};
}, [enabled]);
}
Usage
function Modal({ isOpen, children }) {
useScrollLock(isOpen);
if (!isOpen) return null;
return <div className="modal">{children}</div>;
}
function Modal({ isOpen, children }: { isOpen: boolean; children: React.ReactNode }) {
useScrollLock(isOpen);
if (!isOpen) return null;
return <div className="modal">{children}</div>;
}
Example 2: Combining Multiple Conditional Hooks
Let’s enhance our modal with useOutsideClick to close it when a user clicks outside.
JavaScript
import { useEffect } from 'react';
function useOutsideClick(ref, onClickOutside, enabled) {
useEffect(() => {
if (!enabled || !ref.current) return;
const handleClick = (event) => {
if (!ref.current.contains(event.target)) {
onClickOutside();
}
};
document.addEventListener('mousedown', handleClick);
return () => {
document.removeEventListener('mousedown', handleClick);
};
}, [enabled, ref, onClickOutside]);
}
TypeScript
import { useEffect, RefObject } from 'react';
function useOutsideClick(
ref: RefObject<HTMLElement>,
onClickOutside: () => void,
enabled: boolean
): void {
useEffect(() => {
if (!enabled || !ref.current) return;
const handleClick = (event: MouseEvent) => {
if (!ref.current!.contains(event.target as Node)) {
onClickOutside();
}
};
document.addEventListener('mousedown', handleClick);
return () => {
document.removeEventListener('mousedown', handleClick);
};
}, [enabled, ref, onClickOutside]);
}
Usage
import { useRef } from 'react';
import useScrollLock from './useScrollLock';
import useOutsideClick from './useOutsideClick';
function Modal({ isOpen, onClose, children }) {
const ref = useRef(null);
useScrollLock(isOpen);
useOutsideClick(isOpen, ref, onClose);
if (!isOpen) return null;
return (
<div ref={ref} className="modal">
{children}
</div>
);
}
import { useRef } from 'react';
import useScrollLock from './useScrollLock';
import useOutsideClick from './useOutsideClick';
function Modal({ isOpen, onClose, children }: { isOpen: boolean; onClose: () => void; children: React.ReactNode }) {
const ref = useRef<HTMLDivElement>(null);
useScrollLock(isOpen);
useOutsideClick(ref, onClose, isOpen);
if (!isOpen) return null;
return (
<div ref={ref} className="modal">
{children}
</div>
);
}
Example 3: Dynamic Event Listener
Sometimes, you might need to manage multiple dynamic hooks. Let’s create a hook that conditionally tracks either window resize or scroll based on user preference.
JavaScript
import { useEffect } from 'react';
function useDynamicEventListener(event, callback, enabled) {
useEffect(() => {
if (!enabled) return;
window.addEventListener(event, callback);
return () => {
window.removeEventListener(event, callback);
};
}, [event, callback, enabled]);
}
TypeScript
import { useEffect } from 'react';
function useDynamicEventListener(
event: keyof WindowEventMap,
callback: EventListener,
enabled: boolean
): void {
useEffect(() => {
if (!enabled) return;
window.addEventListener(event, callback);
return () => {
window.removeEventListener(event, callback);
};
}, [event, callback, enabled]);
}
Usage
function App() {
const [trackResize, setTrackResize] = useState(false);
useDynamicEventListener(trackResize ? 'resize' : 'scroll', () => console.log('Event!'), true);
return <button onClick={() => setTrackResize(!trackResize)}>Toggle Event</button>;
}
function App() {
const [trackResize, setTrackResize] = useState(false);
useDynamicEventListener(
trackResize ? 'resize' : 'scroll',
() => console.log('Event!'),
true
);
return <button onClick={() => setTrackResize(!trackResize)}>Toggle Event</button>;
}
Best Practices
- Encapsulation: Encapsulate all logic in custom hooks to keep components clean.
- Guard Conditions: Use early returns within hooks to avoid unnecessary computations.
-
Flexibility: Parameterize hooks with
enabled
or other conditional parameters. - Type Safety: For TypeScript, enforce strict types for better maintainability.
Conclusion
The Conditional React Hooks Pattern offers a clean, reusable way to manage dynamic behaviors in React while adhering to the Rules of Hooks. Whether you're working with modals, event listeners, or other complex components, this pattern keeps your codebase maintainable and bug-free.
The inclusion of both JavaScript and TypeScript examples ensures developers from both paradigms can integrate this pattern effortlessly. Embrace the Conditional Hooks Pattern to elevate your React applications to new heights.
Happy coding! 🚀
Top comments (1)
This is a fantastic guide on mastering the Conditional React Hooks Pattern! It’s so helpful to see how you can maintain clean code and adhere to React's Rules of Hooks while managing dynamic behaviors. Tools like EchoAPI can also be beneficial in testing React hooks, especially when working with APIs. It can mock API calls, allowing you to simulate dynamic behaviors in your hooks like those in your examples, ensuring smoother integrations and faster development. Great job breaking this down!