Skip to content

Instantly share code, notes, and snippets.

@mkarajohn
Last active February 13, 2024 16:31

Revisions

  1. mkarajohn revised this gist Feb 13, 2024. No changes.
  2. mkarajohn created this gist Feb 9, 2024.
    96 changes: 96 additions & 0 deletions useTextInputToDelayedURLParam.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,96 @@
    import { useEffect, useState } from 'react';
    import { useHistory, useLocation } from 'react-router-dom';

    /*
    * The following hook makes use of this technique
    * https://react.dev/reference/react/useState#storing-information-from-previous-renders
    * thus cutting down the amount of re-renders to a minimum.
    * Do not freak out by the lack of useEffects
    * // 2024 Dimitris Karagiannis
    * // Twitter: @MitchKarajohn
    *
    * Problem: We have a text input whose value is reflected in the URL. In reality, we want the
    * URL to be the source of truth and the text input to reflect the URL param. This would be fairly
    * easy to implement except for the fact that when the URL param changes we are sending a GET
    * request to the server, so we cannot obviously fire a request for each letter the user types.
    * (canceling the request is not guaranteed to cancel the server processing, it only ignores the response)
    * Also we would rather not push every single letter to the URL, because that would mess up our
    * history stack (imagine navigating back by 1 letter each time, not a good experience)
    *
    * Solution: This hook makes the input act as a temporary buffer before flushing to the URL and
    * making a request. What this hook does is the following:
    * * if the user enters new input, the change is immediately reflected in the input, but it's flushed
    * to the url after a small delay, this way we only make 1 change to the URL and 1 request for
    * new data once the user has stopped typing
    * * if the url param changes programmatically or by back/forwards navigation, the change is
    * IMMEDIATELY reflected to the input
    */
    export function useTextInputToDelayedURLParam(paramKey: string, delay: number = 700) {
    const history = useHistory();
    const location = useLocation();

    const urlParams = new URLSearchParams(location.search);
    const currentParamValue = urlParams.get(paramKey) || '';
    const [prevParamValue, setPrevParamValue] = useState(currentParamValue);

    const [inputState, setInputState] = useState(currentParamValue);
    const [prevInputState, setPrevInputState] = useState(inputState);

    const [pendingURLParamUpdate, setPendingURLParamUpdate] = useState(false);

    // This means that
    // * either the user entered a new input
    // * or there was a change to the url param which forced a new inputState
    if (prevInputState !== inputState) {
    // we first set the local prevInputState to the new inputState so that we do not re-enter this condition
    // in subsequent re-renders
    setPrevInputState(inputState);
    // We set the flag for a pending url param update
    setPendingURLParamUpdate(true);
    }

    // This means that
    // * either the url params have changed programmatically/due to back/forw navigation
    // * or that there was a change in the text input which forced a new url param after a delay
    if (prevParamValue !== currentParamValue) {
    // we first set the local prevParamValue to the new one so that we do not re-enter this condition
    // in subsequent re-renders
    setPrevParamValue(currentParamValue);
    // we immediately update the current and previous input inputState in order to reflect the url param
    setInputState(currentParamValue);
    setPrevInputState(currentParamValue);
    }

    // This effect is responsible for updating the URL param after a delay
    useEffect(() => {
    if (pendingURLParamUpdate) {
    const urlParams = new URLSearchParams(location.search);
    const timeout = window.setTimeout(() => {
    // we update the url params based on user input
    if (inputState === '') {
    urlParams.delete(paramKey);
    } else {
    urlParams.set(paramKey, inputState);
    }
    // and then push the new url params to history in order to reflect the user input in the
    // address bar
    history.push({
    search: urlParams.toString(),
    });

    // reset the url param update flag
    setPendingURLParamUpdate(false);
    }, delay);

    return function () {
    if (timeout) {
    // Clean up any pending setTimeouts so that the url params do not change after we navigate away
    // from the page
    window.clearTimeout(timeout);
    }
    };
    }
    }, [delay, history, inputState, location.search, paramKey, pendingURLParamUpdate]);

    return [inputState, setInputState] as [typeof inputState, typeof setInputState];
    }