React - My useIsDarkMode Hook

Published: Oct 27, 2024

Recently I've been working with a UI that has a white background, and honestly, it's burning my eyes something terrible.

Lots of other users with all sorts of different situations will really appreciate an application that automatically adjusts to their preference. Users may be viewing your app late at night in a dark setting, trying to preserve battery life or wanting better contrast when there is glare on a screen.

So here's my implementation of a react hook allowing me to efficiently manage the dark and light theme of my UI.

Your eyes will thank you!

This useIsDarkMode hook is an efficient, reusable way to detect and respond to the user’s preferred colour scheme in a React application. Its design makes it performance-friendly and safe to use, particularly with TypeScript’s added type safety. This hook simplifies theme detection by returning either true or false to my components that wish to use it.

import { useEffect, useState } from 'react'

export const useIsDarkMode = (): boolean => {
    const getCurrentScheme = (): boolean =>
        window.matchMedia('(prefers-color-scheme: dark)').matches

    const [isDarkMode, setIsDarkMode] = useState<boolean>(getCurrentScheme)

    useEffect(() => {
        const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
        const handleChange = (e: MediaQueryListEvent) =>
            setIsDarkMode(e.matches)
        mediaQuery.addEventListener('change', handleChange)

        return () => mediaQuery.removeEventListener('change', handleChange)
    }, [])

    return isDarkMode
}

getCurrentScheme is a helper function that queries the window.matchMedia API to determine if the user’s preferred colour scheme is dark. The window.matchMedia('(prefers-color-scheme: dark)') check will return an object whose matches property will be true if dark mode is preferred, otherwise false.

The useState hook, which expects a boolean value, initialises isDarkMode with the result of getCurrentScheme, allowing the hook to synchronise with the user’s current colour scheme.

A useEffect is used to determine when this component mounts, and the mediaQuery is defined to represent the prefers-color-scheme: dark media query.

An Event Listener is set up for handleChange which listens for changes in the colour scheme and updates isDarkMode using setIsDarkMode whenever a change is detected. The MediaQueryListEvent type is used in the handleChange function’s parameter, ensuring that TypeScript knows the e.matches property will be available on the event object. This type safety prevents potential errors if e were incorrectly typed.

By returning a cleanup function that removes the event listener, the hook avoids memory leaks if the component using this hook is unmounted.

Finally, the hook returns the isDarkMode state so that any component using this hook can access the dark mode status. By encapsulating this logic in a custom hook, any component in the application can easily check if dark mode is active without having to duplicate the logic. This separation promotes clean and maintainable code.

Example Usage of useIsDarkMode

Here’s a simple example of how to use the useIsDarkMode hook within a React component:

import React from 'react'
import { useIsDarkMode } from './useIsDarkMode'

export const ThemeIndicator: React.FC = () => {
    const isDarkMode = useIsDarkMode()

    return (
        <div style={{
            backgroundColor: isDarkMode ? '#333' : '#FFF',
            color: isDarkMode ? '#FFF' : '#333',
        }}>
            <p>{isDarkMode ? 'Dark' : 'Light'} Mode is Enabled</p>
        </div>
    )
}