DEV Community

Cover image for 5 Great Custom Hooks For Your React Project
John ✨️
John ✨️

Posted on • Originally published at johna.hashnode.dev

5 Great Custom Hooks For Your React Project

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) => {};
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

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';
}
Enter fullscreen mode Exit fullscreen mode

Possible values are undeterminedmacosioswindowsandroidlinux. 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></>;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 which 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;
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (2)

Collapse
 
sammaji profile image
Samyabrata Maji

Great Article!!

Collapse
 
ademoyejohn profile image
John ✨️

Thank you, Maji.