I've always wanted to implement something like Medium's highlighting and sharing tooltip. But I was surprised just how much work it takes to do it right, especially in SSR React applications. So in the next few blog posts I'll be detailing some tips and tricks I used to create a medium-like sharing tooltip.
Getting the selected text from the document
First off, you're going to want an easy way to reference the Selection object. This is the object that stores "the range of text selected by the user or the current position of the caret." Specifically, we're going to want Selection.toString()
since that's what returns the selected text we'd like to share with our peeps. Since we're working with an SSR React application however, the document
isn't going to be available just anywhere. Moreover, we probably want keep this separate from the components that use the selected text. Which is to say, what we want is a Hook.
useSelection Hook
So let's make a React hook that returns selected text and Selection object. First we're going to want to import useState
and useEffect
from React. We are making a hook after all. But note that in SSR React apps, the window
and document
are only accessible within specific react-lifecycle methods. We're going to access them in the useEffect
hook and store the values we draw from them using the useState
hook. In this case, that means storing our selected text:
const [selected, setSelected] = useState();
useEffect(() => {
const selObj = window.getSelection();
setSelected(selObj);
}, []);
Now if all we wanted was a one time snapshot of the Selection
object, we'd be set. But what we really want is the current value of the Selection
. In other words, whenever the selection changes, our Selection
object should have changed as well. To get this behavior, we're going to use the onselectionchange
event listener. This way, we retrieve the selection.toString()
and set it as selected
using the setSelected()
state hook.
Our final product for a useSelection
hook then looks something like this:
import { useState, useEffect } from "react";
export const useSelection = () => {
const [selected, setSelected] = useState();
// callback function that updates state with current selection
const handleSelectionChange = () => {
let selection;
let text = "";
// if browser supports selection API
if (document.getSelection) {
selection = document.getSelection();
text = selection.toString();
} else if (document.selection) {
selection = document.selection.createRange();
text = selection.text;
}
// update state with selected text and Selection object
setSelected({ text, selection });
};
useEffect(() => {
let isSubscribed = true;
// whenever selection on page changes, call handleSelectionChange callback
document.onselectionchange = () => isSubscribed && handleSelectionChange();
// 'unsubscribe' from event listener on component dismount
return () => (isSubscribed = false);
}, []);
return selected;
};
Now that we have the hook, we can use it in any component to get the Selection
object and its string value.
const selected = useSelection(); // => { selection: Selection, text: selected text str }
Up next
In the next post in the series, I'll detail how we can use this hook to share the selected text using a share-tooltip.