vitess.io/vitess@v0.16.2/web/vtadmin/src/hooks/useSyncedURLParam.ts (about) 1 /** 2 * Copyright 2021 The Vitess Authors. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 import { useCallback } from 'react'; 17 18 import { useURLQuery } from './useURLQuery'; 19 20 /** 21 * useSyncedURLValue is a hook for synchronizing a string between a component 22 * and the URL. It is optimized for values that change quickly, like user input. 23 * 24 * Note: the value returned is always a string, so any formatting/parsing is 25 * left to the caller. 26 * 27 * @param key - The key for the URL parameter. A key of "search", for example, would 28 * manipulate the `?search=...` value in the URL. 29 */ 30 export const useSyncedURLParam = ( 31 key: string 32 ): { 33 updateValue: (nextValue: string | null | undefined) => void; 34 // `value` is always a string, since (a) the value in the URL will 35 // be a string in the end :) and (b) primitive values like strings are much, 36 // much easier to memoize and cache. This means all parsing/formatting is 37 // left to the caller. 38 value: string | null | undefined; 39 } => { 40 // TODO(doeg): a potentially nice enhancement is to maintain an ephemeral cache 41 // (optionally) mapping routes to the last used set of URL parameters. 42 // So, for example, if you were (1) on the /tablets view, (2) updated the "filter" parameter, 43 // (3) navigated away, and then (4) clicked a nav link back to /tablets, the "filter" parameter 44 // parameter you typed in (2) will be lost, since it's only preserved on the history stack, 45 // which is only traversable with the "back" button. 46 47 // Ensure we never parse booleans/numbers since the contract (noted above) is that the value is always a string. 48 const { query, pushQuery, replaceQuery } = useURLQuery({ parseBooleans: false, parseNumbers: false }); 49 const value = `${query[key] || ''}`; 50 51 const updateValue = useCallback( 52 (nextValue: string | null | undefined) => { 53 if (!nextValue) { 54 // Push an undefined value to omit the parameter from the URL. 55 // This gives us URLs like `?goodbye=moon` instead of `?hello=&goodbye=moon`. 56 pushQuery({ [key]: undefined }); 57 } else if (nextValue && !value) { 58 // Replace the current value with a new value. There's a bit of nuance here, since this 59 // means that clearing the input is the _only_ way to persist discrete values to 60 // the history stack, which is not very intuitive or delightful! 61 // 62 // TODO(doeg): One possible, more nuanced re-implementation is to push entries onto the stack 63 // on a timeout. This means you could type a query, pause and click around a bit, 64 // and then type a new query -- both queries would be persisted to the stack, 65 // without resorting to calling `pushQuery` on _every_ character typed. 66 pushQuery({ [key]: nextValue }); 67 } else { 68 // Replace the current value in the URL with a new one. The previous 69 // value (that was replaced) will no longer be available (i.e., it won't 70 // be accessible via the back button). 71 // 72 // We use replaceQuery instead of pushQuery, as pushQuery would push 73 // every single letter onto the history stack, which means every click 74 // of the back button would iterate backwards, one letter at a time. 75 replaceQuery({ [key]: nextValue }); 76 } 77 }, 78 [key, value, pushQuery, replaceQuery] 79 ); 80 81 return { value, updateValue }; 82 };