Back to List

Using Hooks to Reactify a Plain JavaScript Library: A Walkthrough

Andrew Petersen Andrew Petersen  |  
Aug 06, 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.
 

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.

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

JavaScript

 

Love our Blogs?

Sign up to get notified of new Skyline posts.

 


Related Content


Spring 2019 Kentico User Group
Apr 17, 2019
Location: Waukesha County Technical College - Pewaukee Campus - 800 Main Street, Pewaukee, Wisconsin 53072 - Building: Q, Room: Q361
Blog Article
How to Create a Readable and Useful Bug Report
Blayne RoselleBlayne Roselle  |  
Jun 25, 2019
Creating a bug that is both readable and provides enough detail is a must-have skill for a Quality Assurance Analyst. Not only will it help when it comes time to retest, but it also provides credibility with your development team. In the content below, I will share the best practices for creating...
Blog Article
Azure Tips & Tricks: Application Insights Snapshot Debugger
Todd TaylorTodd Taylor  |  
May 21, 2019
A painful memory that is burned into my developer-brain is a production support issue for a .NET web API that I wrote years ago. I worked for a large retailer at the time, and the bug was preventing electronic pricing signs from displaying the most up-to-date price for hundreds of products at...
Blog Article
Thinking Outside the Application Development Box with Unity
Jeff WeberJeff Weber  |  
May 14, 2019
Do you or your company have an idea for an application that sits a little outside your comfort zone? Does your idea possibly require game-like graphics, Augmented Reality, Virtual Reality or similar technology? If so, Unity might be worth a look.   I’ve been using Unity in my spare...
Blog Article
Creating and Installing Project Templates in .NET Core
Ben BuhrBen Buhr  |  
Apr 30, 2019
In my previous blog article, we examined the .NET Core Command Line Interface (CLI). As part of that, we saw that templates in .NET Core can be very useful. Templates in .NET Core are very easy to create, and there already are a ton of very helpful ones available. They allow us to quickly get an...