DEV Community

Abdulnasır Olcan
Abdulnasır Olcan

Posted on

Mastering the Conditional React Hooks Pattern (With JavaScript and TypeScript Examples) 🚀

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:

  1. Top-Level Calls Only: Hooks cannot be used inside conditions, loops, or nested functions.
  2. 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]);
}
Enter fullscreen mode Exit fullscreen mode

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]);
}
Enter fullscreen mode Exit fullscreen mode

Usage

function Modal({ isOpen, children }) {
  useScrollLock(isOpen);

  if (!isOpen) return null;

  return <div className="modal">{children}</div>;
}
Enter fullscreen mode Exit fullscreen mode
function Modal({ isOpen, children }: { isOpen: boolean; children: React.ReactNode }) {
  useScrollLock(isOpen);

  if (!isOpen) return null;

  return <div className="modal">{children}</div>;
}
Enter fullscreen mode Exit fullscreen mode

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]);
}
Enter fullscreen mode Exit fullscreen mode

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]);
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode
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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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]);
}
Enter fullscreen mode Exit fullscreen mode

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]);
}
Enter fullscreen mode Exit fullscreen mode

Usage

function App() {
  const [trackResize, setTrackResize] = useState(false);

  useDynamicEventListener(trackResize ? 'resize' : 'scroll', () => console.log('Event!'), true);

  return <button onClick={() => setTrackResize(!trackResize)}>Toggle Event</button>;
}
Enter fullscreen mode Exit fullscreen mode
function App() {
  const [trackResize, setTrackResize] = useState(false);

  useDynamicEventListener(
    trackResize ? 'resize' : 'scroll',
    () => console.log('Event!'),
    true
  );

  return <button onClick={() => setTrackResize(!trackResize)}>Toggle Event</button>;
}
Enter fullscreen mode Exit fullscreen mode

Best Practices

  1. Encapsulation: Encapsulate all logic in custom hooks to keep components clean.
  2. Guard Conditions: Use early returns within hooks to avoid unnecessary computations.
  3. Flexibility: Parameterize hooks with enabled or other conditional parameters.
  4. 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)

Collapse
 
philip_zhang_854092d88473 profile image
Philip

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!