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 ourdiv
useEffect
to affect thediv
after we render it.
Let’s start with the easy step: wiring up a reference to our DOM element container (the div
).
- Create a variable,
elemRef
, using theuseRef
hook - Set
elemRef
as theref
prop on the containerdiv
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 (effectivelycomponentDidUpdate
)
useEffect
is a function that takes in two arguments
- A function – the actual code of the effect
- 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.
- 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 (likearticle
). - 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:
- Take in an additional destructured prop named element and default it to “div”
- 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:
- Add an
enabled
prop, defaulted to true. - Update shave effect code to only shave if
enabled
. - Update the shave effect references array to include
enabled
so it will also re-run ifenabled
changes. - Add
enabled
as thekey
to our container element so that if aenabled
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.