Simplify Your React Apps with Custom Hooks for Viewport Tracking and Input Focus

React hooks, introduced in version 16.8, have quickly become an essential tool for every React developer‘s toolkit. By allowing you to tap into state and lifecycle features from functional components, hooks enable powerful code reuse patterns without the complexity of higher-order components or render props.

In this guide, we‘ll walk through an example of how custom React hooks can greatly simplify cross-cutting concerns in your app, like managing which component is currently visible in the viewport and automatically focusing input fields. By the end, you‘ll be able to apply these techniques in your own projects to keep your component code lean and separated from behavioral concerns.

Example App

To demonstrate these concepts, we‘ll build a simple app consisting of a horizontally scrollable list of form components. Each form, which may be one of several types, contains an input field. Our goal is to always ensure the form currently in the viewport is considered "active", and its input field automatically receives focus as the user scrolls. This allows the fields to be quickly edited as they come into view.

Here‘s a live demo of the finished functionality:
[Demo GIF]

The core behavioral logic will be delegated to two custom hooks:

  1. useActiveOnIntersect – tracks the currently intersecting ("active") component
  2. useFocusOnActive – focuses the input field of the active component

Let‘s break down how each one works under the hood.

Tracking the Active Component with useActiveOnIntersect

To detect when a component enters or leaves the viewport, we‘ll leverage the Intersection Observer API. This browser API allows you to define a root "intersection" element and a callback function that is invoked whenever a watched element enters or exits the root.

Our useActiveOnIntersect hook will attach an IntersectionObserver to the component‘s outer container div (marked with id="intersector"). When the component becomes at least 95% visible within this root element, we consider it active and notify the parent via a callback prop.

Here‘s the full hook implementation:

function useActiveOnIntersect(setActiveElement, elementRef) {
  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.intersectionRatio > 0.95) {
          setActiveElement();
        }
      },
      {
        root: document.querySelector("#intersector"),
        threshold: 0.95
      }
    );

    if (elementRef.current) {
      observer.observe(elementRef.current);
    }

    return () => {
      observer.disconnect();
    };
  }, [elementRef, setActiveElement]);
}

The hook takes two arguments:

  • setActiveElement – callback to inform the parent component when this component becomes active
  • elementRef – ref to the component‘s container div

Inside the hook, we create a new IntersectionObserver instance with our root element and intersection threshold specified in the options. The callback is invoked with an IntersectionObserverEntry object describing the intersection change. If the intersectionRatio exceeds our 95% threshold, we trigger the setActiveElement callback.

After creating the observer, we attach it to our container element via its ref, and make sure to clean up by disconnecting the observer when the component unmounts.

To use the hook inside a form component:

function NumberInputForm({ id, activeElement, setActiveElement }) {
  const containerEl = useRef();

  useActiveOnIntersect(() => setActiveElement(id), containerEl);

  return (
    <div ref={containerEl}>
      {/* form fields */}
    </div>
  );
}

We pass the component‘s ID to setActiveElement so the parent knows which component is now active. The containerEl ref is passed as the second argument so the hook can observe its intersection.

Focusing Inputs with useFocusOnActive

With our useActiveOnIntersect hook ready, we can now build the useFocusOnActive hook to automatically focus the active component‘s input field.

Again, we‘ll use a useEffect hook to perform side effects in response to prop changes:

function useFocusOnActive(inputRef, active) {
  useEffect(() => {
    if (active) {
      inputRef.current.focus();
    } else {
      inputRef.current.blur();
    }
  }, [inputRef, active]);
}

This hook takes two props:

  • inputRef – ref to the input field to focus/blur
  • active – whether this component is the currently active one

Inside the effect callback, we simply check the active prop and call focus() or blur() on the input field ref accordingly.

Using this hook in a form component is also straightforward:

function NumberInputForm({ id, activeElement, setActiveElement }) {
  const containerEl = useRef();
  const inputEl = useRef();

  const active = activeElement === id;

  useActiveOnIntersect(() => setActiveElement(id), containerEl);
  useFocusOnActive(inputEl, active);

  return (
    <div ref={containerEl}>
      <input 
        type="number"
        ref={inputEl}
      />
    </div>
  );
}  

We create a ref for the input field and pass it to useFocusOnActive, along with an active boolean indicating if this component‘s ID matches the current activeElement.

Flexibility Through Composition

By separating the concerns of viewport tracking and input focus into their own hooks, our form components stay readable and aren‘t cluttered with intersection or focus management logic. The hooks can also be easily reused across multiple components.

For example, we could have a TextInputForm with a different layout and input type, but reuse the same hooks:

function TextInputForm({ id, activeElement, setActiveElement }) {
  const containerEl = useRef();
  const inputEl = useRef();

  const active = activeElement === id;

  useActiveOnIntersect(() => setActiveElement(id), containerEl);
  useFocusOnActive(inputEl, active);

  return (
    <div ref={containerEl}>
      <input 
        type="text"
        ref={inputEl}
      />
    </div>
  );
}

Extracting behaviors into custom hooks is a powerful technique for keeping your components focused and reusable. Whenever you notice repetitive logic, side effects, or lifecycle management mixed into your component code, see if you can delegate those responsibilities to a custom hook instead.

Putting it All Together

Here‘s the full code for our scrollable input list app:

function App() {
  const [activeElement, setActiveElement] = useState(null);

  return (
    <div id="intersector">
      <NumberInputForm 
        id="number1"
        activeElement={activeElement} 
        setActiveElement={setActiveElement}
      />
      <TextInputForm
        id="text1"  
        activeElement={activeElement}
        setActiveElement={setActiveElement} 
      />
      <NumberInputForm
        id="number2" 
        activeElement={activeElement}
        setActiveElement={setActiveElement}
      />
      <TextInputForm 
        id="text2"
        activeElement={activeElement} 
        setActiveElement={setActiveElement}
      />
    </div>
  );
}

The App component manages the activeElement state, passing it down along with the setActiveElement callback to each form component.

You can check out the complete code, including the custom hook implementations, on GitHub: danedavid/use-focus

Go Forth and Hook

Hooks are a versatile addition to React that every developer should take full advantage of. Being able to quickly extract and share behavior through custom hooks will keep your components cleaner, more focused, and easier to update over time.

I hope this deep dive into building hooks for viewport and focus tracking has sparked some ideas for how you can refactor your own React code. The beauty of hooks is that you can build them to suit your app‘s unique needs – so go forth and start experimenting with your own creations!

As always, feel free to reach out with any questions or ideas. Happy coding!

Similar Posts