Tooltips! A Nice Singleton-y Way To Only Show One At A Time
Tags: webdev, javascript, react, ux
Published: 2023-01-17 15:56:57

Tooltips are trickier than they look; For the optimal experience, you have to handle many intricacies and edge cases.

Positioning is complex, with its many edge cases (screen edges, scrolling). Hover behavior could be more intricate, so the tooltip remains open while you hover over it, and not just its target. Appearance and disappearance should both have delays and maybe animation. AND you don't want multiple tooltips appearing together and hiding each other.

Today I'll write about this last part. Let me know if you'd like me to elaborate on the other parts :)

===
## Who am I?
I'm Miki Stanger, a front-end architect and advisor. I've been to the industry for 12 years. I currently accompany companies and help with architecture, decisions, mentoring and problem-solving.

I'm always looking for cool people and projects to help with! You can read more about me and find my up-to-date contact information here: https://about.me/mikistanger/

=== 

## Tooltips (And When To Use Them)
Tooltips are a way to show additional information about something. They are usually triggered by hovering over an element, and should show an explanation about how to read or use a part of your system.

Because it is sensitive to mouse movement and positioned next to other elements, it is best used for short texts; Large bodies of text will require the user to not touch their mouse for a while (even though some people move the mouse, click or select texts while reading or while multitasking), and might create a huge box that overshadows or even hides the parts it explains.

If your explanation is more than a few sentences long, you could use a modal instead; A modal is more persistent and allows the use of photos and videos, and are thus a better medium for larger amounts of content.

## The Problem
The premise of a tooltip is supposedly basic - Hover over something and some element should appear in the right position. However, if you have several available tooltips next to each others, this could happen:

![Multiple tooltips appearing over each other](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/on71899a11k0ullftlz3.gif)


See how the tooltips hide each other? This kind of clutter not only looks messy, it could also steal the user's focus from the one tooltip they're looking for.

## The Solution
When we show a tooltip, we could simply **hide the other ones**. 
As with a lot of tooltip-related things, this is a bit more complex than it sounds, especially when working with React. There are two approaches here:
1. **Have a single tooltip component and control it through props.** You could pass the method through a store or using context, so they're available globally. This is not a bad way, except that it might unnecessarily trigger a full re-render. We'll look at a different approach that doesn't require all of those additional mechanisms.
2. **Have multiple tooltip instances that live where they are used, and some way to turn the others off**. This is also achievable with a store or a context, but there's a better way (imo) to do it - **manage hiding tooltips with a singleton**.

## What Is A Singleton?
A singleton is a single instance of an object that's exposed to the whole system (as opposed to creating multiple objects or instances of a class).

A singleton is useful when one needs to guarantee the same options and methods will be available and persistent throughout the project. A common example for a singleton is a logging instance - a single instance of a class, with its configuration and options, which guarantee that all logging will be done with the same options and policies everywhere.

## The `PreventMultipleTooltipsManager` Singleton

Our tooltip is a component that wraps another component and surrounds it with a tooltip-triggering component:
```tsx
export const WithTooltip({ children }) => {
	const [isVisible, setIsVisible] = useState(false);

	const setIsVisibleToFalse = useCallback(() => {
		setIsVisible(false);
	}, [setIsVisible]);

	const setIsVisibleToTrue = useCallback(() => {
		setIsVisible(true);
	}, [setIsVisible]);

	// Rest of logic

	return <TooltipWithStylesAndStuff
		onMouseEnter={setIsVisibleToTrue} 
		onMouseLeave={setIsVisibleToFalse}
	>
		{children}
	</TooltipWithStylesAndStuff>
}
```

OUTSIDE of the component, we'll create a class and initiate a single instance of it - a singleton:
```tsx
class PreventMultipleTooltipsManager {}

const preventMultipleTooltipsManager = new PreventMultipleTooltipsManager();
```

This singleton is going to hold a function that hides the currently visible tooltip (its `setIsVisibleToFalse` function). When a new tooltip is shown, the singleton will allow us to use the now-previous tooltip's function and replace it with the current one's. We can do both operations in a single method, as they're always going to be called together in the same order:

```tsx
class PreventMultipleTooltipsManager {
	private currentHideFunc: (() => void) | null = null;

	public changeTooltip(hideFunc) {
		this.currentHideFunc?.();
		this.currentHideFunc = hideFunc;
	}
}

const preventMultipleTooltipsManager = new PreventMultipleTooltipsManager();
```

We're also going to add an `unmount` method. This method will be used in a tooltip's teardown, in case it is removed from the DOM while visible. We'll later use it in a teardown function that's the return value of a `useEffect`:
```tsx
class PreventMultipleTooltipsManager {
	private currentHideFunc: (() => void) | null = null;

	public changeTooltip(hideFunc) {
		this.currentHideFunc?.();
		this.currentHideFunc = hideFunc;
	}

	public unmount(hideFunc) {
		if (hideFunc === this.currentHideFunc) {
			this.currentHideFunc = null;
		}
	}
}

const preventMultipleTooltipsManager = new PreventMultipleTooltipsManager();
```

The singleton is done. Now we can call it inside the component. We'll use its `changeTooltip` method in `isSetVisibleToTrue`, and its `unmount` method in a `useEffect`:
```tsx
export const WithTooltip({ children }) => {
	const [isVisible, setIsVisible] = useState(false);

	const setIsVisibleToFalse = useCallback(() => {
		setIsVisible(false);
	}, [setIsVisible]);

	const setIsVisibleToTrue = useCallback(() => {
		preventMultipleTooltipsManager.changeTooltip(setIsVisibleToFalse);
		setIsVisible(true);
	}, [setIsVisible, setIsVisibleToFalse]);

	useEffect(() => (
		() => {
			preventMultipleTooltipsManager.unmount(setIsVisibleToFalse);
		}
	), [setIsVisibleToFalse])

	// Rest of logic

	return <TooltipWithStylesAndStuff
		onMouseEnter={setIsVisibleToTrue} 
		onMouseLeave={setIsVisibleToFalse}
	>
		{children}
	</TooltipWithStylesAndStuff>
}
```

And that's it! Look at it behaving nicely:

![Now, only the required tooltip shows, while the others disappear](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k8009y9trqzmft0ya5vb.gif)


## Conclusion
Tooltips are a wonderful case of how thinking about the small details makes a difference between buggy, unfriendly component and a delightful one. Today, we dug into one of several such details, learned what a singleton is, and use this knowledge to make our tooltips better.

What do you think? How would you implement a solution to this same problem?