Repository: alibaba/hooks
Pull request #1042 (2021-07-18 08:50:07)

feat: add useDeepCompareEffect


Patch:
diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts
index 8270478b..21affac5 100644
--- a/packages/hooks/src/index.ts
+++ b/packages/hooks/src/index.ts
@@ -61,6 +61,7 @@ import useExternal from './useExternal';
 import useSafeState from './useSafeState';
 import useLatest from './useLatest';
 import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect';
+import useDeepCompareEffect from './useDeepCompareEffect';
 
 export {
   useControllableValue,
@@ -128,4 +129,5 @@ export {
   useSafeState,
   useLatest,
   useIsomorphicLayoutEffect,
+  useDeepCompareEffect,
 };
diff --git a/packages/hooks/src/useClickAway/index.ts b/packages/hooks/src/useClickAway/index.ts
index 722f4e46..f0681bfa 100644
--- a/packages/hooks/src/useClickAway/index.ts
+++ b/packages/hooks/src/useClickAway/index.ts
@@ -1,7 +1,8 @@
-import { useEffect, useMemo } from 'react';
+import { useMemo } from 'react';
+import useDeepCompareEffect from '../useDeepCompareEffect';
 import useLatest from '../useLatest';
-import type { BasicTarget } from '../utils/dom2';
 import { getTargetElement } from '../utils/dom2';
+import type { BasicTarget } from '../utils/dom2';
 
 type EventType = MouseEvent | TouchEvent;
 
@@ -17,7 +18,7 @@ export default function useClickAway(
     return targets.map((item) => (typeof target === 'function' ? undefined : item));
   }, [target]);
 
-  useEffect(() => {
+  useDeepCompareEffect(() => {
     const handler = (event: any) => {
       const targets = Array.isArray(target) ? target : [target];
       if (
@@ -36,5 +37,5 @@ export default function useClickAway(
     return () => {
       document.removeEventListener(eventName, handler);
     };
-  }, [...deps, eventName]);
+  }, [deps, eventName]);
 }
diff --git a/packages/hooks/src/useDeepCompareEffect/demo/demo1.tsx b/packages/hooks/src/useDeepCompareEffect/demo/demo1.tsx
new file mode 100644
index 00000000..30cc1375
--- /dev/null
+++ b/packages/hooks/src/useDeepCompareEffect/demo/demo1.tsx
@@ -0,0 +1,39 @@
+/**
+ * title: Default usage
+ * desc: Deep compare.
+ *
+ * title.zh-CN: 基础用法
+ * desc.zh-CN: 深度比较
+ */
+
+import { useDeepCompareEffect } from 'ahooks';
+import React, { useEffect, useState, useRef } from 'react';
+
+export default () => {
+  const [count, setCount] = useState(0);
+  const effectCountRef = useRef(0);
+  const deepCompareCountRef = useRef(0);
+
+  useEffect(() => {
+    effectCountRef.current += 1;
+  }, [{}]);
+
+  useDeepCompareEffect(() => {
+    deepCompareCountRef.current += 1;
+    return () => {
+      // do something
+    };
+  }, [{}]);
+
+  return (
+    <div>
+      <p>effectCount: {effectCountRef.current}</p>
+      <p>deepCompareCount: {deepCompareCountRef.current}</p>
+      <p>
+        <button type="button" onClick={() => setCount((c) => c + 1)}>
+          reRender
+        </button>
+      </p>
+    </div>
+  );
+};
diff --git a/packages/hooks/src/useDeepCompareEffect/index.en-US.md b/packages/hooks/src/useDeepCompareEffect/index.en-US.md
new file mode 100644
index 00000000..23336f88
--- /dev/null
+++ b/packages/hooks/src/useDeepCompareEffect/index.en-US.md
@@ -0,0 +1,29 @@
+---
+title: useDeepCompareEffect
+nav:
+  title: Hooks
+  path: /hooks
+group:
+  title: SideEffect
+  path: /side-effect
+---
+
+# useDeepCompareEffect
+
+Usage is the same as `useEffect`, but deps are compared with [lodash.isEqual](https://lodash.com/docs/4.17.15#isEqual).
+
+
+## Examples
+
+### Default usage
+
+<code src="./demo/demo1.tsx" />
+
+## API
+
+```typescript
+useDeepCompareEffect(
+  effect: React.EffectCallback,
+  deps: React.DependencyList
+);
+```
diff --git a/packages/hooks/src/useDeepCompareEffect/index.tsx b/packages/hooks/src/useDeepCompareEffect/index.tsx
new file mode 100644
index 00000000..010cca71
--- /dev/null
+++ b/packages/hooks/src/useDeepCompareEffect/index.tsx
@@ -0,0 +1,21 @@
+import isEqual from 'lodash.isequal';
+import { useEffect, useRef } from 'react';
+import type { DependencyList, EffectCallback } from 'react';
+
+const depsEqual = (aDeps: DependencyList, bDeps: DependencyList = []) => {
+  return isEqual(aDeps, bDeps);
+};
+
+const useDeepCompareEffect = (effect: EffectCallback, deps: DependencyList) => {
+  const ref = useRef<DependencyList>();
+  const signalRef = useRef<number>(0);
+
+  if (!depsEqual(deps, ref.current)) {
+    ref.current = deps;
+    signalRef.current += 1;
+  }
+
+  useEffect(effect, [signalRef.current]);
+};
+
+export default useDeepCompareEffect;
diff --git a/packages/hooks/src/useDeepCompareEffect/index.zh-CN.md b/packages/hooks/src/useDeepCompareEffect/index.zh-CN.md
new file mode 100644
index 00000000..faf77807
--- /dev/null
+++ b/packages/hooks/src/useDeepCompareEffect/index.zh-CN.md
@@ -0,0 +1,29 @@
+---
+title: useDeepCompareEffect
+nav:
+  title: Hooks
+  path: /hooks
+group:
+  title: SideEffect
+  path: /side-effect
+---
+
+# useDeepCompareEffect
+
+用法与 `useEffect` 一致，但 deps 通过 [lodash.isEqual](https://lodash.com/docs/4.17.15#isEqual) 进行深比较。
+
+
+## 代码演示
+
+### 基础用法
+
+<code src="./demo/demo1.tsx" />
+
+## API
+
+```typescript
+useDeepCompareEffect(
+  effect: React.EffectCallback,
+  deps: React.DependencyList
+);
+```


Test patch:
diff --git a/packages/hooks/src/useDeepCompareEffect/__tests__/index.test.ts b/packages/hooks/src/useDeepCompareEffect/__tests__/index.test.ts
new file mode 100644
index 00000000..a15c4f5f
--- /dev/null
+++ b/packages/hooks/src/useDeepCompareEffect/__tests__/index.test.ts
@@ -0,0 +1,25 @@
+import { renderHook } from '@testing-library/react-hooks';
+import useDeepCompareEffect from '../index';
+
+describe('useDeepCompareEffect', () => {
+  it('should be defined', () => {
+    expect(useDeepCompareEffect).toBeDefined();
+  });
+
+  it('test deep compare', () => {
+    let mountedState = 1;
+    const hook = renderHook(() =>
+      useDeepCompareEffect(() => {
+        mountedState += 1;
+        return () => {
+          mountedState = 0;
+        };
+      }, [{}]),
+    );
+    expect(mountedState).toEqual(2);
+    hook.rerender();
+    expect(mountedState).toEqual(2);
+    hook.unmount();
+    expect(mountedState).toEqual(0);
+  });
+});