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