Building resizable React component using custom React Hooks
Tags: react, webdev
Published: 2020-05-12 20:55:51

---
title: Building resizable React component using custom React Hooks
published: true
date: 2020-04-17 13:00:15 UTC
tags: react,webdev
canonical_url: https://blog.logrocket.com/building-resizable-react-component-using-custom-react-hooks/
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/16skkb3kv7d2cbuuelcp.png
---
**Written by [Kasra Khosravi](https://blog.logrocket.com/author/kasrakhosravi/)**✏️

## Custom Hooks

We are going to talk about some [cool examples of custom React Hooks](https://github.com/beautifulinteractions/beautiful-react-hooks) and build a resizable React component using them. If you are not familiar with the concept of Hooks, please review the [Hook’s basic definitions](https://reactjs.org/docs/hooks-intro.html) and [rules](https://reactjs.org/docs/hooks-rules.html) before continuing this article.

Hooks allow us to think in new ways. Now, we can group certain parts of React component’s logic related to a certain context (like fetch data or page events) into custom React Hooks. This is happening without the need to refactor the components hierarchy that needs to share a state with each other. Also, we do not need the verbosity of repeating and using unrelated logic in lifecycle methods.

## Resizable React component example

We are going to build a simple React component together that uses some of the custom Hooks from [`beautiful-react-hooks`](https://github.com/beautifulinteractions/beautiful-react-hooks) library. We will cover these custom Hooks individually and glue everything together in the end to build our component. As a learning exercise, we will also build these examples using [React class](https://github.com/beautifulinteractions/beautiful-react-hooks) and [lifecycle methods](https://github.com/beautifulinteractions/beautiful-react-hooks) to see what benefits we might gain by using Hooks.

As an example, this component would display a dynamic list of elements that get [truncated](https://www.hacksparrow.com/javascript/truncating-an-array.html), if their total list’s width is bigger than the current window’s width. In case the list gets truncated, we want to show the user how many remaining items are in the list. The final result could look something like this:

![Image Source: Assets in https://picturepan2.github.io/spectre/](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2020/04/reactcomponent.png?resize=1090%2C96&ssl=1)<figcaption id="caption-attachment-17242">Image Source: Assets in <a href="https://picturepan2.github.io/spectre/" rel="nofollow">https://picturepan2.github.io/spectre/</a></figcaption>

Let’s get started.

[![LogRocket Free Trial Banner](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2017/03/f760c-1gpjapknnuyhu8esa3z0jga.png?resize=1200%2C280&ssl=1)](https://logrocket.com/signup/)

## useGlobalEvent and useWindowResize

To build our component, we need a mechanism for listening and reacting to [`resize event`[(https://developer.mozilla.org/en-US/docs/Web/API/Window/resize_event) in the context of [`global window object`](https://developer.mozilla.org/en-US/docs/Web/API/Window#Event_handlers). As it turns out, there is a very useful custom Hook called [`useGlobalEvent`](https://github.com/beautifulinteractions/beautiful-react-hooks/blob/master/docs/useGlobalEvent.md) which can help us. You just pass an event’s name and the Hook adds a listener for that event on the window object. It returns a handler setter (`onWindowResize` in the below example) for it, which is immediately invoked.

Keep in mind that this handler should not be run asynchronously and it does not cause the component to re-render. We are making sure the component responds to `resize` changes by setting new state in the component using [`useState`](https://reactjs.org/docs/hooks-state.html). This way, the handler setter which is a reference to the Hook will be called again, with a new `windowWidth` state.

After the component is unmounted, we need to clean up after ourselves by removing the event listeners that were attached. But why is that?

Remember that after each re-render caused by `setWindowWidth` and new `windowWidth`, we are calling our Hook again. This will cause `n` number of bindings to the `resize event` which can cause memory leaks in our application. `useGlobalEvent` takes care of this for us, by removing the event handler of new re-renders.

Here is [an example](https://github.com/beautifulinteractions/beautiful-react-hooks/blob/master/docs/useGlobalEvent.md) of using `useGlobalEvent` Hook:

```jsx
// global dependencies
import * as React from "react";
import { useGlobalEvent } from "beautiful-react-hooks";

// initalization
const { useState } = React;

const App = () => {
  const [windowWidth, setWindowWidth] = useState(window.innerWidth);
  const onWindowResize = useGlobalEvent("resize");

  onWindowResize((event: React.SyntheticEvent) => {
    setWindowWidth(window.innerWidth);
  });

  return (
    <div className="toast toast-primary">
      Current window width: {windowWidth}
    </div>
  );
};

export default App;
```

{% codesandbox okclu %}

Here is an example of another custom Hook [`useWindowResize`](https://github.com/beautifulinteractions/beautiful-react-hooks/blob/master/docs/useWindowResize.md), built on top of `useGlobalEvent` which makes the component even simpler:

```jsx
// global dependencies
import * as React from "react";
import { useWindowResize } from "beautiful-react-hooks";

// initalization
const { useState } = React;

const App = () => {
  const [windowWidth, setWindowWidth] = useState(window.innerWidth);

  useWindowResize((event: React.SyntheticEvent) => {
    setWindowWidth(window.innerWidth);
  });

  return (
    <div className="toast toast-primary">
      Current window width: {windowWidth}
    </div>
  );
};

export default App;
```

{% codesandbox okclu %}

Here is the example using class and React lifecycle methods. This is a simple example, but you can see that the above custom React Hook takes care of cleaning up automatically before the next component re-render. This is something we need to cover in React lifecycle methods manually:

```jsx
// global dependencies
import * as React from "react";

// interface
interface IProps {}

interface IState {
  width?: number;
}

class App extends React.Component<IProps, IState> {
  constructor(props: any) {
    super(props);
    this.state = {
      width: window.innerWidth
    };
  }

  // local methods
  setWindowWidth = () => {
    this.setState({
      width: window.innerWidth
    });
  };

  // lifecycle methods
  componentDidMount() {
    window.addEventListener("resize", this.setWindowWidth);
  }

  componentWillUnmount() {
    window.removeEventListener("resize", this.setWindowWidth);
  }

  render() {
    return (
      <div className="toast toast-primary">
        Current window width: {this.state.width}
      </div>
    );
  }
}

export default App;

```

{% codesandbox ecwrf %}

So far, we have managed to set a handler for the `resize events` which will help us to build our component. But first, is there any optimization we can do for the above examples?

## useDebouncedFn and useThrottleFn

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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop). We might need to handle `setWindowWidth` less often which can gain us some rendering performance. We can do this with the help of [`useDebouncedFn`](https://github.com/beautifulinteractions/beautiful-react-hooks/blob/master/docs/useDebouncedFn.md) and [`useThrottleFn`](https://github.com/beautifulinteractions/beautiful-react-hooks/blob/master/docs/useThrottledFn.md), to delay the execution of `setWindowWidth` function over time.

## Debouncing

When talking [debouncing the execution of a function](https://css-tricks.com/debouncing-throttling-explained-examples/), we are trying to batch multiple function calls into a single one to improve performance. In this way, when the user is changing the window’s width, we are making sure to batch all of the calls to the `setWindowWidth` into a single one for every **0.25 seconds**. If the `resize events` are happening fast and rapidly, debouncing takes place; otherwise not (check the `console.log` value in the sandbox below and compare it with `throttle` example below).

Here is an example using this custom Hook:

```jsx
// global dependencies
import * as React from "react";
import { useGlobalEvent, useDebouncedFn } from "beautiful-react-hooks";

// initalization
const { useState } = React;

const App = () => {
  const [windowWidth, setWindowWidth] = useState(window.innerWidth);
  const onWindowResize = useGlobalEvent("resize");

  const onWindowResizeHandler = useDebouncedFn(() => {
    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;
```

{% codesandbox 9rmvq %}

## Throttling

[The throttling concept](https://css-tricks.com/debouncing-throttling-explained-examples/), even though it is similar to `debounce`, has its differences. For example with `throttle`, you do not allow the execution of `setWindowWidth` more than once every 0.25 seconds. However, the regular execution of function is guaranteed every 0.25 seconds.

Check this scenario by checking the `console.log` in the below example:

```jsx
// global dependencies
import * as React from "react";
import { useGlobalEvent, useThrottledFn } from "beautiful-react-hooks";

// initalization
const { useState } = React;

const App = () => {
  const [windowWidth, setWindowWidth] = useState(window.innerWidth);
  const onWindowResize = useGlobalEvent("resize");

  const onWindowResizeHandler = useThrottledFn(() => {
    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 see `debouncing` in the context of using lifecycle methods. We are gonna use [`lodash.debounce`](https://www.npmjs.com/package/lodash.debounce). All we need to do is to `debounce` our call to `setWindowWidth` when listening to `resize event` in [`componentDidMount`](https://reactjs.org/docs/react-component.html#componentdidmount):

```jsx
import _debounce from "lodash.debounce";

componentDidMount() {
    window.addEventListener(
      "resize",
      _debounce(() => {
        this.setWindowWidth();
      }, 250)
    );
  }
```

Here is the full example:

```jsx
// global dependencies
import * as React from "react";
import _debounce from "lodash.debounce";

// interface
interface IProps {}

interface IState {
  width?: number;
}

class App extends React.Component<IProps, IState> {
  constructor(props: any) {
    super(props);
    this.state = {
      width: window.innerWidth
    };
  }

  // local methods
  setWindowWidth = () => {
    this.setState({
      width: window.innerWidth
    });
  };

  // lifecycle methods
  componentDidMount() {
    window.addEventListener(
      "resize",
      _debounce(() => {
        this.setWindowWidth();
      }, 250)
    );
  }

  componentWillUnmount() {
    window.removeEventListener("resize", this.setWindowWidth);
  }

  render() {
    return (
      <div className="toast toast-primary">
        Current window width: {this.state.width}
      </div>
    );
  }
}

export default App;
```

{% codesandbox vuzhr %}

## Final result

So far, we have set a debounced handler to listen for `resize events` and set the `windowWidth` state. Now, we are gonna bring everything together to build the resizable React component we have described in the beginning of the article. A few things to note:

- The number of items we want to show is dynamic, meaning that it will be different on each initial render. This would require us to show a certain number of items in a row after debounced `resize events` are fired
- We utilize some of the Hooks that are already integrated into [React library (from 16.8)](https://reactjs.org/docs/hooks-effect.html). You are probably already familiar with them, but here is a short description from the [official documentation](https://reactjs.org/docs/hooks-reference.html):
  - [`useState`](https://reactjs.org/docs/hooks-reference.html#usestate) returns a stateful value, and a function to update it
  - [`useEffect`](https://reactjs.org/docs/hooks-reference.html#useeffect) accepts a function that contains imperative, possibly effectful code
  - [`useRef`](https://reactjs.org/docs/hooks-reference.html#useeffect) returns a mutable ref object whose `.current` property is initialized to the passed argument (`initialValue`)
- We use mocked `dynamicData` and its total number to mimic the behaviour of an API call:

```jsx
// helpers
const integerGenerator = (n: number) => Math.ceil(Math.random() * n);

// faking a dynamic data count which in real life
// scenario would come from an api endpoint
const dynamicDataCount = integerGenerator(100);

// data mocks
const mockedData =  () => {
  const data = [];
  for (let i = 0; i < dynamicDataCount; i++) {
    const image : any = (
      <figure className="avatar mr-2" data-initial="...">
        <img src="https://picturepan2.github.io/spectre/img/avatar-1.png" alt="YZ" />
      </figure>
    );
    data.push(image);
  };
  return data;
};

// this would generate an array of mockedData
// elements with a length of random dynamicDataCount
mockedData();
```

- We built a `handleView` function to calculate the total number of items that are possible to be shown. If their total width exceeds `window width`, we attach a new element to the list of items which shows how many items are hidden from view. If not, we just return the list of items. The idea of this helper was formed after reading [this article](https://medium.com/hootsuite-engineering/resizing-react-components-6f911ba39b59). Make sure to check it out for another perspective:

```jsx
const handleView = (items: Array<Element>) => {  
    // 8 is the value of margin right applied to image elements (8px)
    var maxItemsToShow = Math.floor(windowWidth / (elementWidth + 8));

    // return current view if total number of items is less than maximum possible
    // number of items that can be shown based on the current window width
    if (items.length <= maxItemsToShow) {
      return items;
    }

    // if not, we need a new element which shows how many more items are in the list that are now shown
    const moreDataPlaceholder = 1;
    const numberOfRemainingItems = items.length - maxItemsToShow + moreDataPlaceholder;
    const truncatedItems = items.slice(0, maxItemsToShow - moreDataPlaceholder);
    const displayNumberHtml : any = (
      <figure className="avatar badge" data-badge={numberOfRemainingItems} data-initial="..." />
    );

    truncatedItems.push(displayNumberHtml);
    return truncatedItems;
  }
```

The final code looks something like this:

```jsx
// global dependencies
import * as React from "react";
import { useGlobalEvent, useDebouncedFn } from "beautiful-react-hooks";

// initalization
const { useState, useRef, useEffect } = React;

// helpers
const integerGenerator = (n: number) => Math.ceil(Math.random() * n);

// faking a dynamic data count which in real life 
// scenario would come from an api endpoint
const dynamicDataCount = integerGenerator(100);

// data mocks
const mockedData =  (ref: any) => {
  const data = [];
  for (let i = 0; i < dynamicDataCount; i++) {
    const image : any = (
      <figure ref={ref} className="avatar mr-2" data-initial="...">
        <img src="https://picturepan2.github.io/spectre/img/avatar-1.png" alt="YZ" />
      </figure>
    );
    data.push(image);
  };
  return data;
};

const App = () => {
  // component initialization
  const ref = useRef<HTMLInputElement>(null);
  const [windowWidth, setWindowWidth] = useState(window.innerWidth);
  const [elementWidth, setElementWidth] = useState(0);
  const onWindowResize = useGlobalEvent("resize");

  // handler for initially calculating individual elements width 
  useEffect(() => {
    const width = ref.current ? ref.current.offsetWidth : 0;
    setElementWidth(width);
  }, []);

  // handler for calculating window width on resize event
  const onWindowResizeHandler = useDebouncedFn(() => {
    setWindowWidth(window.innerWidth);
  }, 250);
  onWindowResize(onWindowResizeHandler);

  const handleView = (items: Array<Element>) => {  
    // 8 is the value of margin right applied to image elements (8px)
    var maxItemsToShow = Math.floor(windowWidth / (elementWidth + 8));

    // return current view if total number of items is less than maximum possible
    // number of items that can be shown based on the current window width
    if (items.length <= maxItemsToShow) {
      return items;
    }
  
    // if not, we need a new element which shows how many more items are in the list that are now shown
    const moreDataPlaceholder = 1;
    const numberOfRemainingItems = items.length - maxItemsToShow + moreDataPlaceholder;
    const truncatedItems = items.slice(0, maxItemsToShow - moreDataPlaceholder);
    const displayNumberHtml : any = (
      <figure className="avatar badge" data-badge={numberOfRemainingItems} data-initial="..." />
    );
  
    truncatedItems.push(displayNumberHtml);
    return truncatedItems;
  }
  
  return (
    <div className="toast toast-primary px-0 mx-0">
      {handleView(mockedData(ref)).map((element : Element) => element)}
    </div>
  );
};

export default App;
```

{% codesandbox bpk03 %}

Now it is time to see this example, using class and lifecycle method. At first glance, you see the lifecycle methods like `componentDidMount` gets a bit more complicated. It is because the logic of class components is about grouping side effect management in different phases of component lifecycle, rather than basing them on individual effects (like setting the `window width` and individual `element width`):

```jsx
// global dependencies
import * as React from "react";
import _debounce from "lodash.debounce";

// helpers
const integerGenerator = (n: number) => Math.ceil(Math.random() * n);

// faking a dynamic data count which in real life
// scenario would come from an api endpoint
const dynamicDataCount = integerGenerator(100);

// data mocks
const mockedData = (ref: any) => {
  const data = [];
  for (let i = 0; i < dynamicDataCount; i++) {
    const image: any = (
      <figure ref={ref} className="avatar mr-2" data-initial="...">
        <img
          src="https://picturepan2.github.io/spectre/img/avatar-1.png"
          alt="YZ"
        />
      </figure>
    );
    data.push(image);
  }
  return data;
};

// interface
interface IProps {}

interface IState {
  windowWidth?: number;
  elementWidth?: number;
}

class App extends React.Component<IProps, IState> {
  private ref = React.createRef<HTMLDivElement>();

  constructor(props: any) {
    super(props);
    this.state = {
      windowWidth: window.innerWidth,
      elementWidth: 0
    };
  }

  // local methods
  setWindowWidth = () => {
    this.setState({
      windowWidth: window.innerWidth
    });
  };

  setElementWidth = (elementWidth: number) => {
    this.setState({
      elementWidth: elementWidth
    });
  };

  // lifecycle methods
  componentDidMount() {
    const elementWidth = this.ref.current ? this.ref.current.offsetWidth : 0;
    this.setElementWidth(elementWidth);
    window.addEventListener(
      "resize",
      _debounce(() => {
        this.setWindowWidth();
      }, 250)
    );
  }

  componentWillUnmount() {
    window.removeEventListener("resize", this.setWindowWidth);
  }

  handleView = (items: Array<Element>) => {
    // 8 is the value of margin right applied to image elements (8px)
    let maxItemsToShow = 0;

    if (this.state.windowWidth && this.state.elementWidth) {
      maxItemsToShow = Math.floor(
        this.state.windowWidth / (this.state.elementWidth + 8)
      );
    }

    // return current view if total number of items is less than maximum possible
    // number of items that can be shown based on the current window width
    if (items.length <= maxItemsToShow) {
      return items;
    }

    // if not, we need a new element which shows how many more items are in the list that are now shown
    const moreDataPlaceholder = 1;
    const numberOfRemainingItems =
      items.length - maxItemsToShow + moreDataPlaceholder;
    const truncatedItems = items.slice(0, maxItemsToShow - moreDataPlaceholder);
    const displayNumberHtml: any = (
      <figure
        className="avatar badge"
        data-badge={numberOfRemainingItems}
        data-initial="..."
      />
    );

    truncatedItems.push(displayNumberHtml);
    return truncatedItems;
  };

  render() {
    return (
      <div className="toast toast-primary px-0 mx-0">
        {this.handleView(mockedData(this.ref)).map(
          (element: Element) => element
        )}
      </div>
    );
  }
}

export default App;
```

{% codesandbox 0xo2r %}

## Conclusion

Let’s review what we have learned together:

- Together we built a simple React component that adapts to different window widths sizes and shows a dynamic number of items. We also learned how to optimize this process by delaying function calls to our event handlers.
- We saw, in action, how Hooks can make building components easier and how [custom Hooks](https://beautifulinteractions.github.io/beautiful-react-hooks/) can make that even smoother. But changing direction and deciding to write or re-write components using Hooks is not very straightforward. Before making any decision, make sure to read [React’s official adaptation guide](https://beautifulinteractions.github.io/beautiful-react-hooks/). And remember to experiment more with this new concept to get more informed about its advantages and disadvantage.

## References

[https://reactjs.org/docs/hooks-intro.html](https://reactjs.org/docs/hooks-intro.html)

[https://github.com/beautifulinteractions/beautiful-react-hooks](https://github.com/beautifulinteractions/beautiful-react-hooks)

[https://css-tricks.com/debouncing-throttling-explained-examples/](https://css-tricks.com/debouncing-throttling-explained-examples/)

[https://www.pluralsight.com/guides/re-render-react-component-on-window-resize](https://www.pluralsight.com/guides/re-render-react-component-on-window-resize)

[https://medium.com/hootsuite-engineering/resizing-react-components-6f911ba39b59](https://medium.com/hootsuite-engineering/resizing-react-components-6f911ba39b59)

* * * 
## Full visibility into production React apps 

Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, [try LogRocket.](https://www2.logrocket.com/react-performance-monitoring)

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/eq752g8qhbffxt3hp9t4.png)

[LogRocket](https://www2.logrocket.com/react-performance-monitoring) is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — [start monitoring for free.](https://www2.logrocket.com/react-performance-monitoring)
* * *

The post [Building resizable React component using custom React Hooks](https://blog.logrocket.com/building-resizable-react-component-using-custom-react-hooks/) appeared first on [LogRocket Blog](https://blog.logrocket.com).