DEV Community

Andrew Petersen
Andrew Petersen

Posted on

Reactify Vanilla JS Libraries with Hooks

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

In this walk through I'll be focusing on a single library, Shave.js, but the techniques and ideas should be applicable to any DOM updating javascript library.

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
    )
}
Enter fullscreen mode Exit fullscreen mode

Adding Behavior

At this point we have a component that takes in content, and renders it. 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.

Lets 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>;
}
Enter fullscreen mode Exit fullscreen mode

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 doesn't scale with added complexity.

Instead let's say, "Our shave should always respect the passed in maxHeight, so anytime 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 2 arguments

  1. A function - the actual code of the effect
  2. An array - Anytime 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]);
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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 it's job. We specify a max height and our component gets cutoff. But let's imagine after using it in a few different spots in our app, 2 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>
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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.

1000 bonus points to anyone that comments with a link to a forked CodeSandbox that includes debouncing the resize event!

Top comments (0)