Repository: alibaba/hooks
Pull request #1061 (2021-07-25 09:20:33)

feat: add useRafState


Patch:
diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts
index 6a745dda..cb9fce0e 100644
--- a/packages/hooks/src/index.ts
+++ b/packages/hooks/src/index.ts
@@ -62,6 +62,7 @@ import useLatest from './useLatest';
 import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect';
 import useDeepCompareEffect from './useDeepCompareEffect';
 import useAsyncEffect from './useAsyncEffect';
+import useRafState from './useRafState';
 
 export {
   useControllableValue,
@@ -130,4 +131,5 @@ export {
   useIsomorphicLayoutEffect,
   useDeepCompareEffect,
   useAsyncEffect,
+  useRafState,
 };
diff --git a/packages/hooks/src/useMouse/index.ts b/packages/hooks/src/useMouse/index.ts
index 76a86937..131a2de2 100644
--- a/packages/hooks/src/useMouse/index.ts
+++ b/packages/hooks/src/useMouse/index.ts
@@ -1,4 +1,4 @@
-import { useState } from 'react';
+import { useRafState } from '..';
 import useEventListener from '../useEventListener';
 
 export interface CursorState {
@@ -20,7 +20,7 @@ const initState: CursorState = {
 };
 
 export default () => {
-  const [state, setState] = useState(initState);
+  const [state, setState] = useRafState(initState);
 
   useEventListener(
     'mousemove',
diff --git a/packages/hooks/src/useRafState/demo/demo1.tsx b/packages/hooks/src/useRafState/demo/demo1.tsx
new file mode 100644
index 00000000..2cbce6e4
--- /dev/null
+++ b/packages/hooks/src/useRafState/demo/demo1.tsx
@@ -0,0 +1,38 @@
+/**
+ * title: Default usage
+ *
+ * title.zh-CN: 基础用法
+ */
+
+import React, { useRef, useEffect } from 'react';
+import { useRafState } from 'ahooks';
+
+export default () => {
+  const [state, setState] = useRafState({
+    width: 0,
+    height: 0,
+  });
+
+  useEffect(() => {
+    const onResize = () => {
+      setState({
+        width: document.documentElement.clientWidth,
+        height: document.documentElement.clientHeight,
+      });
+    };
+    onResize();
+
+    window.addEventListener('resize', onResize);
+
+    return () => {
+      window.removeEventListener('resize', onResize);
+    };
+  }, []);
+
+  return (
+    <div>
+      <p>Try to resize the window </p>
+      current: {JSON.stringify(state)}
+    </div>
+  );
+};
diff --git a/packages/hooks/src/useRafState/index.en-US.md b/packages/hooks/src/useRafState/index.en-US.md
new file mode 100644
index 00000000..a51cb985
--- /dev/null
+++ b/packages/hooks/src/useRafState/index.en-US.md
@@ -0,0 +1,25 @@
+---
+title: useRafState
+nav:
+  title: Hooks
+  path: /hooks
+group:
+  title: State
+  path: /state
+---
+
+# useRafState
+
+<Tag lang="en-US" tags="ssr&crossPlatform"></Tag>
+
+Update the state in [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) callback, generally used for performance optimization.
+
+## Examples
+
+### Default usage
+
+<code src="./demo/demo1.tsx" />
+
+### API
+
+Same as useState.
\ No newline at end of file
diff --git a/packages/hooks/src/useRafState/index.ts b/packages/hooks/src/useRafState/index.ts
new file mode 100644
index 00000000..b171dea1
--- /dev/null
+++ b/packages/hooks/src/useRafState/index.ts
@@ -0,0 +1,27 @@
+import { useCallback, useRef, useState } from 'react';
+import type { Dispatch, SetStateAction } from 'react';
+import useUnmount from '../useUnmount';
+
+function useRafState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];
+function useRafState<S = undefined>(): [S | undefined, Dispatch<SetStateAction<S | undefined>>];
+
+function useRafState<S>(initialState?: S | (() => S)) {
+  const ref = useRef(0);
+  const [state, setState] = useState(initialState);
+
+  const setRafState = useCallback((value: S | ((prevState: S) => S)) => {
+    cancelAnimationFrame(ref.current);
+
+    ref.current = requestAnimationFrame(() => {
+      setState(value);
+    });
+  }, []);
+
+  useUnmount(() => {
+    cancelAnimationFrame(ref.current);
+  });
+
+  return [state, setRafState] as const;
+}
+
+export default useRafState;
diff --git a/packages/hooks/src/useRafState/index.zh-CN.md b/packages/hooks/src/useRafState/index.zh-CN.md
new file mode 100644
index 00000000..5366d0af
--- /dev/null
+++ b/packages/hooks/src/useRafState/index.zh-CN.md
@@ -0,0 +1,25 @@
+---
+title: useRafState
+nav:
+  title: Hooks
+  path: /hooks
+group:
+  title: State
+  path: /state
+---
+
+# useRafState
+
+<Tag lang="zh-CN" tags="ssr&crossPlatform"></Tag>
+
+只在 [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) callback 时更新 state，一般用于性能优化。
+
+## 代码演示
+
+### 基础用法
+
+<code src="./demo/demo1.tsx" />
+
+### API
+
+与 useState 一致
\ No newline at end of file
diff --git a/packages/hooks/src/useScroll/index.ts b/packages/hooks/src/useScroll/index.ts
index 844b7424..fc7bf84e 100644
--- a/packages/hooks/src/useScroll/index.ts
+++ b/packages/hooks/src/useScroll/index.ts
@@ -1,7 +1,8 @@
-import { useEffect, useState } from 'react';
+import { useEffect } from 'react';
+import { useRafState } from '..';
 import useLatest from '../useLatest';
-import type { BasicTarget } from '../utils/dom2';
 import { getTargetElement } from '../utils/dom2';
+import type { BasicTarget } from '../utils/dom2';
 
 type Position = { left: number; top: number };
 
@@ -12,7 +13,7 @@ function useScroll(
   target?: Target,
   shouldUpdate: ScrollListenController = () => true,
 ): Position | undefined {
-  const [position, setPosition] = useState<Position>();
+  const [position, setPosition] = useRafState<Position>();
 
   const shouldUpdateRef = useLatest(shouldUpdate);
 
diff --git a/packages/hooks/src/useSize/index.ts b/packages/hooks/src/useSize/index.ts
index 9ff4b4d8..9ac9a6cc 100644
--- a/packages/hooks/src/useSize/index.ts
+++ b/packages/hooks/src/useSize/index.ts
@@ -1,13 +1,13 @@
-import { useState } from 'react';
 import ResizeObserver from 'resize-observer-polyfill';
 import useIsomorphicLayoutEffect from '../useIsomorphicLayoutEffect';
-import type { BasicTarget } from '../utils/dom2';
+import useRafState from '../useRafState';
 import { getTargetElement } from '../utils/dom2';
+import type { BasicTarget } from '../utils/dom2';
 
 type Size = { width: number; height: number };
 
 function useSize(target: BasicTarget): Size | undefined {
-  const [state, setState] = useState<Size>();
+  const [state, setState] = useRafState<Size>();
 
   useIsomorphicLayoutEffect(() => {
     const el = getTargetElement(target);


Test patch:
diff --git a/packages/hooks/src/useMouse/__tests__/index.test.ts b/packages/hooks/src/useMouse/__tests__/index.test.ts
index f979df2a..10d4d3ef 100644
--- a/packages/hooks/src/useMouse/__tests__/index.test.ts
+++ b/packages/hooks/src/useMouse/__tests__/index.test.ts
@@ -24,6 +24,7 @@ describe('useMouse', () => {
     expect(hook.result.current.pageY).toEqual(NaN);
 
     moveMouse(10, 10);
+    await hook.waitForNextUpdate();
 
     // can't manually set pageX & pageY for mouseEvent, default undefined here.
     expect(hook.result.current.pageX).toEqual(undefined);
diff --git a/packages/hooks/src/useRafState/__tests__/index.test.ts b/packages/hooks/src/useRafState/__tests__/index.test.ts
new file mode 100644
index 00000000..6f83ac0a
--- /dev/null
+++ b/packages/hooks/src/useRafState/__tests__/index.test.ts
@@ -0,0 +1,7 @@
+import useRafState from '../index';
+
+describe('useRafState', () => {
+  it('should be defined', () => {
+    expect(useRafState).toBeDefined();
+  });
+});
diff --git a/packages/hooks/src/useScroll/__tests__/index.test.ts b/packages/hooks/src/useScroll/__tests__/index.test.ts
index dd447396..380c30b3 100644
--- a/packages/hooks/src/useScroll/__tests__/index.test.ts
+++ b/packages/hooks/src/useScroll/__tests__/index.test.ts
@@ -7,6 +7,6 @@ describe('useScroll', () => {
   });
   it('document body', () => {
     const hook = renderHook(() => useScroll(document));
-    expect(hook.result.current).toEqual({ left: 0, top: 0 });
+    expect(hook.result.current).toEqual(undefined);
   });
 });