persistedStore.tsx 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. import React, {
  2. createContext,
  3. useCallback,
  4. useContext,
  5. useEffect,
  6. useMemo,
  7. useState,
  8. } from 'react';
  9. import {useMutation} from '@tanstack/react-query';
  10. import useApi from 'sentry/utils/useApi';
  11. import useOrganization from 'sentry/utils/useOrganization';
  12. import {OnboardingState} from 'sentry/views/onboarding/types';
  13. import OrganizationStore from './organizationStore';
  14. import {useLegacyStore} from './useLegacyStore';
  15. type PersistedStore = Readonly<{
  16. onboarding: Partial<OnboardingState> | null;
  17. }>;
  18. export const DefaultPersistedStore: PersistedStore = {
  19. onboarding: null,
  20. };
  21. export const DefaultLoadedPersistedStore: PersistedStore = {
  22. onboarding: {},
  23. };
  24. type PersistedStoreContextValue = [
  25. PersistedStore,
  26. React.Dispatch<React.SetStateAction<PersistedStore>>
  27. ];
  28. export const PersistedStoreContext = createContext<PersistedStoreContextValue | null>(
  29. null
  30. );
  31. function usePersistedStore(): PersistedStoreContextValue {
  32. const context = useContext(PersistedStoreContext);
  33. if (!context) {
  34. throw new Error('usePersistedStore was called outside of PersistedStoreProvider');
  35. }
  36. return context;
  37. }
  38. // Client-only state with TTL persisted on the server side in a redis store.
  39. export function PersistedStoreProvider(props: {children: React.ReactNode}) {
  40. const [state, setState] = useState<PersistedStore>(DefaultPersistedStore);
  41. const api = useApi();
  42. const {organization} = useLegacyStore(OrganizationStore);
  43. useEffect(() => {
  44. if (!organization) {
  45. return undefined;
  46. }
  47. let shouldCancelRequest = false;
  48. api
  49. .requestPromise(`/organizations/${organization.slug}/client-state/`)
  50. .then((response: PersistedStore) => {
  51. if (shouldCancelRequest) {
  52. return;
  53. }
  54. setState({...DefaultLoadedPersistedStore, ...response});
  55. })
  56. .catch(() => {
  57. setState(DefaultPersistedStore);
  58. });
  59. return () => {
  60. shouldCancelRequest = true;
  61. };
  62. }, [api, organization]);
  63. return (
  64. <PersistedStoreContext.Provider value={[state, setState]}>
  65. {props.children}
  66. </PersistedStoreContext.Provider>
  67. );
  68. }
  69. type UsePersistedCategory<T> = [T | null, (nextState: T | null) => void];
  70. export function usePersistedStoreCategory<C extends keyof PersistedStore>(
  71. category: C
  72. ): UsePersistedCategory<PersistedStore[C]> {
  73. const api = useApi({persistInFlight: true});
  74. const organization = useOrganization();
  75. const [state, setState] = usePersistedStore();
  76. const endpointLocation = `/organizations/${organization.slug}/client-state/${category}/`;
  77. const {mutate: clearState} = useMutation({
  78. mutationFn: () =>
  79. api.requestPromise(endpointLocation, {
  80. method: 'DELETE',
  81. }),
  82. retry: 3,
  83. });
  84. const {mutate: syncState} = useMutation({
  85. mutationFn: (val: PersistedStore[C]) =>
  86. api.requestPromise(endpointLocation, {
  87. method: 'PUT',
  88. data: val,
  89. }),
  90. retry: 3,
  91. });
  92. const setCategoryState = useCallback(
  93. (val: PersistedStore[C] | null) => {
  94. setState(oldState => ({...oldState, [category]: val}));
  95. // If a state is set with null, we can clear it from the server.
  96. if (val === null) {
  97. clearState();
  98. return;
  99. }
  100. // Else we want to sync our state with the server
  101. syncState(val);
  102. },
  103. [setState, syncState, category, clearState]
  104. );
  105. const result = state[category];
  106. const stableState: UsePersistedCategory<PersistedStore[C]> = useMemo(() => {
  107. return [result ?? null, setCategoryState];
  108. }, [result, setCategoryState]);
  109. return stableState;
  110. }