Editor’s note: This article was last updated on 24 August 2023 to update code blocks according to React 18, the latest React version at the time of writing.
The introduction of custom React Hooks has transformed how developers organize their code logic. It enables them to create manageable code by separating stateful logic from components. This facilitates sharing and reusing logic, which eliminates the need to juggle it across components. Moreover, in the evolving landscape of web interfaces, the ability to create React components holds immense importance to ensure adaptability and a cohesive user experience.
In this article, we are going to review some useful examples of custom React Hooks and leverage them to build a resizable React component. If you are not familiar with React Hooks, check out this cheat sheet.
Jump ahead:
useDebouncedFn
and useThrottleFn
Custom React Hooks have become an integral part of React development, revolutionizing how we organize and share code in applications. These Hooks allow developers to abstract functionalities, making them reusable across components and reducing code repetition. This follows the DRY (Don’t Repeat Yourself) principle and promotes a consistent codebase.
Custom Hooks also allow for a separation between logic and user interface concerns. This not only enhances code maintainability, but it also simplifies the process of sharing stateful logic between components. Gone are the days of patterns like render props or higher order components; custom Hooks provide a straightforward alternative.
Custom Hooks gather logic in one place, making it easier to isolate and test. Even when logic and UI are closely connected, the logic stays distinct within the Hook. This allows developers to focus on testing functionalities without getting caught up in UI-related complexities.
Custom Hooks also streamline the integration of third party libraries. They serve as a layer that encapsulates integration logic, ensuring the proper management and isolation of any associated side effects from the rest of the component logic.
Lastly, named custom Hooks contribute to code readability by providing immediate insight into their functionality. For example, a Hook named useLocalStorage
clearly indicates its purpose to any developer reviewing the code.
Custom React Hooks provide developers with an organized and effective approach to handle state, side effects, and logic, ensuring an optimized experience during the development process.
React developers often encounter the challenge of making sure their components can adjust smoothly and responsively to screen or container sizes. There are two approaches that offer adaptability: event listeners and Hooks.
resize
eventJavaScript provides a core feature called the resize
event, which occurs whenever a document’s viewport size changes. In React, developers typically make use of this event by adding an event listener.
The listener function then updates the components’ state with the correct dimensions by using React’s useState
and useEffect
Hooks. It is important to remember to remove the event listener when unmounting the component in order to ensure memory usage and avoid performance issues.
beautiful-react-hooks
libraryFor developers looking for a different approach, the beautiful-react-hooks
library offers a set of utilities designed specifically for handling events in a more React-focused manner. One such utility is the useGlobalEvent
Hook, which simplifies attaching global event listeners. When it comes to resizing, this Hook can be used to listen for the resize
event. Once triggered, developers can update their component’s state with the window dimensions.
Both of these strategies highlight React’s core philosophy, which is to create responsive UI components that adapt to changes in their surroundings. These changes can be triggered by user interactions or adjustments made to the viewport. However, it’s important to exercise caution when dealing with event-driven processes in JavaScript.
Frequent window resizes, without any throttling mechanism in place, can negatively impact performance. Therefore, developers should prioritize optimizing their listeners and callbacks to ensure a good user experience regardless of the chosen approach.
In the following sections, we are going to build a simple React component that uses some of the custom Hooks from the beautiful-react-hooks
library. We’ll review these custom Hooks individually, and combine everything together when building our component. Because React classes are now deprecated, this example will be constructed using React functional components.
Our resizable component will be able to display a dynamic list of elements that get truncated if their total list’s width is bigger than the current window’s width. In case the list is truncated, we want to show the user how many remaining items are in the list. The final result might look something like this:
In this section, we’ll explore three distinct methods to handle the resize
event in a React application:
useGlobalEvent
HookuseWindowResize
HookuseEffect
HookuseGlobalEvent
The useGlobalEvent
Hook offers a method for connecting global events to the window object. When you provide the name of an event, the Hook listens for that event on the window object, and returns a handler setter as demonstrated in the example with onWindowResize
.
It’s important to remember that the handler should not operate asynchronously and shouldn’t directly trigger a component re-render. Instead, by using useState
to set a state in the component, it will appropriately respond to changes in window size.
Check it out in the code example below, or on CodeSandbox:
import { useState, SyntheticEvent } from "react"; import useGlobalEvent from "beautiful-react-hooks/useGlobalEvent"; import * as React from "react"; const App = () => { const [windowWidth, setWindowWidth] = useState(window.innerWidth); const onWindowResize = useGlobalEvent("resize"); onWindowResize((event: SyntheticEvent) => { setWindowWidth(window.innerWidth); }); return ( <div className="toast toast-primary"> Current window width: {windowWidth} </div> ); }; export default App;
useWindowResize
Expanding on the previous example, there is an additional custom Hook called useWindowResize
that offers a more efficient solution. When used with the useGlobalEvent
function, the useWindowResize
Hook removes the requirement of initializing the event listener.
Check out this example in the code below, or on CodeSandbox:
import * as React from "react"; import { useWindowResize } from "beautiful-react-hooks"; import { useState } from "react"; const App = () => { const [width, setWidth] = useState<number>(window.innerWidth); const setWindowWidth = () => { setWidth(window.innerWidth); }; useWindowResize((event: React.SyntheticEvent) => { window.addEventListener("resize", setWindowWidth); return () => { window.removeEventListener("resize", setWindowWidth); }; }); return ( <div className="toast toast-primary">Current window width: {width}</div> ); }; export default App;
useEffect
Let’s now dive into window resizing using the useEffect
Hook and a functional component. In the example below, observe that the custom Hooks mentioned earlier take care of automatic cleanup before the next component is rendered again:
import * as React from "react"; // interface interface IProps {} const App: React.FC<IProps> = () => { const [width, setWidth] = React.useState<number>(window.innerWidth); const updateWindowWidth = () => { setWidth(window.innerWidth); }; React.useEffect(() => { window.addEventListener("resize", updateWindowWidth); // Cleanup on component unmount return () => { window.removeEventListener("resize", updateWindowWidth); }; }, []); // Empty dependency array ensures this effect runs once on mount and cleanup on unmount return ( <div className="toast toast-primary">Current window width: {width}</div> ); }; export default App;
You can also see this example on CodeSandbox.
So far, we have managed to set a handler for the resize events
, which will help us build our component. But what if we want to optimize the above examples?
useDebouncedFn
and useThrottleFn
In the previous section, we explored effective approaches to handle window resizing. However, it’s important to consider performance degradation when event handlers are executed frequently — especially for events that occur repeatedly, like resizing. To address this issue, we will explore techniques such as debouncing and throttling. These techniques are used to regulate the frequency at which a function is executed.
You might have noticed that in the window resize example above, we are calling the setWindowWidth
for every resize
event that is handled in the event loop. We might need to handle setWindowWidth
less often, which can improve rendering performance. We can do this with the help of useDebouncedCallback
and useThrottledCallback
, which delay the execution of the setWindowWidth
function over time.
When we talk about debouncing the execution of a function, we are trying to batch multiple function calls into a single one to improve performance. This way, when the user is changing the window’s width, we make sure to batch all of the calls to setWindowWidth
into a single one every 0.25 seconds. Debouncing prevents resize
events from being executed too many times (check the console.log
value in the code below, and on CodeSandbox to compare it with the throttle
example below).
Here is an example using the custom useDebouncedCallback
Hook:
import * as React from "react"; import useGlobalEvent from "beautiful-react-hooks/useGlobalEvent"; import useDebouncedCallback from "beautiful-react-hooks/useDebouncedCallback"; // initalization const { useState } = React; const App = () => { const [windowWidth, setWindowWidth] = useState<number>(window.innerWidth); const onWindowResize = useGlobalEvent("resize"); const onWindowResizeHandler = useDebouncedCallback(() => { console.log("I am debouncing", windowWidth); setWindowWidth(window.innerWidth); },[250]); onWindowResize(onWindowResizeHandler); return ( <div className="toast toast-primary"> Current window width: {windowWidth} </div> ); }; export default App;
Throttling guarantees that a function won’t be called more often than a specified rate. For example, if we set a throttle for a function to execute once every 0.25 seconds — even if the event triggering that function occurs rapidly (like window resizing or mouse movement) — the function won’t be called more frequently than every 0.25 seconds. It’s like setting a timer; regardless of how many times the event happens, the function will execute at fixed intervals.
Throttling ensures that a function is executed consistently and at a controlled pace while debouncing ensures that a function is triggered after a brief pause following rapid events:
// global dependencies import * as React from "react"; import useThrottledCallback from "beautiful-react-hooks/useThrottledCallback"; import useGlobalEvent from "beautiful-react-hooks/useGlobalEvent"; const { useState } = React; const App = () => { const [windowWidth, setWindowWidth] = useState(window.innerWidth); const onWindowResize = useGlobalEvent("resize"); const onWindowResizeHandler = useThrottledCallback(() => { console.log("I am throttling", windowWidth); setWindowWidth(window.innerWidth); }, [250]); onWindowResize(onWindowResizeHandler); return ( <div className="toast toast-primary"> Current window width: {windowWidth} </div> ); }; export default App;
Finally, let’s look at debouncing in the context of lifecycle methods. In our example, we are going to use the lodash.debounce
library. All we need to do is to debounce our call to setWindowWidth
when listening to the resize
event in the useEffect
Hook:
import * as React from "react"; import _debounce from "lodash.debounce"; React.useEffect(() => { const handleResize = _debounce(() => { setWidth(window.innerWidth); }, 250); window.addEventListener("resize", handleResize); });
Here is the full example. You can also check it out on CodeSandbox:
import * as React from "react"; import _debounce from "lodash.debounce"; interface IProps {} const App: React.FC<IProps> = () => { const [width, setWidth] = React.useState<number>(window.innerWidth); React.useEffect(() => { const handleResize = _debounce(() => { setWidth(window.innerWidth); }, 250); window.addEventListener("resize", handleResize); return () => { window.removeEventListener("resize", handleResize); }; }, []); return ( <div className="toast toast-primary">Current window width: {width}</div> ); }; export default App;
The final code for our resizable component looks something like the following code. You can also preview it on CodeSandbox:
import React, { useState, useEffect, useRef } from "react"; import _debounce from "lodash.debounce"; const integerGenerator = (n: number): number => Math.ceil(Math.random() * n); const dynamicDataCount = integerGenerator(100); const mockedData = ( ref: React.RefObject<HTMLElement> ): React.ReactElement[] => { const data: React.ReactElement[] = []; for (let i = 0; i < dynamicDataCount; i++) { const image = ( <figure ref={ref} className="avatar mr-2" data-initial="..." key={i}> <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/f/fc/Sun_icon.svg/640px-Sun_icon.svg.png" alt="Sun Icon" /> </figure> ); data.push(image); } return data; }; const App: React.FC = () => { const ref = useRef<HTMLElement>(null); const [windowWidth, setWindowWidth] = useState<number>(window.innerWidth); const [elementWidth, setElementWidth] = useState<number>(0); useEffect(() => { const handleResize = _debounce(() => { setWindowWidth(window.innerWidth); }, 250); window.addEventListener("resize", handleResize); if (ref.current) { setElementWidth(ref.current.offsetWidth); } return () => { window.removeEventListener("resize", handleResize); }; }, []); const handleView = (items: React.ReactElement[]): React.ReactElement[] => { const margin = 8; let maxItemsToShow = 0; if (windowWidth && elementWidth) { maxItemsToShow = Math.floor(windowWidth / (elementWidth + margin)); } if (items.length <= maxItemsToShow) { return items; } const moreDataPlaceholder = 1; const truncatedItems = items.slice(0, maxItemsToShow - moreDataPlaceholder); return truncatedItems; }; const remainingItems = dynamicDataCount - handleView(mockedData(ref)).length; return ( <div className="container"> <div className="icons px-0 mx-0"> {handleView(mockedData(ref)).map((element) => element)} </div> {remainingItems > 0 && ( <div className="displayNumber">{remainingItems}</div> )} </div> ); }; export default App;
In this tutorial, we explored ways to make resizable React components using custom React Hooks. We began by understanding the advantages of custom Hooks, such as reusing code, separating concerns, and improving testability. Then we delved into implementing the resizing functionality for React components using strategies like the native window resize
event and utilities from the beautiful-react-hooks
library.
We also examined three methods for handling window resizes; using useGlobalEvent
, useWindowResize
, and functional components with the useEffect
Hook.
Finally, we addressed optimizing performance using techniques like debouncing and throttling. These methods ensure that handling events don’t negatively impact our application’s performance.
I hope this tutorial serves as a guide for anyone interested in building resizable React components. With the availability of custom Hooks, you have a range of options to explore in order to develop code that is efficient, easy to maintain, and can be reused extensively.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowJavaScript’s Date API has many limitations. Explore alternative libraries like Moment.js, date-fns, and the new Temporal API.
Explore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.
Build a real-time image background remover in Vue using Transformers.js and WebGPU for client-side processing with privacy and efficiency.