Last active
February 13, 2024 16:31
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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