Browse Source

feat(ui): Add useIsMountedRef hook (#41098)

Scott Cooper 2 years ago
parent
commit
7da03aaef1

+ 5 - 8
static/app/components/avatar/gravatar.tsx

@@ -1,8 +1,9 @@
-import {useCallback, useEffect, useRef, useState} from 'react';
+import {useCallback, useEffect, useState} from 'react';
 import styled from '@emotion/styled';
 import * as qs from 'query-string';
 
 import ConfigStore from 'sentry/stores/configStore';
+import {useIsMountedRef} from 'sentry/utils/useIsMountedRef';
 
 import {imageStyle, ImageStyleProps} from './styles';
 
@@ -25,25 +26,21 @@ function Gravatar({
   onLoad,
   suggested,
 }: Props) {
-  const isMounted = useRef(false);
+  const isMountedRef = useIsMountedRef();
   const [MD5, setMD5] = useState<HasherHelper>();
 
   const loadMd5Helper = useCallback(async () => {
     const mod = await import('crypto-js/md5');
 
-    if (isMounted.current) {
+    if (isMountedRef.current) {
       // XXX: Use function invocation of `useState`s setter since the mod.default
       // is a function itself.
       setMD5(() => mod.default);
     }
-  }, []);
+  }, [isMountedRef]);
 
   useEffect(() => {
-    isMounted.current = true;
     loadMd5Helper();
-    return () => {
-      isMounted.current = false;
-    };
   }, [loadMd5Helper]);
 
   if (MD5 === undefined) {

+ 49 - 0
static/app/utils/useIsMountedRef.spec.tsx

@@ -0,0 +1,49 @@
+import {reactHooks} from 'sentry-test/reactTestingLibrary';
+
+import {useIsMountedRef} from './useIsMountedRef';
+
+describe('useIsMounted', () => {
+  it('should return a ref', () => {
+    const {result} = reactHooks.renderHook(() => useIsMountedRef());
+
+    expect(result.current).toBeInstanceOf(Object);
+  });
+
+  it('should return false within first render', () => {
+    const {result} = reactHooks.renderHook(() => {
+      const isMountedRef = useIsMountedRef();
+      return isMountedRef.current;
+    });
+
+    expect(result.current).toBe(false);
+  });
+
+  it('should return true after mount', () => {
+    const {result} = reactHooks.renderHook(() => useIsMountedRef());
+
+    expect(result.current.current).toBe(true);
+  });
+
+  it('should return same function on each render', () => {
+    const {result, rerender} = reactHooks.renderHook(() => useIsMountedRef());
+
+    const fn1 = result.current;
+    rerender();
+    const fn2 = result.current;
+    rerender();
+    const fn3 = result.current;
+
+    expect(fn1).toBe(fn2);
+    expect(fn2).toBe(fn3);
+  });
+
+  it('should return false after component unmount', () => {
+    const {result, unmount} = reactHooks.renderHook(() => useIsMountedRef());
+
+    expect(result.current.current).toBe(true);
+
+    unmount();
+
+    expect(result.current.current).toBe(false);
+  });
+});

+ 21 - 0
static/app/utils/useIsMountedRef.tsx

@@ -0,0 +1,21 @@
+import {useEffect, useRef} from 'react';
+
+/**
+ * Returns a ref that captures the current mounted state of the component
+ *
+ * This hook is handy for the cases when you have to detect component mount state
+ * within async effects.
+ */
+export function useIsMountedRef() {
+  const isMounted = useRef(false);
+
+  useEffect(() => {
+    isMounted.current = true;
+
+    return () => {
+      isMounted.current = false;
+    };
+  }, []);
+
+  return isMounted;
+}

+ 8 - 2
static/app/views/alerts/builder/projectProvider.tsx

@@ -8,6 +8,7 @@ import LoadingIndicator from 'sentry/components/loadingIndicator';
 import {t} from 'sentry/locale';
 import {Member, Organization} from 'sentry/types';
 import useApi from 'sentry/utils/useApi';
+import {useIsMountedRef} from 'sentry/utils/useIsMountedRef';
 import useProjects from 'sentry/utils/useProjects';
 import useScrollToTop from 'sentry/utils/useScrollToTop';
 
@@ -23,6 +24,7 @@ type RouteParams = {
 
 function AlertBuilderProjectProvider(props: Props) {
   const api = useApi();
+  const isMountedRef = useIsMountedRef();
   const [members, setMembers] = useState<Member[] | undefined>(undefined);
   useScrollToTop({location: props.location});
 
@@ -48,8 +50,12 @@ function AlertBuilderProjectProvider(props: Props) {
     }
 
     // fetch members list for mail action fields
-    fetchOrgMembers(api, organization.slug, [project.id]).then(mem => setMembers(mem));
-  }, [api, organization, project]);
+    fetchOrgMembers(api, organization.slug, [project.id]).then(mem => {
+      if (isMountedRef.current) {
+        setMembers(mem);
+      }
+    });
+  }, [api, organization, isMountedRef, project]);
 
   if (!initiallyLoaded || fetching) {
     return <LoadingIndicator />;