React Hooks — useObserve (use ResizeObserver Custom Hook)

Eyas Mattar
6 min readJun 24, 2020

--

Create your own custom hook article

React Hooks changed the way we write components and changed the way we preserve state in our React components, now we can write components without the need to use React.Component Class in order to maintain a components’ state.

In some use cases, when we write/develop a view layer component, we would like to observe an elements’ size if that element changes dynamically and unexpectedly, so we would like to be notified when that element changes.

The ResizeObserver interface intoroduced by JavaScript, allows us to listen to an Elements’ size.

The ResizeObserver interface reports changes to the dimensions of an Element's content or border box, or the bounding box of an SVGElement.

Let’s say we have a Shape Class Component and we want to observe the shape-text element

import React from "react";
import PropTypes from "prop-types";


class Shape extends React.Component {
render() {
return (
<div>
<div>Shape</div>
<div id="shape-text">{this.props.text}</div>
</div>
);
}
}Shape.propTypes = {
text: PropTypes.string
};
export default Shape;

In order to observe the element we can write this possible solution

import React from "react";
import PropTypes from "prop-types";


class Shape extends React.Component {
constructor(props) {
super(props);
this.state = {
shapeRef: null,
observer: new ResizeObserver(somecallback),
};
}

setRef(ref) {
const { observer } = this.state;
this.state.shapeRef = ref;
if (this.state.shapeRef && this.state.shapeRef.current) {
observer.observe(this.state.shapeRef.current)
}
};

componentWillUnmount() {
const { observer, shapeRef } = this.state;
if (observer && shapeRef && shapeRef.current) {
observer.unobserve(shapeRef.current);
}
}

render() {
return (
<div>
<div>Shape</div>
<div ref={this.setRef} id="shape-text">
{this.props.text}
</div>
</div>
);
}
}
Shape.propTypes = {
text: PropTypes.string
};
export default Shape;
  1. We obtained reference for the element using ref callback
  2. We observe the element using observe function of the observer
  3. We unobserve the element when we Unmount

Let’s say we want to the same but in our functional components.

In our React functional component we will have

import React from "react";
import PropTypes from "prop-types";


function Shape({ text }) {
return (
<div>
<div>Shape</div>
<div id="shape-text">{text}</div>
</div>
);
}

Shape.propTypes = {
text: PropTypes.string
};
export default Shape;

What we want to do is we want to create a reusable logic that we can use in multiple functional components.

more about reusablity, best practices & performance, check my course

The first step of understanding what we want to create, is just simply, to use it first.

import React, { useRef } from "react";
import PropTypes from "prop-types";


function Shape({ text }) {

const shapeRef = useRef(null);

const someCallback = () => {
// logic
};

useResizeObserver({callback: someCallback, element: shapeRef});

return (
<div>
<div>Shape</div>
<div ref={shapeRef} id="shape-text">{text}</div>
</div>
);
}

Shape.propTypes = {
text: PropTypes.string
};
export default Shape;

We want to write a custom Reack Hook useResizeObserver, that we can use in the above way, so whenever we want to observe some element in our functional components, we just use our super Hook.

Writing Our Own Custom Hook

First of all, lets create our function that takes our arguments callback and ref

import {useEffect, useRef} from 'react';
import PropTypes from 'prop-types';

const useObserver = ({ callback, element }) => {


};

useObserver.propTypes = {
element: PropTypes.object,
callback: PropTypes.func,
};

export default useObserver;

When we execute the useObserver, we want to create a ResizeObserver that will observer the element

import {useEffect, useRef} from 'react';
import PropTypes from 'prop-types';
import ResizeObserver from "resize-observer-polyfill";

const useObserver = ({ callback, element }) => {

const observer = useRef(null);

useEffect(() => {
observer.current = new ResizeObserver(callback);
}, [element.current]);

};

useObserver.propTypes = {
element: PropTypes.object,
callback: PropTypes.func,
};

export default useObserver;

We want to create a reference for our observer only once, and we don’t want re-create this reference in every useObserver execution.

More on Hooks

