context.tsx 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. import {createContext, useCallback, useContext, useEffect, useState} from 'react';
  2. import type {PlainRoute} from 'sentry/types/legacyReactRouter';
  3. import getRouteStringFromRoutes from 'sentry/utils/getRouteStringFromRoutes';
  4. import {useRoutes} from 'sentry/utils/useRoutes';
  5. type ExplicitTitleProps = {
  6. routes: PlainRoute[];
  7. title: string;
  8. };
  9. type PathMap = Record<string, string>;
  10. type Context = {
  11. /**
  12. * Represents a mapping of paths to breadcrumb names
  13. */
  14. pathMap: PathMap;
  15. /**
  16. * Set's an explicit breadcrumb title at the provided routes path.
  17. *
  18. * You should call the returned cleanup function to have the explicit title
  19. * removed when appropriate (typically on unmount of the route component)
  20. */
  21. setExplicitTitle: (update: ExplicitTitleProps) => () => void;
  22. };
  23. const BreadcrumbContext = createContext<Context | undefined>(undefined);
  24. type ProviderProps = {
  25. children: React.ReactNode;
  26. };
  27. function BreadcrumbProvider({children}: ProviderProps) {
  28. const routes = useRoutes();
  29. // Maps path strings to breadcrumb names
  30. const [pathMap, setPathMap] = useState<PathMap>({});
  31. // The explicit path map is used when we override the name for a specific
  32. // route. We keep this separate from the path map which is resolved from the
  33. // `routes` match array so that that does not override an explicit title
  34. const [explicitPathMap, setExplicitPathMap] = useState<PathMap>({});
  35. // When our routes change update the path mapping
  36. useEffect(
  37. () =>
  38. setPathMap(oldPathMap => {
  39. const routePath = getRouteStringFromRoutes(routes);
  40. const newPathMap = {...oldPathMap};
  41. for (const fullPath in newPathMap) {
  42. if (!routePath.startsWith(fullPath)) {
  43. delete newPathMap[fullPath];
  44. }
  45. }
  46. return newPathMap;
  47. }),
  48. [routes, setPathMap]
  49. );
  50. const setExplicitTitle = useCallback(
  51. ({routes: updateRoutes, title}: ExplicitTitleProps) => {
  52. const key = getRouteStringFromRoutes(updateRoutes);
  53. setExplicitPathMap(lastState => ({...lastState, [key]: title}));
  54. return () =>
  55. setExplicitPathMap(lastState => {
  56. const {[key]: _removed, ...newExplicitPathMap} = lastState;
  57. return newExplicitPathMap;
  58. });
  59. },
  60. [setExplicitPathMap]
  61. );
  62. const ctx: Context = {
  63. pathMap: {...pathMap, ...explicitPathMap},
  64. setExplicitTitle,
  65. };
  66. return <BreadcrumbContext.Provider value={ctx}>{children}</BreadcrumbContext.Provider>;
  67. }
  68. /**
  69. * Provides the mapping of paths to breadcrumb titles.
  70. *
  71. * Outside of the BreadcrumbContext this will return an empty mapping
  72. */
  73. function useBreadcrumbsPathmap() {
  74. return useContext(BreadcrumbContext)?.pathMap ?? {};
  75. }
  76. /**
  77. * Used to set the breadcrumb title of the passed route while the current
  78. * component is rendererd.
  79. *
  80. * Is a no-op if used outside of the BreadcrumbContext.
  81. */
  82. function useBreadcrumbTitleEffect(props: ExplicitTitleProps) {
  83. const context = useContext(BreadcrumbContext);
  84. const setExplicitTitle = context?.setExplicitTitle;
  85. useEffect(() => setExplicitTitle?.(props), [setExplicitTitle, props]);
  86. }
  87. export {BreadcrumbProvider, useBreadcrumbsPathmap, useBreadcrumbTitleEffect};