Using Hooks to Reactify a Plain JavaScript Library: A Walkthrough

By: Andrew Petersen | August 6, 2019

React Hooks make it really easy to wrap a vanilla JavaScript library with a React component so you can easily reuse it throughout your app and stay in “React Mode”.

In this walkthrough I’ll be focusing on a single library, Shave.js, but the techniques and ideas should be applicable to any DOM updating JavaScript library. (Learn more about our app modernization solutions)

Example Library: Shave.js

Open sourced by Dollar Shave Club, shave.js helps cut off multi-line text with an ellipses once you hit your specified height (this is a surprisingly complicated issue).

Shave.js cleanly figures out how many lines will fit given your styles and specified height.

Vanilla JS Usage

The first thing to do is figure out how to use the library without worrying about anything React’y.

Shave.js is nice and simple. Tell it which element to shave and give it a max height.

shave(".selector", maxHeight);

You can also pass a DOM element (instead of string selector). This will come in handy when in React land.

let elem = document.querySelector(".selector");
shave(elem, maxHeight);

The Shave React Component: Basic

Let’s create a React component called Shave.

We’ll let people put whatever content they want inside of Shave and have them pass in a maxHeight prop.

The usage would be something like this:

<Shave maxHeight={100}>
 Offal vice etsy heirloom bitters selvage prism. Blue bottle forage
 flannel bushwick jianbing kitsch pabst flexitarian mlkshk whatever you
 probably havent heard of them selvage crucifix. La croix typewriter
 blue bottle drinking vinegar yuccie, offal hella bicycle rights iPhone
 pabst edison bulb jianbing street art single-origin coffee cliche. YOLO
 twee venmo, post-ironic ugh affogato whatever tote bag blog artisan.
</Shave>

Component Boilerplate

We’ll begin by creating a React function component. In React, you can easily render whatever developers put inside your component by using the special children prop.

function Shave({ children, maxHeight }) {
    return (
      <div>{children}</div>
    )
}

Adding Behavior

At this point we have a component that takes in content and renders it. It’s not super useful yet. What we really want to do is update the rendered div by calling shave on it (passing our maxHeight prop value).

Rephrasing, we want to force an effect on the div that we rendered.

The React hooks we’ll need are:

  • useRef to get a reference to our div
  • useEffect to affect the div after we render it.

Let’s start with the easy step: wiring up a reference to our DOM element container (the div).

  1. Create a variable, elemRef, using the useRef hook
  2. Set elemRef as the ref prop on the container div
function Shave({ children, maxHeight }) {
  // keep track of the DOM element to shave
  let elemRef = useRef();

  // apply our elemRef to the container div
  return <div ref={elemRef}>{children}</div>;
}

The next step is a little more…weird.

For myself, the hardest part of learning React Hooks has been useEffect and switching from a “lifecycle” mindset to a “keep the effect in sync” mindset.

It’d be tempting to say, “When our component first mounts, we want to run the shave function”. But that’s the old “lifecycle” way of thinking, and it doesn’t scale with added complexity.

Instead, let’s say, “Our shave should always respect the passed in maxHeight, so any time we have a new value for maxHeight, we want to (re)run our ‘shave’ effect”.

  • On initial render, we go from nothing to something, so our effect will run (effectively componentDidMount)
  • If the maxHeight prop changes, our effect will run again (effectively componentDidUpdate)

useEffect is a function that takes in two arguments

  1. A function – the actual code of the effect
  2. An array – Any time an item in the array changes, the effect will re-run.
    • As a rule of thumb, anything your effect function code references should be specified in this array (some exceptions being globals and refs).

The “shave” effect

// Run a shave every time maxHeight changes
useEffect(() => {
  shave(elemRef.current, maxHeight);
}, [maxHeight]);

With the shave effect calling shave on our div ref, we have a working component!

The basic Shave component

function Shave({ children, maxHeight }) {
  // keep track of the DOM element to shave
  let elemRef = useRef();

  // Run an effect every time maxHeight changes
  useEffect(() => {
    shave(elemRef.current, maxHeight);
  }, [maxHeight]);

  // apply our elemRef to the container div
  return <div ref={elemRef}>{children}</div>;
}

You can play with a demo of the working basic Shave component in this CodeSandbox.

The Shave React Component: Advanced

