Senior Devs Use These Hooks

November 22, 2024

Hooks have hanged the way we build React applications.

They got rid of a lot of code complexity by encouraging composition over inheritance

But where the magic really shines is with custom hooks.

So today I want to share must-know hooks that I personally can't go without.

By the way, you can find all these hooks in usehook-ts or other react hook libraries (like useHooks, aHooks or react-hookz).

#1 useLocalStorage

If you ever wanted to persist a state between page reloads, then this one is for you:

useLocalStorage is just like useState, with the difference being that the value is automatically saved and restored in the local storage.

You use it like useState, you just have to provide a key along the initial value:

const [value, setValue] = useLocalStorage<string>("key", initialValue);

Checkout this example, where the state of the counter is preserved, even if you reload the page:

Your Count is 0

"use client";
import { useLocalStorage } from "usehooks-ts";
import { Button, Flex, Text } from "@mantine/core";

export function WithLocalStorage() {
  const [count, setCount] = useLocalStorage("examples/count", 0);
  return (
    <>
      <Text>Your Count is {count}</Text>
      <Flex gap={"sm"} pt="sm">
        <Button size="sm" onClick={() => setCount((c) => c - 1)}>
          -
        </Button>
        <Button size="sm" onClick={() => setCount((c) => c + 1)}>
          +
        </Button>
      </Flex>
    </>
  );
}

#2 useDebounceValue

A hook to debounced a value, which is super useful for optimizing expensive operations like search or API calls.

const [state, setState] = useState("");
// when state is stable for 500 ms, then debouncedState is updated
const debouncedState = useDebounceValue(value, 500);

This is super useful because it gets rid of jumping UI when the user is typing and reduces the API calls you're making for the intermediate states.

Try it out yourself:

Input:

Debounced Input:

"use client";
import { useState } from "react";
import { useDebounceValue } from "usehooks-ts";
import { Box, Text, TextInput } from "@mantine/core";

export function Debouce() {
  const [input, setInput] = useState("");
  const [debouncedInput] = useDebounceValue(input, 500);
  return (
    <Box>
      <Text>Input: {input}</Text>
      <Text>Debounced Input: {debouncedInput}</Text>
      <TextInput
        label="Type something!"
        value={input}
        onChange={(e) => setInput(e.target.value)}
      />
    </Box>
  );
}

#3 useEventListener

useEventListener is a neat hook that lets you easily add n event listener to any component:

useEventListener("click", handler);

If you ever wanted to add an event listener to the document, the you know how boiler platy it can be.

But with useEventListener you can easily do that:

import { useEventListener } from "usehooks-ts";
import { notifications } from "@mantine/notifications";
import { Text } from "@mantine/core";

export const ClickNotifications = () => {
  useEventListener(
    "click",
    () =>
      notifications.show({
        message: "You clicked!"
      }),
    { current: document.body }
  );
  return <Text>Click anywhere and see what happens ✨.</Text>;
};

The cool thing is that the also clears up the event listener depending on the component lifecycle, so you don't have to worry about 'dangling' callbacks.

#4 useMediaQuery

useMediaQuery is a hook that makes it easy to handle media queries inside your components:

const matches = useMediaQuery("(min-width: 768px)");

Try opening this website on another device to see the difference ;) :

You're on

Desktop

"use client";
import { Card, Text } from "@mantine/core";
import { useMediaQuery } from "usehooks-ts";

export function MediaQuery() {
  const matches = useMediaQuery("(max-width: 768px)");
   return (
     <Card
       withBorder
       radius="md"
       p="md"
       c="primary"
       bg={{ light: "blue.1", dark: "blue-8" }}
     >
       <Text mx="auto">You{"'"}re on</Text>
       <Text mx="auto" size="2xl" fw="600">
         {matches ? "Mobile" : "Desktop"}
       </Text>
     </Card>
   );
}

#5 useWindowSize

useWindowSize is pretty stright forward: It tracks the window's width and height.

const { width, height } = useWindowSize();

The nice this is that it provides the values ' in real time'.

