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  };