useQueryParamState.tsx 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. import {useCallback, useState} from 'react';
  2. import * as Sentry from '@sentry/react';
  3. import {defined} from 'sentry/utils';
  4. import {type decodeList, decodeScalar, type decodeSorts} from 'sentry/utils/queryString';
  5. import useLocationQuery from 'sentry/utils/url/useLocationQuery';
  6. import {useUrlBatchContext} from '../contexts/urlParamBatchContext';
  7. interface UseQueryParamStateWithScalarDecoder<T> {
  8. fieldName: string;
  9. decoder?: typeof decodeScalar;
  10. deserializer?: (value: ReturnType<typeof decodeScalar>) => T;
  11. serializer?: (value: T) => string;
  12. }
  13. interface UseQueryParamStateWithListDecoder<T> {
  14. decoder: typeof decodeList;
  15. fieldName: string;
  16. deserializer?: (value: ReturnType<typeof decodeList>) => T;
  17. serializer?: (value: T) => string[];
  18. }
  19. interface UseQueryParamStateWithSortsDecoder<T> {
  20. decoder: typeof decodeSorts;
  21. fieldName: string;
  22. serializer: (value: T) => string[];
  23. deserializer?: (value: ReturnType<typeof decodeSorts>) => T;
  24. }
  25. type UseQueryParamStateProps<T> =
  26. | UseQueryParamStateWithScalarDecoder<T>
  27. | UseQueryParamStateWithListDecoder<T>
  28. | UseQueryParamStateWithSortsDecoder<T>;
  29. /**
  30. * Hook to manage a state that is synced with a query param in the URL
  31. *
  32. * @param fieldName - The name of the query param to sync with the state
  33. * @param deserializer - A function to transform the query param value into the desired type
  34. * @returns A tuple containing the current state and a function to update the state
  35. */
  36. export function useQueryParamState<T = string>({
  37. fieldName,
  38. decoder,
  39. deserializer,
  40. serializer,
  41. }: UseQueryParamStateProps<T>): [T | undefined, (newField: T | undefined) => void] {
  42. const {batchUrlParamUpdates} = useUrlBatchContext();
  43. // The URL query params give us our initial state
  44. const parsedQueryParams = useLocationQuery({
  45. fields: {
  46. [fieldName]: decoder ?? decodeScalar,
  47. },
  48. });
  49. const [localState, setLocalState] = useState<T | undefined>(() => {
  50. const decodedValue = parsedQueryParams[fieldName];
  51. if (!defined(decodedValue)) {
  52. return undefined;
  53. }
  54. return deserializer
  55. ? deserializer(decodedValue as any)
  56. : // TODO(nar): This is a temporary fix to avoid type errors
  57. // When the deserializer isn't provided, we should return the value
  58. // if T is a string, or else return undefined
  59. (decodedValue as T);
  60. });
  61. const updateField = useCallback(
  62. (newField: T | undefined) => {
  63. setLocalState(newField);
  64. if (!defined(newField)) {
  65. batchUrlParamUpdates({[fieldName]: undefined});
  66. } else if (serializer) {
  67. batchUrlParamUpdates({[fieldName]: serializer(newField)});
  68. } else {
  69. // At this point, only update the query param if the new field is a string, number, boolean, or array
  70. if (
  71. ['string', 'number', 'boolean'].includes(typeof newField) ||
  72. Array.isArray(newField)
  73. ) {
  74. batchUrlParamUpdates({[fieldName]: newField as any});
  75. } else {
  76. Sentry.captureException(
  77. new Error(
  78. 'useQueryParamState: newField is not a primitive value and not provided a serializer'
  79. )
  80. );
  81. batchUrlParamUpdates({[fieldName]: undefined});
  82. }
  83. }
  84. },
  85. [batchUrlParamUpdates, serializer, fieldName]
  86. );
  87. return [localState, updateField];
  88. }