Try resizing your window to see the size change.

This window is

x

"use client";
import { useWindowSize } from "usehooks-ts";
import { Card, Text } from "@mantine/core";

export function WindowSize() {
  const { width, height } = useWindowSize();

  return (
    <Card
      withBorder
      radius="md"
      p="md"
      c="primary"
      bg={{ light: "blue.1", dark: "blue-8" }}
    >
      <Text mx="auto">This window is</Text>
      <Text mx="auto" size="2xl" fw="600">
        {width}x{height}
      </Text>
    </Card>
  );
}

#6 useHover

useHover is a hook I wish I discovered earlier...

It detects whether an element is hovered or not:

const ref = useRef(null);
const isHovered = useHover(ref); // true or false

I am unhovered

"use client";
import { useRef } from "react";
import { useHover } from "usehooks-ts";
import { Card, Text } from "@mantine/core";

export function Hover() {
  const hoverRef = useRef(null);
  const isHover = useHover(hoverRef);
  return (
    <Card
      radius="md"
      p="md"
      ref={hoverRef}
      bg={isHover ? "primary" : "primary.1"}
    >
      <Text
        mx="auto"
        c={!isHover ? "primary" : "primary.1"}
        size="xl"
        fw="600"
      >{`I am ${isHover ? `hovered` : `unhovered`}`}</Text>
    </Card>
  );
}

#7 useIntersectionObserver

useIntersectionObserver is a hook used to observe when an element enters or leaves the viewport:

const { isIntersecting, ref } = useIntersectionObserver({ threshold: 0.5 });

IntersectionObserver is often used for lazy loading images, infinite scrolling, animations on scroll, and tracking element visibility in view for analytics or other dynamic changes.

This section fades in when in view

"use client";
import { useIntersectionObserver } from "usehooks-ts";

export const IntersectionObserver = () => {
  const { ref, isIntersecting } = useIntersectionObserver({
    threshold: 0.1, // Trigger when 10% of the element is in view
  });

  return (
    <div
      ref={ref}
      style={{
        opacity: isIntersecting ? 1 : 0,
        transform: isIntersecting ? "translateY(0)" : "translateY(20px)",
        transition: "opacity 0.6s ease-out, transform 10s ease-out",
      }}
    >
      <h2>This section fades in when in view</h2>
    </div>
  );
};

#8 useCopyToClipboard

useCopyToClipboard is a hook that allows you to copy text or data to the clipboard.

const [copiedText, copyFn] = useCopyToClipboard();

It is often used for quick copying of information like URLs, codes, or messages with feedback on copy success.

"use client";
import { Button } from "@mantine/core";
import { useCopyToClipboard } from "usehooks-ts";

export const Clipboard = () => {
  const [copied, copy] = useCopyToClipboard();

  const handleCopy = () => {
    copy("This is the text to copy!");
  };

  return (
    <div>
      <Button onClick={handleCopy}>{copied ? "Copied!" : "Copy Text"}</Button>
      {copied && <p style={{ color: "green" }}>Text copied to clipboard!</p>}
    </div>
  );
};

#9 useInterval

useInterval(function, duration);

The useInterval hook is used to repeatedly execute a function at specified intervals, similar to setInterval in vnilla JavaScript, but in React-friendly way.

Current Time

17:09:45

"use client";
import { Card, Text } from "@mantine/core";
import dayjs from "dayjs";
import { useState } from "react";
import { useInterval } from "usehooks-ts";

export function Interval() {
  const [time, setTime] = useState(new Date());
  useInterval(() => setTime(new Date()), 1000);
  return (
    <Card
      withBorder
      radius="md"
      p="md"
      c="primary"
      bg={{ light: "blue.1", dark: "blue-8" }}
    >
      <Text mx="auto">Current Time</Text>
      <Text mx="auto" size="2xl" fw="600">
        {dayjs(time).format("HH:mm:ss")}
      </Text>
    </Card>
  );
}

useInterval is useful for tasks like auto-saving, updating clocks or timers and polling APIs (although, you should really use react query for this).