Back to Blog

Code Snippets - React Hooks

8 min read

code-snippetsreacthooksintermediate

December 16, 2021 (Updated on January 10, 2022)

Code Snippets - React Hooks

Contents


Introduction

Below you'll find a collection of utility hooks I've built over time, all in one convenient place. I'll be updating this list over time, as new hooks are built.

useArrayLimiter

This hook takes an array, and returns a limited set of the same array. A function is returned to increase the number of elements visible, along with a reset function and an indicator if all elements are returned.

For an example, see the blog posts listed at the bottom of the blogs page. A "show more" button is visible when more than 6 articles are available.

import { useEffect, useState } from 'react';

/**
 * Hook to limit the number of entries in an array, and split them into batches.
 * @param entries Set of entries to limit.
 * @param batchSize Size of the entries to return.
 * @returns The limited set of entries.
 */
export const useArrayLimiter = <T extends any>(entries: T[], batchSize = 6): [T[], boolean, () => void, () => void] => {
    const [visibleCount, setVisibleCount] = useState(batchSize);

    const visibleEntries = entries.slice(0, visibleCount);
    const isAllVisible = visibleCount >= entries.length;

    const viewMore = () => setVisibleCount((count) => count + batchSize);
    const reset = () => setVisibleCount(batchSize);

    useEffect(() => setVisibleCount(batchSize), [entries]);

    return [visibleEntries, isAllVisible, viewMore, reset];
};

useCombinedSubset

This hook takes an array of arrays, combines them and returns a limited set of the combination.

For an example, see the bottom of any blog post. The hook combines two sets of recommendations into one, and then displays the latest three.

import { useMemo } from 'react';

/**
 * Hook to combine a set of arrays into a single array, then return a limited number of those arrays.
 * @param limit Number of entries to limit.
 * @param arrays Arrays to combine.
 * @returns Limited set to combine
 */
export const useCombinedSubset = <T extends any>(limit: number, arrays: T[][]): T[] => {
    return useMemo(() => {
        const limitedSet: T[] = [];

        arrays.forEach((arr) => {
            const numOfEntriesNeeded = limit - limitedSet.length;

            if (numOfEntriesNeeded === 0) {
                return;
            }

            limitedSet.push(...arr.slice(0, numOfEntriesNeeded));
        });

        return limitedSet;
    }, [limit, arrays]);
};

useFilter

This hook acts as a more advanced version of the .filter function. It takes a set of data, a filter, and a predicate in how to filter the data. The data is only filtered if the filter parameter is defined, otherwise it returns all the provided data.

For an example, try selecting some topics on the blogs page.

import { useMemo } from 'react';

/**
 * Hook to filter a set of data based on a predicate and filter.
 * The data is only filtered when the filter parameter is defined.
 * @param data Data to filter.
 * @param filter Value used to filter.
 * @param predicate Predicate to filter data.
 * @returns The data filtered by the predicate.
 */
export const useFilter = <TFilter, TData>(
    data: TData[],
    filter: TFilter = undefined,
    predicate: (data: TData, filter: TFilter) => boolean
): TData[] => {
    return useMemo(() => {
        if (!filter || (Array.isArray(filter) && filter.length === 0)) {
            return data;
        }

        return data.filter((entry) => predicate(entry, filter));
    }, [filter, data, predicate]);
};

useIsMobile

This very simple hook wraps Chakra-UI's useBreakpointValue hook to return true/false if the screen size is of mobile or less.

import { useBreakpointValue } from '@chakra-ui/react';

/**
 * Hook to return true if the current breakpoint is mobile.
 * @returns True if the current screen width is mobile size.
 */
export const useIsMobile = (): boolean => {
    return useBreakpointValue([true, null, false]);
};

useMergedStyles

This hook takes two style objects, and deeply merges them to produce a single style object. This can be adapted for whatever style system being used, and is useful especially in design systems where styles may need to be overridden.

import { SystemStyleObject } from '@chakra-ui/react';
import { merge } from 'lodash';
import { useMemo } from 'react';

/**
 * Hook to deeply merge styles from multiple sources.
 * @param sx Style object passed into the component.
 * @param styles Internal styles of the component.
 * @returns A deeply merged style object.
 */
export const useMergedStyles = (sx: SystemStyleObject = {}, styles: SystemStyleObject): SystemStyleObject => {
    return useMemo(() => merge(styles, sx ?? {}), [styles, sx]);
};

useParamsEvent

This hook is used to fire an event if a specific query parameter is found within the URL. The callback is called with the values of the key specified.

For an example, selecting the most popular topic on the stats page will direct you to the blog page, with the blogs already pre-filtered.

