Skip to content

Instantly share code, notes, and snippets.

@mkarajohn
Last active February 13, 2024 16:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mkarajohn/89487d77e258febad32a056b1927fe83 to your computer and use it in GitHub Desktop.
Save mkarajohn/89487d77e258febad32a056b1927fe83 to your computer and use it in GitHub Desktop.
A React hook that propagates the value of a text input to a specific URL param after a small delay. This implementation depends on react-router-dom ^5.x.x but can be modified to use another solution
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];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment