Reactify Vanilla JS Libraries with Hooks
Tags: javascript, react, webdev
Published: 2019-07-02 21:53:05

---
title: Reactify Vanilla JS Libraries with Hooks
published: true
description: 
tags: javascript, react, webdevelopment
---

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](https://dollarshaveclub.github.io/shave/), 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](https://dollarshaveclub.github.io/shave/) 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.*
<img src='https://thepracticaldev.s3.amazonaws.com/i/de5temujs1pm225nsupa.PNG' width=500 />

## 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.

```javascript
shave(".selector", maxHeight);
```

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

```javascript
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:

```html
<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. 

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

### 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`

```jsx
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 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*
```javascript
// 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*
```jsx
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](https://codesandbox.io/s/reactify-shave-part-1-37wq1).


## 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:

```jsx
<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

```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.

```html
// 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".


```jsx
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.

```jsx
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](https://codesandbox.io/s/reactify-shave-part-2-gzeq2?fontsize=14).

Finally, if you are still here and interested in taking this further, [here is another iteration](https://codesandbox.io/s/reactify-shave-part-3-wmdef) 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!*