persistedStore.tsx 3.2 KB

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