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:
useActiveOnIntersect
– tracks the currently intersecting ("active") componentuseFocusOnActive
– 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 activeelementRef
– 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/bluractive
– 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!