With the introduction of React Hooks, the number of shareable code in the React codebase has exploded. Because Hooks are the thin API on top of React, developers can collaborate by attaching reusable behavior to components and isolating those behaviors into smaller modules.

While this is similar to a JavaScript developer abstracting business logic from a normal JavaScript module, Hooks offer much more than pure JavaScript functions. Instead of bringing data in and out, developers can extend the possibilities of what happens inside the Hook.

For example, developers can.

  • Mutate and manage a state for a particular component or an entire application
  • Trigger side effects on the page, such as changing the title of the browser TAB
  • Fix the external API by using Hooks into the React component’s life cycle

In this article, we will explore the latter possibility. As an example, we’ll abstract the MutationObserver API in a custom React Hook to show how we can build powerful, shareable pieces of logic in the React code base.

We will create a dynamic label that can self-update to indicate how many items we have in a list. Instead of using the element state array provided by React, we use the MutationObserver API to detect added elements and update labels accordingly.

Update the dynamic TAB to count the number of fruits in the list.

Implementation of the outline

The code below is a simple component that renders our list. It also updates the value of a counter that represents the number of fruits in the current list.

export default function App() {
  const listRef = useRef();
  const [count, setCount] = useState(2);
  const [fruits, setFruits] = useState(["apple", "peach"]);
  const onListMutation = useCallback(
    (mutationList) => {
      setCount(mutationList[0].target.children.length);
    },
    [setCount]
  );

  useMutationObservable(listRef.current, onListMutation);

  return (
    <div>
      <span>{`Added ${count} fruits`}</span>
      <br />
      <button
        onClick={() => setFruits([...fruits, `random fruit ${fruits.length}`])}
      >
        Add random fruit
      </button>
      <ul ref={listRef}>
        {fruits.map((f) => (
          <li key={f}>{f}</li>
        ))}
      </ul>
    </div>
  );
}

Copy the code

We want to trigger a callback function when our list elements change. In the callback we refer to, the child of the element gives us the number of elements in the list.

implementationuseMutationObservableCustomize the Hook

Let’s look at integration points.

useMutationObservable(listRef.current, onListMutation);

Copy the code

The useMutationObservable custom Hook above abstracts the operations necessary to observe changes to the element passed as the first parameter. It then runs the callback as the second argument whenever the target element changes.

Now let’s implement our useMutationObservable custom Hook.

In hooks, there are some template operations to understand. First, we must provide a set of options that conform to the MutationObserver API.

Once a MutationObserver instance is created, we must call Observe to listen for changes to the target DOM element.

When we no longer need to listen for these changes, we must call Disconnect on the observer to clean up our subscription. This must happen when the App component is uninstalled.

const DEFAULT_OPTIONS = {
  config: { attributes: true, childList: true, subtree: true },
};
function useMutationObservable(targetEl, cb, options = DEFAULT_OPTIONS) {
  const [observer, setObserver] = useState(null);

  useEffect(() => {
    const obs = new MutationObserver(cb);
    setObserver(obs);
  }, [cb, options, setObserver]);

  useEffect(() => {
    if (!observer) return;
    const { config } = options;
    observer.observe(targetEl, config);
    return () => {
      if (observer) {
        observer.disconnect();
      }
    };
  }, [observer, targetEl, options]);
}

Copy the code

All of the above work, including initializing MutationObserver with the correct parameters, observing changes by calling observer.observe, and cleaning up with observer.disconnect, is abstracted from the client.

We not only output functionality, but also clean up the MutationObserver instance by hooking the React component’s lifecycle and utilizing cleanup callbacks on the effect hooks.

Now that we have a functional base version of the Hook, we can consider iterating through its API to improve its quality and enhance the developer experience around this shareable code.

Input validation and development

An important aspect of designing custom React Hooks is input validation. We need to be able to communicate to developers when things aren’t working smoothly or when a use case hits an edge.

Often, development logs help developers learn about unfamiliar code in order to tweak their implementation. Also, we can enhance the above implementation by adding run-time checks and comprehensive warning logs to validate and communicate problems to other developers.

