Repository: alibaba/hooks
Pull request #1103 (2021-08-03 15:43:06)

feat: introduce create-update-effect
<!--
First of all, thank you for your contribution! 😄

New feature please send a pull request to next branch, and rest to master branch.
Pull requests will be merged after one of the collaborators approve.
Please makes sure that these forms are filled before submitting your pull request, thank you!
-->

[[中文版模板 / Chinese template](https://github.com/alibaba/hooks/blob/master/.github/PULL_REQUEST_TEMPLATE/pr_cn.md)]

### 🤔 This is a ...

- [ ] New feature
- [ ] Bug fix
- [ ] Site / documentation update
- [ ] Demo update
- [ ] TypeScript definition update
- [ ] Bundle size optimization
- [ ] Performance optimization
- [ ] Enhancement feature
- [ ] Internationalization
- [ ] Refactoring
- [ ] Code style optimization
- [ ] Test Case
- [ ] Branch merge
- [ ] Other (about what?)

### 🔗 Related issue link

<!--
1. Describe the source of requirement, like related issue link.
-->

### 💡 Background and solution

<!--
1. Describe the problem and the scenario.
2. GIF or snapshot should be provided if includes UI/interactive modification.
3. How to fix the problem, and list final API implementation and usage sample if that is a new feature.
-->

### 📝 Changelog

<!--
Describe changes from the user side, and list all potential break changes or other risks.
--->

| Language   | Changelog |
| ---------- | --------- |
| 🇺🇸 English |           |
| 🇨🇳 Chinese |           |

### ☑️ Self Check before Merge

⚠️ Please check all items below before review. ⚠️

- [ ] Doc is updated/provided or not needed
- [ ] Demo is updated/provided or not needed
- [ ] TypeScript definition is updated/provided or not needed
- [ ] Changelog is provided or not needed



Patch:
diff --git a/packages/hooks/package.json b/packages/hooks/package.json
index 1fedc371..7149548f 100644
--- a/packages/hooks/package.json
+++ b/packages/hooks/package.json
@@ -33,8 +33,6 @@
   ],
   "dependencies": {
     "@ahooksjs/use-request": "^3.0.0-alpha.0",
-    "@ant-design/icons": "^4.6.2",
-    "@types/js-cookie": "^2.2.6",
     "core-js": "^2.6.12",
     "dayjs": "^1.9.1",
     "intersection-observer": "^0.7.0",
@@ -47,14 +45,15 @@
     "react": "^16.8.6 || ^17.0"
   },
   "devDependencies": {
+    "@ant-design/icons": "^4.6.2",
+    "@types/js-cookie": "^2.2.6",
     "@alifd/next": "^1.20.6",
     "@types/enzyme": "^3.10.5",
     "antd": "^4.16.9",
     "enzyme-adapter-react-16": "^1.15.4",
     "jest-websocket-mock": "^2.1.0",
     "mock-socket": "^9.0.3",
-    "react-drag-listview": "^0.1.6",
-    "umi-request": "^1.0.8"
+    "react-drag-listview": "^0.1.6"
   },
   "engines": {
     "node": ">=8.0.0"
diff --git a/packages/hooks/src/createUpdateEffect/index.ts b/packages/hooks/src/createUpdateEffect/index.ts
new file mode 100644
index 00000000..845893f6
--- /dev/null
+++ b/packages/hooks/src/createUpdateEffect/index.ts
@@ -0,0 +1,24 @@
+import { useRef } from 'react';
+import type { useEffect, useLayoutEffect } from 'react';
+
+type effectHookType = typeof useEffect | typeof useLayoutEffect;
+
+export const createUpdateEffect: (hook: effectHookType) => effectHookType =
+  (hook) => (effect, deps) => {
+    const isMounted = useRef(false);
+
+    // for react-refresh
+    hook(() => {
+      return () => {
+        isMounted.current = false;
+      };
+    }, []);
+
+    hook(() => {
+      if (!isMounted.current) {
+        isMounted.current = true;
+      } else {
+        return effect();
+      }
+    }, deps);
+  };
diff --git a/packages/hooks/src/useCookieState/demo/demo1.tsx b/packages/hooks/src/useCookieState/demo/demo1.tsx
index 8be3c78e..032f9021 100644
--- a/packages/hooks/src/useCookieState/demo/demo1.tsx
+++ b/packages/hooks/src/useCookieState/demo/demo1.tsx
@@ -1,12 +1,12 @@
 /**
  * title: Store state into cookie
- * desc: Refresh this page and you will find the value of input box get restored from cookie.
+ * desc: Refresh this page and you will find the value of input box gets restored from cookie.
  *
  * title.zh-CN: 将 state 存储在 cookie 中
  * desc.zh-CN: 刷新页面后，可以看到输入框中的内容被从 cookie 中恢复了。
  */
 
-import * as React from 'react';
+import React from 'react';
 import { useCookieState } from 'ahooks';
 
 export default () => {
diff --git a/packages/hooks/src/useCookieState/demo/demo2.tsx b/packages/hooks/src/useCookieState/demo/demo2.tsx
index 2e21e952..dc84161b 100644
--- a/packages/hooks/src/useCookieState/demo/demo2.tsx
+++ b/packages/hooks/src/useCookieState/demo/demo2.tsx
@@ -1,12 +1,12 @@
 /**
  * title: SetState can receive function
- * desc: Function updater is also acceptable with useCookieState's setState，linke useState.
+ * desc: Function updater is also acceptable with useCookieState's setState，similar to how useState is used.
  *
  * title.zh-CN: setState 可以接收函数
  * desc.zh-CN: useCookieState 的 setState 可以接收 function updater，就像 useState 那样。
  */
 
-import * as React from 'react';
+import React from 'react';
 import { useCookieState } from 'ahooks';
 
 export default function App() {
diff --git a/packages/hooks/src/useCookieState/demo/demo3.tsx b/packages/hooks/src/useCookieState/demo/demo3.tsx
index 6849afbc..e25bc53e 100644
--- a/packages/hooks/src/useCookieState/demo/demo3.tsx
+++ b/packages/hooks/src/useCookieState/demo/demo3.tsx
@@ -1,12 +1,12 @@
 /**
  * title: Use the option property to configure cookie
- * desc: Can be configured defaultValue、expires、path、domain、secure、sameSite etc.
+ * desc: Available options: defaultValue、expires、path、domain、secure、sameSite etc.
  *
  * title.zh-CN: 使用 option 配置 cookie
  * desc.zh-CN: 可配置属性：默认值、有效时间、路径、域名、协议、跨域等，详见 Options 文档。
  */
 
-import * as React from 'react';
+import React from 'react';
 import { useCookieState } from 'ahooks';
 
 export default function App() {
diff --git a/packages/hooks/src/useCookieState/index.en-US.md b/packages/hooks/src/useCookieState/index.en-US.md
index e158f6f4..6237ad09 100644
--- a/packages/hooks/src/useCookieState/index.en-US.md
+++ b/packages/hooks/src/useCookieState/index.en-US.md
@@ -12,7 +12,7 @@ group:
 
 <Tag lang="en-US" tags="ssr"></Tag>
 
-A Hook for store state into cookie.
+A Hook that stores state into cookie.
 
 ## Examples
 
@@ -44,23 +44,23 @@ const [state, setState]: [State, SetState] = useCookieState(
 )
 ```
 
-If you want to delete this record from document.cookie, you can use `setState()` or `setState(null)` or `setState(undefined)`.
+If you want to delete this record from document.cookie, use `setState()` or `setState(null)` or `setState(undefined)`.
 
 ### Params
 
 | Property       | Description                 | Type                 | Default |
 | ------------ | ------------------------ | -------------------- | ------ |
 | cookieKey    | Local cookie key | `string` | - |
-| options | Optional, Configure cookies, See options for details | `Options` | - |
+| options | `Optional`. Cookie configuration. See options below for more details | `Options` | - |
 
 ### Result
 
 | Property       | Description                 | Type                 |
 | -------- | ------------ | ---------------------------------------------------------------------------------------------------- |
 | state    | Local cookie value | `string` \| `undefined`  \|`null`                              |
-| setState | Configure cookies. According to the options attribute, sync to the local cookie | `SetState` |
+| setState | Update cookie value | `SetState` |
 
-setState can update cookie options, and merge with the options set by `useCookieState`.
+setState can update cookie options, which will be merged with the options set by `useCookieState`.
 
 `const targetOptions = {...options, ...updateOptions}`
 
@@ -68,11 +68,11 @@ setState can update cookie options, and merge with the options set by `useCookie
 
 | Property       | Description                 | Type                 | Default |
 | -------- | ------------------------------------------------- | --------------------- | ------ |
-| defaultValue | Optional, default value, but not sync to cookie | `string` \| `undefined` \| `(() => (string \| undefined))` | `undefined`
-| expires  | Optional, Set cookie validity time | `number` \| `Date` | - |
-| path | Optional, Define available paths | `string` | `/` |
-| domain | Optional, Define available domain. Default creation domain| `string` | - |
-| secure | Optional, https security protocol | `boolean` | `false` |
-| sameSite | Optional, Cookies cannot be sent with cross-domain requests | `strict` \| `lax` \| `none` | - |
-
-Options is same to [js-cookie attributes](https://github.com/js-cookie/js-cookie#cookie-attributes).
\ No newline at end of file
+| defaultValue | `Optional`. Default value, but not sync to cookie | `string` \| `undefined` \| `(() => (string \| undefined))` | `undefined`
+| expires  | `Optional`. Set cookie expiration time | `number` \| `Date` | - |
+| path | `Optional`. Specify available paths | `string` | `/` |
+| domain | `Optional`. Specify available domain. Default creation domain| `string` | - |
+| secure | `Optional`. Specify whether the cookie can only be transmitted over secure protocol as https | `boolean` | `false` |
+| sameSite | `Optional`. Specify whether the browser can send this cookie along with cross-site requests | `strict` \| `lax` \| `none` | - |
+
+Options is same as [js-cookie attributes](https://github.com/js-cookie/js-cookie#cookie-attributes).
\ No newline at end of file
diff --git a/packages/hooks/src/useEventTarget/demo/demo2.tsx b/packages/hooks/src/useEventTarget/demo/demo2.tsx
index 8a1f1369..18016257 100644
--- a/packages/hooks/src/useEventTarget/demo/demo2.tsx
+++ b/packages/hooks/src/useEventTarget/demo/demo2.tsx
@@ -1,6 +1,6 @@
 /**
  * title: Custom transformer function
- * desc: Controlled Input component with number input only
+ * desc: Controlled input component with number input only
  *
  * title.zh-CN: 自定义转换函数
  * desc.zh-CN: 只能输入数字的 input 组件
diff --git a/packages/hooks/src/useEventTarget/index.en-US.md b/packages/hooks/src/useEventTarget/index.en-US.md
index 4cd29031..fb952bfd 100644
--- a/packages/hooks/src/useEventTarget/index.en-US.md
+++ b/packages/hooks/src/useEventTarget/index.en-US.md
@@ -12,7 +12,7 @@ group:
 
 <Tag lang="en-US" tags="ssr&crossPlatform"></Tag>
 
-The hook encapsulates `onChange` and `value` logic for form controls that obtains value through `event.target.value`. It also supports custom transformer and reset functionalities.
+A hook that encapsulates `onChange` and `value` logic for form controls that obtains value through `event.target.value`. It also supports custom transformer and reset functionalities.
 
 ## Example
 
diff --git a/packages/hooks/src/useLocalStorageState/index.en-US.md b/packages/hooks/src/useLocalStorageState/index.en-US.md
index 1eb13cd0..e72f8ee1 100644
--- a/packages/hooks/src/useLocalStorageState/index.en-US.md
+++ b/packages/hooks/src/useLocalStorageState/index.en-US.md
@@ -12,7 +12,7 @@ group:
 
 <Tag lang="zh-CN" tags="ssr"></Tag>
 
-A Hook can store state into localStorage.
+A Hook that stores state into localStorage.
 
 ## Examples
 
diff --git a/packages/hooks/src/useUpdateEffect/demo/demo1.tsx b/packages/hooks/src/useUpdateEffect/demo/demo1.tsx
index adae76fd..87af5e63 100644
--- a/packages/hooks/src/useUpdateEffect/demo/demo1.tsx
+++ b/packages/hooks/src/useUpdateEffect/demo/demo1.tsx
@@ -1,6 +1,6 @@
 /**
  * title: Basic usage
- * desc: This hook is exactly the same as useEffect, except it omits the first execution and only runs when dependencies update.
+ * desc: This hook is exactly the same as useEffect, except it skips running the effect for the first time.
  *
  * title.zh-CN: 基础用法
  * desc.zh-CN: 使用上与 useEffect 完全相同，只是它忽略了首次执行，且只在依赖项更新时执行。
diff --git a/packages/hooks/src/useUpdateEffect/index.en-US.md b/packages/hooks/src/useUpdateEffect/index.en-US.md
index acd2d74a..56c344f6 100644
--- a/packages/hooks/src/useUpdateEffect/index.en-US.md
+++ b/packages/hooks/src/useUpdateEffect/index.en-US.md
@@ -12,7 +12,7 @@ group:
 
 <Tag lang="en-US" tags="ssr&crossPlatform"></Tag>
 
-A hook of useEffect that only runs when dependencies update.
+A hook alike `useEffect` but skips running the effect for the first time.
 
 ## Examples
 
diff --git a/packages/hooks/src/useUpdateEffect/index.ts b/packages/hooks/src/useUpdateEffect/index.ts
index e67bd917..5117ca5c 100644
--- a/packages/hooks/src/useUpdateEffect/index.ts
+++ b/packages/hooks/src/useUpdateEffect/index.ts
@@ -1,22 +1,4 @@
-import { useEffect, useRef } from 'react';
+import { useEffect } from 'react';
+import { createUpdateEffect } from '../createUpdateEffect';
 
-const useUpdateEffect: typeof useEffect = (effect, deps) => {
-  const isMounted = useRef(false);
-
-  // for react-refresh
-  useEffect(() => {
-    return () => {
-      isMounted.current = false;
-    };
-  }, []);
-
-  useEffect(() => {
-    if (!isMounted.current) {
-      isMounted.current = true;
-    } else {
-      return effect();
-    }
-  }, deps);
-};
-
-export default useUpdateEffect;
+export default createUpdateEffect(useEffect);
diff --git a/packages/hooks/src/useUpdateLayoutEffect/demo/demo1.tsx b/packages/hooks/src/useUpdateLayoutEffect/demo/demo1.tsx
index e3334960..b0fae8b4 100644
--- a/packages/hooks/src/useUpdateLayoutEffect/demo/demo1.tsx
+++ b/packages/hooks/src/useUpdateLayoutEffect/demo/demo1.tsx
@@ -1,6 +1,6 @@
 /**
  * title: Basic usage
- * desc: This hook is exactly the same as useLayoutEffect, except it omits the first execution and only runs when dependencies update.
+ * desc: This hook is exactly the same as useLayoutEffect, except it skips running the effect for the first time.
  *
  * title.zh-CN: 基础用法
  * desc.zh-CN: 使用上与 useLayoutEffect 完全相同，只是它忽略了首次执行，且只在依赖项更新时执行。
diff --git a/packages/hooks/src/useUpdateLayoutEffect/index.en-US.md b/packages/hooks/src/useUpdateLayoutEffect/index.en-US.md
index d832d2df..b35e06c1 100644
--- a/packages/hooks/src/useUpdateLayoutEffect/index.en-US.md
+++ b/packages/hooks/src/useUpdateLayoutEffect/index.en-US.md
@@ -12,7 +12,7 @@ group:
 
 <Tag lang="en-US" tags="ssr&crossPlatform"></Tag>
 
-A hook of useLayoutEffect that only runs when dependencies update.
+A hook alike `useLayoutEffect` but skips running the effect for the first time.
 
 ## Examples
 
diff --git a/packages/hooks/src/useUpdateLayoutEffect/index.ts b/packages/hooks/src/useUpdateLayoutEffect/index.ts
index 59e167ff..a6421485 100644
--- a/packages/hooks/src/useUpdateLayoutEffect/index.ts
+++ b/packages/hooks/src/useUpdateLayoutEffect/index.ts
@@ -1,22 +1,4 @@
-import { useLayoutEffect, useRef } from 'react';
+import { useLayoutEffect } from 'react';
+import { createUpdateEffect } from '../createUpdateEffect';
 
-const useUpdateLayoutEffect: typeof useLayoutEffect = (effect, deps) => {
-  const isMounted = useRef(false);
-
-  // for react-refresh
-  useLayoutEffect(() => {
-    return () => {
-      isMounted.current = false;
-    };
-  }, []);
-
-  useLayoutEffect(() => {
-    if (!isMounted.current) {
-      isMounted.current = true;
-    } else {
-      return effect();
-    }
-  }, deps);
-};
-
-export default useUpdateLayoutEffect;
+export default createUpdateEffect(useLayoutEffect);


Test patch:
diff --git a/packages/hooks/src/createUpdateEffect/__tests__/index.test.ts b/packages/hooks/src/createUpdateEffect/__tests__/index.test.ts
new file mode 100644
index 00000000..481a1164
--- /dev/null
+++ b/packages/hooks/src/createUpdateEffect/__tests__/index.test.ts
@@ -0,0 +1,37 @@
+import { renderHook } from '@testing-library/react-hooks';
+import { useEffect, useLayoutEffect, useRef } from 'react';
+import { createUpdateEffect } from '../index';
+
+describe('createUpdateEffect', () => {
+  it('should be defined', () => {
+    expect(createUpdateEffect).toBeDefined();
+  });
+
+  it('should work for useEffect', () => {
+    const useUpdateEffect = createUpdateEffect(useEffect);
+
+    let mountedState = 1;
+    const hook = renderHook(() =>
+      useUpdateEffect(() => {
+        mountedState = 2;
+      }),
+    );
+    expect(mountedState).toEqual(1);
+    hook.rerender();
+    expect(mountedState).toEqual(2);
+  });
+
+  it('should work for useLayoutEffect', () => {
+    const useUpdateLayoutEffect = createUpdateEffect(useLayoutEffect);
+
+    let mountedState = 1;
+    const hook = renderHook(() =>
+      useUpdateLayoutEffect(() => {
+        mountedState = 2;
+      }),
+    );
+    expect(mountedState).toEqual(1);
+    hook.rerender();
+    expect(mountedState).toEqual(2);
+  });
+});