So in order to achieve that we want to use the useRef, and the useRef will achieve that because it maintains the same object reference but with diferrent current property value.

We create our ResizeObserver in our useEffect.

Our useEffect will be executed every time our ref prop changes, so we want to write something like

useEffect(() => {
/// .....
}, [element.current]);

After we created once our ResizeObserver, we want now to listen to our element.

The way we listen to an element using ResizeObserver is by just calling the observe() function

const observe = () => {
if (element && element.current && observer.current) {
observer.current.observe(element.current);
}
};

So our Custom Hook now will look like this

import {useEffect, useRef} from 'react';
import PropTypes from 'prop-types';
import ResizeObserver from "resize-observer-polyfill";

const useObserver = ({ callback, element }) => {

const observer = useRef(null);

useEffect(() => {
const resizeObserverOrPolyfill = ResizeObserver;
observer.current = new resizeObserverOrPolyfill(callback);
observe();
}, [element.current]);

const observe = () => {
if (element && element.current && observer.current) {
observer.current.observe(element.current);
}
};

};

useObserver.propTypes = {
element: PropTypes.object,
callback: PropTypes.func,
};

export default useObserver;

So, our useEffect is now responsible for initializing our Observer and also starting the observing.

Before we continue, let’s protect our code from undefined values

import {useEffect, useRef} from 'react';
import PropTypes from 'prop-types';
import ResizeObserver from "resize-observer-polyfill";

const useObserver = ({ callback, element }) => {

const current = element && element.current;

const observer = useRef(null);

useEffect(() => {
const resizeObserverOrPolyfill = ResizeObserver;
observer.current = new resizeObserverOrPolyfill(callback);
observe();
}, [current]);

const observe = () => {
if (element && element.current && observer.current) {
observer.current.observe(element.current);
}
};

};

useObserver.propTypes = {
element: PropTypes.object,
callback: PropTypes.func,
};

export default useObserver;

Every time our current value will change from outside our hook, we want to observe again but a new/different element.

So we want to unobserve the old one, and observe the new one.

import {useEffect, useRef} from 'react';
import PropTypes from 'prop-types';
import ResizeObserver from "resize-observer-polyfill";

const useObserver = ({ callback, element }) => {

const current = element && element.current;

const observer = useRef(null);

useEffect(() => {
// if we are already observing old element
if (observer && observer.current && current) {
observer.current.unobserve(current);
}

const resizeObserverOrPolyfill = ResizeObserver;
observer.current = new resizeObserverOrPolyfill(callback);
observe();
}, [current]);

const observe = () => {
if (element && element.current && observer.current) {
observer.current.observe(element.current);
}
};

};

useObserver.propTypes = {
element: PropTypes.object,
callback: PropTypes.func,
};

export default useObserver;

Last thing we need to do is we need to do some clean up when our components will UNMOUNT so we don’t cause leaks in ou application, so we don’t want to continue observing our elements.

The way we implement an UNMOUNT logic in React Hooks, is by returning a callback in our useEffect callback

import {useEffect, useRef} from 'react';
import PropTypes from 'prop-types';
import ResizeObserver from "resize-observer-polyfill";

const useObserver = ({ callback, element }) => {

const current = element && element.current;

const observer = useRef(null);

useEffect(() => {
// if we are already observing old element
if (observer && observer.current && current) {
observer.current.unobserve(current);
}
const resizeObserverOrPolyfill = ResizeObserver;
observer.current = new resizeObserverOrPolyfill(callback);
observe();

return () => {
if (observer && observer.current && element &&
element.current) {
observer.current.unobserve(element.current);
}
};

}, [current]);

const observe = () => {
if (element && element.current && observer.current) {
observer.current.observe(element.current);
}
};

};

useObserver.propTypes = {
element: PropTypes.object,
callback: PropTypes.func,
};

export default useObserver;

So, our custom Hooks now can observe any element ref we pass to it.

This implementation can be done in different ways, but this is one way we can do it, and the most important thing is to maintain all the flows, where we render, re-render, change props and unmount our components.

If you want to see more Advanced React Topics & Techniques, you can check my Advanced React Course:

--

--

Responses (2)