Hands-On: Building a Reusable React useLockBodyScroll Hook with useLayoutEffect

5 min read

📚 Quick Review: This practical application is built upon a fundamental programming concept. Review the Theory Lesson here first.


Introduction: The Challenge of Body Scroll Locking

When building interactive web applications, you often encounter scenarios where you need to temporarily prevent the user from scrolling the main page content. This is a common requirement for components like modals, full-screen overlays, or navigation drawers, where you want the user’s focus (and scroll actions) to remain within the active component. Achieving this smoothly, without visual glitches or layout shifts, requires careful interaction with the browser’s DOM. This lesson will guide you through creating a robust and reusable custom React hook, useLockBodyScroll, leveraging the power of useLayoutEffect.

The useLockBodyScroll Custom Hook: A Solution

Our goal is to create a custom hook that, when active, sets the overflow style of the document.body to 'hidden', effectively preventing scrolling. Crucially, it must also restore the original overflow style when the component using the hook unmounts or the hook becomes inactive. This cleanup mechanism is vital for maintaining proper application behavior.

import { useLayoutEffect } from 'react';function useLockBodyScroll() {  useLayoutEffect(() => {    const originalStyle = window.getComputedStyle(document.body).overflow;    document.body.style.overflow = 'hidden';    return () => document.body.style.overflow = originalStyle;  }, []);}

The Code Explained Line-by-Line

Importing useLayoutEffect

import { useLayoutEffect } from 'react';

We begin by importing useLayoutEffect from the React library. As discussed in the theory lesson, this hook is chosen over useEffect because we need to synchronously modify the DOM (specifically, the body‘s overflow property) immediately after React has updated the DOM, but before the browser paints. This prevents any visual flicker where the scrollbar might briefly appear or disappear incorrectly.

Defining Our Custom Hook

function useLockBodyScroll() {

We define a standard JavaScript function named useLockBodyScroll. By convention, React custom hooks start with 'use', indicating they are hooks and can contain other hooks (like useLayoutEffect) and stateful logic.

The Heart of the Hook: useLayoutEffect Callback

  useLayoutEffect(() => {

Inside our custom hook, we call useLayoutEffect. It takes a function as its first argument, which is the effect callback. This callback will contain the logic to lock the body scroll.

Preserving Original Scroll Behavior

    const originalStyle = window.getComputedStyle(document.body).overflow;

Before we modify the document.body‘s overflow property, it’s crucial to capture its current, original value. window.getComputedStyle(document.body).overflow retrieves the currently applied (computed) overflow style of the body element. We store this in originalStyle so we can restore it later.

Applying the Scroll Lock

    document.body.style.overflow = 'hidden';

Here, we directly manipulate the DOM. We set the overflow style of the document.body to 'hidden'. This CSS property prevents content from overflowing its container and, in the case of the body, effectively disables scrolling for the entire page.

The Crucial Cleanup Function

    return () => document.body.style.overflow = originalStyle;

The function returned by useLayoutEffect‘s callback is its cleanup function. This function runs when the component using the hook unmounts, or before the effect runs again if its dependencies change (though in our case, dependencies are empty). The cleanup function is critical for reversing the side effect. Here, we restore the document.body‘s overflow style to its originalStyle, ensuring that scrolling is re-enabled when the modal closes or the component using the hook is no longer rendered.

Dependency Array: Ensuring Single Execution

  }, []);

The second argument to useLayoutEffect is an optional dependency array. An empty array [] tells React that this effect should only run once after the initial render and that its cleanup function should run only once when the component unmounts. This is ideal for our scroll lock, as we only need to set and reset the overflow property once per mount/unmount cycle of the component that uses the hook.

Execution Environment and Lifecycle

When Does It Run?

When a component that calls useLockBodyScroll mounts:

  1. React renders the component.
  2. React updates the actual DOM.
  3. useLayoutEffect‘s callback runs synchronously, before the browser paints. It saves the original overflow style and then sets document.body.style.overflow = 'hidden'.
  4. The browser performs layout and paints the screen with the body scroll locked.

The Cleanup Mechanism

When the component that calls useLockBodyScroll unmounts:

  1. React detects the component is being removed.
  2. useLayoutEffect‘s cleanup function runs. It restores document.body.style.overflow to its originalStyle.
  3. The browser re-enables scrolling for the body.

This ensures that the scroll lock is active precisely when needed and correctly removed afterwards, preventing side effects from lingering.

How to Integrate useLockBodyScroll into Your Components

Using this custom hook is straightforward. You simply call it within any functional component that needs to lock the body scroll. Typically, you’d use it in conjunction with a conditional render, such as when a modal is open.

import React, { useState } from 'react';import useLockBodyScroll from './useLockBodyScroll'; // Assuming your hook is in this filefunction Modal({ isOpen, onClose, children }) {  if (isOpen) {    useLockBodyScroll(); // Activate the scroll lock when modal is open  }  return isOpen ? (    
{children}
) : null;}function App() { const [isModalOpen, setIsModalOpen] = useState(false); return (

Main Content

Scrollable content here...

setIsModalOpen(false)}>

Modal Title

This is the content of the modal. It should prevent body scrolling.

More content to make it scrollable if needed.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

);;}export default App;

In this example, the useLockBodyScroll() hook is called conditionally within the Modal component. When isOpen is true, the hook is active, and the body scroll is locked. When isOpen becomes false, the Modal component unmounts (or the hook is no longer called), triggering the cleanup function to restore the body’s original scroll behavior.

💡 Developer Tip: When locking body scroll for modals or overlays, ensure your modal content itself is scrollable if it exceeds the viewport height. Apply overflow-y: auto; to the modal’s inner content container to maintain accessibility and usability for larger content blocks.

Leave a Reply

Your email address will not be published. Required fields are marked *