The previous Shave component does its job. We specify a max height and our component gets cut off. But let’s imagine after using it in a few different spots in our app, two new requirements emerge.

  1. The tech lead mentions that it should probably allow developers to be more semantic. Instead of always rendering a div, the component should optionally allow the developers to specify a more semantic dom element (like article).
  2. You are using the Shave component for the details section of a card’ish component and you need to toggle the “shave” on and off when the user clicks a “Read more” button.

Overriding the DOM element

We’ll add an “element” prop to the Shave component (with a default value of “div”). Then, if developers want to specify a different HTML element, they can with this syntax:

<Shave maxHeight={150} element="article">
  Multiline text content...
</Shave>

To update the Shave component:

  1. Take in an additional destructured prop named element and default it to “div”
  2. Create a variable name Element and use that as the container element in the returned JSX
function Shave({ children, maxHeight, element = "div" }) {
  // keep track of the DOM element to shave
  let elemRef = useRef();

  // Set our container element to be whatever was passed in (or defaulted to div)
  let Element = element;

  // Run an effect every time maxHeight changes
  useEffect(() => {
    shave(elemRef.current, maxHeight);
  }, [maxHeight]);

  // apply our elemRef to the container element
  return <Element ref={elemRef}>{children}</Element>;
}

What’s slick about this solution is it actually supports both native HTML elements (as a string value), or you can pass a reference to a custom React component.

// Renders the default, a DIV
<Shave maxHeight={150}>
  Multiline text content...
</Shave>

// Renders an ARTICLE
<Shave maxHeight={150} element="article">
  Multiline text content...
</Shave>

// Renders a custom BodyText react component
<Shave maxHeight={150} element={BodyText}>
  Multiline text content...
</Shave>

Allow “Shave” Toggling

To support toggling in the Shave component:

  1. Add an enabled prop, defaulted to true.
  2. Update shave effect code to only shave if enabled.
  3. Update the shave effect references array to include enabled so it will also re-run if enabled changes.
  4. Add enabled as the key to our container element so that if a enabled changes, React will render a completely new DOM node, causing our “shave” effect will run again. This is the trick to “unshaving”.
function Shave({ children, maxHeight, element = "div", enabled = true }) {
  // keep track of the DOM element to shave
  let elemRef = useRef();
  // Allow passing in which dom element to use
  let Element = element;

  // The effect will run anytime maxHeight or enabled changes
  useEffect(() => {
    // Only shave if we are supposed to
    if (enabled) {
      shave(elemRef.current, maxHeight);
    }
  }, [maxHeight, enabled]);

  // By using enabled as our 'key', we force react to create a
  // completely new DOM node if enabled changes.
  return (
    <Element key={enabled} ref={elemRef}>
      {children}
    </Element>
  );
}

Lastly, we need to update the parent component to keep track of whether it should be shaved or not. We’ll use the useState hook for this and wire up a button to toggle the value.

function ParentComponent() {
  // Keep track of whether to shave or not
  let [isShaved, setIsShaved] = useState(true);

  return (
    <div>
      <h1>I have shaved stuff below</h1>
      <Shave maxHeight={70} element="p" enabled={isShaved}>
        Mutliline content...
      </Shave>

      <button type="button" onClick={() => setIsShaved(!isShaved)}>
        Toggle Shave
      </button>
    </div>
  );
}

You can play with a demo of the working enhanced Shave component in this CodeSandbox.

Finally, if you are still here and interested in taking this further, here is another iteration of the Shave component that re-runs the shave every time the window resizes. It demonstrates how to properly clean up an effect by removing the resize event listener at the appropriate time.

Andrew is a full-stack developer specializing in front-end development with React, Node.js, and Office 365 customizations. As a project's Tech Lead, he comfortably interfaces with the business, BAs, PMs, and other developers to ensure a successful technical delivery.

Subscribe to our Newsletter

Stay informed on the latest technology news and trends

Relevant Insights

24 New Updates to Microsoft Teams | March 2024

If you are a frequent reader of this blog series, you know that for the last few months I’ve been...
Read More about 24 New Updates to Microsoft Teams | March 2024

Overcoming Poor Help Desk Experience with the Right IT MSP

Reliable IT support services are essential to keep your digital infrastructure and operations efficient and secure. If you've hired an...
Read More about Overcoming Poor Help Desk Experience with the Right IT MSP