vitess.io/vitess@v0.16.2/web/vtadmin/src/hooks/useURLQuery.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, useMemo } from 'react';
    17  import { useHistory, useLocation } from 'react-router-dom';
    18  import { ArrayFormatType, parse, QueryParams, stringify } from '../util/queryString';
    19  
    20  export interface URLQueryOptions {
    21      arrayFormat?: ArrayFormatType;
    22      parseBooleans?: boolean;
    23      parseNumbers?: boolean;
    24  }
    25  
    26  /**
    27   * useURLQuery is a hook for getting and setting query parameters from the current URL,
    28   * where "query parameters" are those appearing after the "?":
    29   *
    30   *      https://test.com/some/route?foo=bar&count=123&list=one&list=two&list=3
    31   *                                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    32   *
    33   * The query parameters from the above URL would be parsed as:
    34   *
    35   *      { foo: "bar", count: 123, list: ["one", "two", "three"] }
    36   *
    37   * For lots more usage examples, see the useURLQuery unit tests.
    38   */
    39  export const useURLQuery = (
    40      opts: URLQueryOptions = {}
    41  ): {
    42      /**
    43       * The current URL query parameters, parsed into an object.
    44       */
    45      query: QueryParams;
    46  
    47      /**
    48       * `pushQuery` merges `nextQuery` with the current query parameters
    49       * and pushes the resulting search string onto the history stack.
    50       *
    51       * This does not affect location.pathname: if your current path
    52       * is "/test?greeting=hello", then calling `pushQuery({ greeting: "hi" })`
    53       * will push "/test?greeting=hi". If you *do* want to update the pathname,
    54       * then use useHistory()'s history.push directly.
    55       */
    56      pushQuery: (nextQuery: QueryParams) => void;
    57  
    58      /**
    59       * `replaceQuery` merges `nextQuery` with the current query parameters
    60       * and replaces the resulting search string onto the history stack.
    61       *
    62       * This does not affect location.pathname: if your current path
    63       * is "/test?greeting=hello", then calling `replaceQuery({ greeting: "hi" })`
    64       * will replace "/test?greeting=hi". If you *do* want to update the pathname,
    65       * then use useHistory()'s history.replace directly.
    66       */
    67      replaceQuery: (nextQuery: QueryParams) => void;
    68  } => {
    69      const history = useHistory();
    70      const location = useLocation();
    71  
    72      // A spicy note: typically, we always want to use the `location` from useLocation() instead of useHistory().
    73      // From the React documentation: https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/history.md#history-is-mutable
    74      //
    75      //      The history object is mutable. Therefore it is recommended to access the location from the render props of <Route>,
    76      //      not from history.location. This ensures your assumptions about React are correct in lifecycle hooks.
    77      //
    78      // However, in a *test* environment, the "?...string" one usually finds at `location.search`
    79      // is (confusingly) nested at `location.location.search`. This seems like a discrepancy between how
    80      // `history.push` + `history.replace` calls are handled by `Router` + memory history (used for tests)
    81      // vs. `BrowserRouter` (used "for real", in the browser).
    82      //
    83      // So, in practice, this `search` variable is set to `location.search` "for real" (in the browser)
    84      // and only falls back to `location.location.search` for tests. It's... not ideal. :/ But it seems to work.
    85      const search = location.search || history.location.search;
    86  
    87      // Destructure `opts` for more granular useMemo and useCallback dependencies.
    88      const { arrayFormat, parseBooleans, parseNumbers } = opts;
    89  
    90      // Parse the URL search string into a mapping from URL parameter key to value.
    91      const query = useMemo(
    92          () =>
    93              parse(search, {
    94                  arrayFormat,
    95                  parseBooleans,
    96                  parseNumbers,
    97              }),
    98          [search, arrayFormat, parseBooleans, parseNumbers]
    99      );
   100  
   101      const pushQuery = useCallback(
   102          (nextQuery: QueryParams) => {
   103              const nextSearch = stringify({ ...query, ...nextQuery }, { arrayFormat });
   104              return history.push({ search: `?${nextSearch}` });
   105          },
   106          [arrayFormat, history, query]
   107      );
   108  
   109      const replaceQuery = useCallback(
   110          (nextQuery: QueryParams) => {
   111              const nextSearch = stringify({ ...query, ...nextQuery }, { arrayFormat });
   112              return history.replace({ search: `?${nextSearch}` });
   113          },
   114          [arrayFormat, history, query]
   115      );
   116  
   117      return { query, pushQuery, replaceQuery };
   118  };