As I was searching through the codebase and writing the components for my current work ticket, I came across some useful custom hooks from a number of third-party libraries like react-use, usehooks-ts, Mantine. But there are for sure other similar libraries out there such as use-http, react-hanger and many more.
1️⃣ useTitle
This hook from react-use allows you to dynamically set the title of a page in your application. It can be used to set the title of the page to a specific value or to update the title in response to user interactions or state changes in the application.
You may think that there's nothing particularly special about that since you could easily do document.title
and you're good to go, but under the hood, the hook checks that the document is not undefined and since it's a side effect it implements the useEffect
hook:
import { useEffect, useRef } from "react";
export interface UseTitleOptions {
restoreOnUnmount?: boolean;
}
const DEFAULT_USE_TITLE_OPTIONS: UseTitleOptions = {
restoreOnUnmount: false,
};
function useTitle(
title: string,
options: UseTitleOptions = DEFAULT_USE_TITLE_OPTIONS
) {
const prevTitleRef = useRef(document.title);
if (document.title !== title) document.title = title;
useEffect(() => {
if (options && options.restoreOnUnmount) {
return () => {
document.title = prevTitleRef.current;
};
} else {
return;
}
}, []);
}
export default typeof document !== "undefined"
? useTitle
: (_title: string) => {};
Usage:
import { useTitle } from 'react-use';
function MyComponent() {
const [title, setTitle] = useTitle('Old Title');
const handleClick = () => setTitle('New Title');
return (
<div>
<h1>{title}</h1>
<button onClick={handleClick}>Change Title</button>
</div>
);
}
An instance of the useTitle
hook is used to set the initial value of the title
state variable to 'Default Title'
. The setter function setTitle
is then called to update the title on clicking the button.
2️⃣ useCopyToClipboard
This implementation of the useCopyToClipboard
hook comes from the usehooks-ts library. It provides an easy way to copy text to the clipboard. Other React hooks libraries also have this hook which they implement differently: some rely on JavaScript libraries like copy-to-clipboard that run on document.execCommand()
while others like this one depend on the clipboard
API.
import { useState } from 'react'
type CopiedValue = string | null
type CopyFn = (text: string) => Promise<boolean> // Return success
function useCopyToClipboard(): [CopiedValue, CopyFn] {
const [copiedText, setCopiedText] = useState<CopiedValue>(null)
const copy: CopyFn = async text => {
if (!navigator?.clipboard) {
console.warn('Clipboard not supported')
return false
}
// Try to save to clipboard then save it in the state if worked
try {
await navigator.clipboard.writeText(text)
setCopiedText(text)
return true
} catch (error) {
console.warn('Copy failed', error)
setCopiedText(null)
return false
}
}
return [copiedText, copy]
}
export default useCopyToClipboard
The clipboard API does the cut, copy and paste asynchronously, which is why writeText()
is called in a try-catch
block. The state variable copiedText
holds the value or null
depending on the outcome of the asynchronous action.
Usage
import { useCopyToClipboard } from 'usehooks-ts'
export default function Component() {
const [value, copy] = useCopyToClipboard()
return (
<>
<h1>Click to copy:</h1>
<div style={{ display: 'flex' }}>
<button onClick={() => copy('A')}>A</button>
<button onClick={() => copy('B')}>B</button>
<button onClick={() => copy('C')}>C</button>
</div>
<p>Copied value: {value ?? 'Nothing is copied yet!'}</p>
</>
)
}
In this example, the letters passed to the setter get copied to the user's clipboard on clicking the buttons. Then value
is conditionally displayed in the p
element below.
3️⃣ useOs
Have you ever thought about designing or making your application behave differently based on the user's operating system (OS) such as implementing adaptive design or providing different download links for different platforms? Well, this hook from the Mantine library provides you with the OS name, returning it as a string
.
export type OS = 'undetermined' | 'macos' | 'ios' | 'windows' | 'android' | 'linux';
function getOS(): OS {
const { userAgent } = window.navigator;
const macosPlatforms = /(Macintosh)|(MacIntel)|(MacPPC)|(Mac68K)/i;
const windowsPlatforms = /(Win32)|(Win64)|(Windows)|(WinCE)/i;
const iosPlatforms = /(iPhone)|(iPad)|(iPod)/i;
if (macosPlatforms.test(userAgent)) {
return 'macos';
}
if (iosPlatforms.test(userAgent)) {
return 'ios';
}
if (windowsPlatforms.test(userAgent)) {
return 'windows';
}
if (/Android/i.test(userAgent)) {
return 'android';
}
if (/Linux/i.test(userAgent)) {
return 'linux';
}
return 'undetermined';
}
export function useOs(): OS {
if (typeof window !== 'undefined') {
return getOS();
}
return 'undetermined';
}
Possible values are undetermined
, macos
, ios
, windows
, android
, linux
. If os cannot be identified, for example, during server-side rendering (since the window object would be undefined) undetermined
will be returned.
Usage:
import { useOs } from '@mantine/hooks';
function Demo() {
const os = useOs();
return <>Your os is <b>{os}</b></>;
}
This example is from Mantine's documentation for this hook. So if you're viewing it from an android device, for example, you should get:
Your os is android
4️⃣ createGlobalState
createGlobalSize
also belongs to react-use and it provides a way for you to create a global state that can be shared across multiple components without having to go for state management libraries like redux.
In a typical React application, the state is usually managed locally within a component and passed down to child components through props. However, in some cases, it may be desirable to have a global state that can be accessed and updated by components that are not directly connected in the component tree. This hook serves that purpose quite well.
Under the hood, createGlobalState
accepts some initial state (or not) of any type and returns a function that has the typical behaviour of useState
in that it returns an array of the current state and a setter. This function can then be used in as many components as necessary.
import { useState } from 'react';
import { IHookStateInitAction, IHookStateSetAction, resolveHookState } from '../misc/hookState';
import useEffectOnce from '../useEffectOnce';
import useIsomorphicLayoutEffect from '../useIsomorphicLayoutEffect';
export function createGlobalState<S = any>(
initialState: IHookStateInitAction<S>
): () => [S, (state: IHookStateSetAction<S>) => void];
export function createGlobalState<S = undefined>(): () => [
S,
(state: IHookStateSetAction<S>) => void
];
export function createGlobalState<S>(initialState?: S) {
const store: {
state: S;
setState: (state: IHookStateSetAction<S>) => void;
setters: any[];
} = {
state: initialState instanceof Function ? initialState() : initialState,
setState(nextState: IHookStateSetAction<S>) {
store.state = resolveHookState(nextState, store.state);
store.setters.forEach((setter) => setter(store.state));
},
setters: [],
};
return () => {
const [globalState, stateSetter] = useState<S | undefined>(store.state);
useEffectOnce(() => () => {
store.setters = store.setters.filter((setter) => setter !== stateSetter);
});
useIsomorphicLayoutEffect(() => {
if (!store.setters.includes(stateSetter)) {
store.setters.push(stateSetter);
}
});
return [globalState, store.setState];
};
}
export default createGlobalState;
The createGlobalState
custom hook also implements some other hooks in react-use like useIsomorphicLayoutEffect and useEffectOnce.
Usage
const useGlobalValue = createGlobalState<number>(0);
const CompA: FC = () => {
const [value, setValue] = useGlobalValue();
return <button onClick={() => setValue(value + 1)}>+</button>;
};
const CompB: FC = () => {
const [value, setValue] = useGlobalValue();
return <button onClick={() => setValue(value - 1)}>-</button>;
};
const Demo: FC = () => {
const [value] = useGlobalValue();
return (
<div>
<p>{value}</p>
<CompA />
<CompB />
</div>
);
};
In the use case above createGlobalState
was initialized with 1 and gets number
passed as a type parameter. It then returns a useState-like function which is called useGlobalValue
here.
CompA
and CompB
use the setter from it to update the global state variable value
. This can be used everywhere in the application. I like to see createGlobalState
as an umbrella state holder that shares data among components.
5️⃣useWindowSize
One use case for the useWindowSize
hook, which belongs to react-use, is for responsive design. The useWindowSize
hook keeps track of the size of the browser window which makes it possible to apply different styles (layouts, displays, etc.) to user interfaces at different sizes. It returns an object containing the current width and height of the window.
import { useState } from 'react'
import { useEventListener, useIsomorphicLayoutEffect } from '..'
interface WindowSize {
width: number
height: number
}
function useWindowSize(): WindowSize {
const [windowSize, setWindowSize] = useState<WindowSize>({
width: 0,
height: 0,
})
const handleSize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
})
}
useEventListener('resize', handleSize)
// Set size at the first client-side load
useIsomorphicLayoutEffect(() => {
handleSize()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return windowSize
}
export default useWindowSize
The hook is typed with WindowSize
. It gets the dimensions from the window object, listens for resize
and gets updated based on that.
Usage
import {useWindowSize} from 'react-use';
const Demo = () => {
const {width, height} = useWindowSize();
return (
<div>
<div>width: {width}</div>
<div>height: {height}</div>
</div>
);
};
The hook returns windowSize
which is destructured in the example above (as is typical in handling return values from hooks in React) and the value can be easily used in the application.
Final Thoughts
As you can see there are quite a lot of customs hooks out there that serve as many useful utility purposes as:
sensors
animations
state
UI
side-effects
I thought about writing these custom hooks by myself but there's no point reinventing the wheel and they're probably more reliable since many developers are working on the libraries.