import { useEffect, useState } from 'react';
import { useLocation } from '@reach/router';

/**
 * Hook to fire an event when a specific URL parameter is matched against.
 * @param key Query parameter key to use.
 * @param onMatched Event fired when the query parameter is matched.
 */
export const useParamsEvent = (key: string, onMatched: (matched: string[]) => void): void => {
    const location = useLocation();
    const [hasMatched, setHasMatched] = useState(false);

    useEffect(() => {
        const matched = location.search;
        const params = new URLSearchParams(matched);

        if (params.has(key) && !hasMatched) {
            setHasMatched(true);

            const matchedParam = params.get(key);
            if (matchedParam.includes(',')) {
                onMatched(matchedParam.split(','));
            }

            onMatched([matchedParam]);
        }
    }, [location, key, onMatched]);
};

useToggleSet

This hook allows values to be toggled on/off within an array. When a value is provided to the toggleValue function, it is added to the internal state. Provide it again, and it gets removed.

For an example, the topics filters on the blog page use this hook to track selection states.

import { useCallback, useState } from 'react';

/**
 * Hook to allow toggling of values within an array. Toggling once will add it to the state array, toggling again will remove it.
 * @param initialValue Initial value of the state array.
 */
export const useToggleSet = <T extends any>(initialValue?: T[]): [T[], (toggle: T) => void, () => void] => {
    const [internalState, setInternalState] = useState<T[]>(initialValue ?? []);

    const toggleValue = useCallback(
        (value: T) => {
            if (internalState.includes(value)) {
                setInternalState(internalState.filter((v) => v !== value));
            } else {
                setInternalState([...internalState, value]);
            }
        },
        [internalState, setInternalState]
    );

    const resetSet = useCallback(() => {
        setInternalState(initialValue ?? []);
    }, [setInternalState, initialValue]);

    return [internalState, toggleValue, resetSet];
};

useIfClient

This hook allows values to be returned only if it's being executed within the browser, otherwise a default value can be provided in place of the function's result.

/**
 * Hook to execute the provided function only if executing in the browser.
 * Useful in SSG environments such as Gatsby, when accessing window or document elements.
 */
export const useIfClient = <T extends any>(func: () => T, defaultValue?: T): T | undefined => {
    const isClient = typeof window !== 'undefined';
    return isClient ? func() : defaultValue;
};

useDistanceFromTop

This hook allows you to calculate in pixels the distance an element is from the top of the document. The ref is assigned to the element you wish to track, and the distance is returned in the returned object.

import React, { MutableRefObject, useRef } from 'react';
import { useIfClient } from '~hooks';

export const useDistanceFromTop = (): [MutableRefObject<HTMLElement>, { distance: number }] => {
    const ref = useRef<HTMLElement>();
    const distance = useIfClient(
        () => (ref.current?.getBoundingClientRect().top ?? 0) + (document?.documentElement.scrollTop ?? 0),
        0
    );

    return [ref, { distance }];
};

useRelativeScrollPercentage

This hooks uses the functionality of the useDistanceFromTop hook, and allows you to calculate the percentage the document has scrolled between two points. Negative percentages are returned when the range hasn't been scrolled into, and percentages above 100 are returned when scrolled past. An offset can also be provided to change the trigger point of the scroll.

An example of this is the progress bar in blog posts to the right of the screen, when viewing the blog on desktop.

import { MutableRefObject, useMemo } from 'react';
import { useWindowScroll } from 'react-use';

import { useDistanceFromTop } from '..';

type UseRelativeScrollPercentageResult = [
    MutableRefObject<HTMLElement>,
    MutableRefObject<HTMLElement>,
    { percentage: number }
];

export const useRelativeScrollPercentage = (offset = 0): UseRelativeScrollPercentageResult => {
    const [fromRef, { distance: fromDistance }] = useDistanceFromTop();
    const [toRef, { distance: toDistance }] = useDistanceFromTop();
    const { y } = useWindowScroll();

    const current = y - fromDistance + offset;
    const end = toDistance - fromDistance;

    const percentage = useMemo(() => {
        return (current / end) * 100;
    }, [current, end, y]);

    return [fromRef, toRef, { percentage: percentage }];
};

0%

Discuss on Twitter
Share on Twitter

Written by Adam Young

Adam Young is a front-end engineer, who specializes in React and modern web technologies. He's working at Curve as a front-end engineer. He currently lives in Birmingham with his fiancรฉ and two cats.


Related Articles


Socials

Contact

Q&AEmail

All rights reserved ยฉ Adam Young 2022

Source code on Github