function useMutationObservable(targetEl, cb, options = DEFAULT_OPTIONS) { const [observer, setObserver] = useState(null); useEffect(() => { // A) if (! cb || typeof cb ! == "function") { console.warn( `You must provide a valid callback function, instead you've provided ${cb}` ); return; } const { debounceTime } = options; const obs = new MutationObserver(cb); setObserver(obs); }, [cb, options, setObserver]); useEffect(() => { if (! observer) return; if (! targetEl) { // B) console.warn( `You must provide a valid DOM element to observe, instead you've provided ${targetEl}` ); } const { config } = options; try { observer.observe(targetEl, config); } catch (e) { // C) console.error(e); } return () => { if (observer) { observer.disconnect(); }}; }, [observer, targetEl, options]); }Copy the code

In this example, we check to see if a callback is passed as a second argument. This runtime API checking can easily alert developers to problems on the caller’s side.

We can also see if the supplied DOM element gives the Hook an incorrect or invalid value at run time. These are all recorded together to inform us of a quick solution to the problem.

Also, if Observe throws an error, we can catch and report it. We have to avoid breaking the flow of the JavaScript runtime as much as possible, so by catching errors, we can either log it or report it, depending on the context.

Scalability is achieved through configuration

If we want to add more functionality to our hooks, we should do so in a retroactively compatible way, such as a selection capability, with almost no friction to its adoption.

Let’s see how we can selectively remove the provided callback function, so the caller can specify a time interval when no other changes are triggered in the target element. This allows you to run a single callback instead of running the same number of elements or variations of their children.

import debounce from "lodash.debounce"; const DEFAULT_OPTIONS = { config: { attributes: true, childList: true, subtree: true }, debounceTime: 0 }; function useMutationObservable(targetEl, cb, options = DEFAULT_OPTIONS) { const [observer, setObserver] = useState(null); useEffect(() => { if (! cb || typeof cb ! == "function") { console.warn( `You must provide a valida callback function, instead you've provided ${cb}` ); return; } const { debounceTime } = options; const obs = new MutationObserver( debounceTime > 0 ? debounce(cb, debounceTime) : cb ); setObserver(obs); }, [cb, options, setObserver]); / /...Copy the code

This is handy if we have to run a heavy operation, such as triggering a network request, making sure it runs as few times as possible.

Our debounceTime option can now pass in our custom Hook. If a value greater than 0 is passed to the MutationObservable, the callback is delayed accordingly.

Through simple configuration in our Hook API, we allow other developers to dejitter their callbacks, which may result in a higher performance implementation because we may significantly reduce the number of times the callback code is executed.

Of course, we can always remove callbacks on the client side, but this way we can enrich our API and make the caller’s implementation smaller and more declarative.

test

Testing is an important part of developing any type of shared capability. When a common API is heavily contributed and shared, it helps us ensure a certain level of quality.

The guide for testing React Hooks has a lot of details about testing that can be implemented in this tutorial.

The document

Documentation can improve the quality of custom Hooks and make them developer friendly.

But even when writing plain JavaScript, you can write JSDoc documentation for your custom Hook API to ensure that the Hook communicates the right information to the developer.

Let’s focus on the useMutationObservable function declaration and how to add a formatted JSDoc document to it.

/** * This custom hooks abstracts the usage of the Mutation Observer with React components. * Watch for changes being made to the DOM tree and trigger a custom callback. * @param {Element} targetEl DOM element to be observed * @param {Function} cb callback that will run when there's a change in targetEl or any * child element (depending on the provided  options) * @param {Object} options * @param {Object} options.config check \[options\](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe) * @param {number} [options.debounceTime=0] a number that represents the amount of time in ms * that you which to debounce the call to the provided callback function */ function useMutationObservable(targetEl, cb, options = DEFAULT_OPTIONS) {Copy the code

Writing this is not only useful for documentation, but also leverages IntelliSense functionality to automate the use of hooks and provide in-stock information for Hook parameters. This saves the developer a few seconds per use, potentially adding to the time wasted reading the code and trying to understand it.

conclusion

With the different kinds of custom Hooks we can implement, we’ve seen how they integrate external apis into the React world. It is easy to integrate state management in Hooks and run on input from components that use Hooks.

Remember, to build high-quality Hooks, it is important.

  • Design easy-to-use, declarative apis
  • Enhance the development experience by checking for correct usage and logging warnings and errors
  • By configuring exposure functions such asdebounceTimeAn example of
  • Simplify the use of hooks by writing JSDoc documentation

You can see the full implementation of the custom React Hook here.

The postGuide to custom React Hooks with MutationObserver appearedfirst on LogRocketBlog.