Repository: alibaba/hooks
Pull request #1017 (2021-07-08 12:38:41)

refactor: useSize


Patch:
diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts
index 1df1bdbf..8270478b 100644
--- a/packages/hooks/src/index.ts
+++ b/packages/hooks/src/index.ts
@@ -60,6 +60,7 @@ import useUnmountedRef from './useUnmountedRef';
 import useExternal from './useExternal';
 import useSafeState from './useSafeState';
 import useLatest from './useLatest';
+import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect';
 
 export {
   useControllableValue,
@@ -126,4 +127,5 @@ export {
   useExternal,
   useSafeState,
   useLatest,
+  useIsomorphicLayoutEffect,
 };
diff --git a/packages/hooks/src/useIsomorphicLayoutEffect/index.en-US.md b/packages/hooks/src/useIsomorphicLayoutEffect/index.en-US.md
new file mode 100644
index 00000000..5686f288
--- /dev/null
+++ b/packages/hooks/src/useIsomorphicLayoutEffect/index.en-US.md
@@ -0,0 +1,30 @@
+---
+title: useIsomorphicLayoutEffect
+nav:
+  title: Hooks
+  path: /hooks
+group:
+  title: SideEffect
+  path: /side-effect
+---
+
+# useIsomorphicLayoutEffect
+
+<Tag lang="en-US" tags="ssr&crossPlatform"></Tag>
+
+In SSR mode, the following warning will appear when use useLayoutEffect
+
+> ⚠️ Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client. See https://fb.me/react-uselayouteffect-ssr for common fixes.
+
+To avoid this warning, useIsomorphicLayoutEffect can be used instead of useLayoutEffect.
+
+
+The source code of useIsomorphicLayoutEffect is:
+
+```javascript
+const useIsomorphicLayoutEffect = isBrowser ? useLayoutEffect : useEffect;
+```
+
+UseEffect is returned in a non-browser environment, and useLayoutEffect is returned in a browser environment.
+
+For more information, please refer to [useLayoutEffect and SSR](https://medium.com/@alexandereardon/uselayouteffect-and-ssr-192986cdcf7a)
\ No newline at end of file
diff --git a/packages/hooks/src/useIsomorphicLayoutEffect/index.ts b/packages/hooks/src/useIsomorphicLayoutEffect/index.ts
new file mode 100644
index 00000000..2e3e71c7
--- /dev/null
+++ b/packages/hooks/src/useIsomorphicLayoutEffect/index.ts
@@ -0,0 +1,6 @@
+import { useEffect, useLayoutEffect } from 'react';
+import { isBrowser } from '../utils/dom2';
+
+const useIsomorphicLayoutEffect = isBrowser ? useLayoutEffect : useEffect;
+
+export default useIsomorphicLayoutEffect;
diff --git a/packages/hooks/src/useIsomorphicLayoutEffect/index.zh-CN.md b/packages/hooks/src/useIsomorphicLayoutEffect/index.zh-CN.md
new file mode 100644
index 00000000..01a81466
--- /dev/null
+++ b/packages/hooks/src/useIsomorphicLayoutEffect/index.zh-CN.md
@@ -0,0 +1,31 @@
+---
+title: useIsomorphicLayoutEffect
+nav:
+  title: Hooks
+  path: /hooks
+group:
+  title: SideEffect
+  path: /side-effect
+---
+
+# useIsomorphicLayoutEffect
+
+<Tag lang="zh-CN" tags="ssr&crossPlatform"></Tag>
+
+在 SSR 模式下，使用 useLayoutEffect 时，会出现以下警告
+
+> ⚠️ Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client. See https://fb.me/react-uselayouteffect-ssr for common fixes.
+
+
+为了避免该警告，可以使用 useIsomorphicLayoutEffect 代替 useLayoutEffect。
+
+
+useIsomorphicLayoutEffect 源码如下：
+
+```js
+const useIsomorphicLayoutEffect = isBrowser ? useLayoutEffect : useEffect;
+```
+
+在非浏览器环境返回 useEffect，在浏览器环境返回 useLayoutEffect。
+
+更多信息可以参考 [useLayoutEffect and SSR](https://medium.com/@alexandereardon/uselayouteffect-and-ssr-192986cdcf7a)
\ No newline at end of file
diff --git a/packages/hooks/src/useSize/demo/demo1.tsx b/packages/hooks/src/useSize/demo/demo1.tsx
index 89b59ce8..47046cbb 100644
--- a/packages/hooks/src/useSize/demo/demo1.tsx
+++ b/packages/hooks/src/useSize/demo/demo1.tsx
@@ -1,9 +1,9 @@
 /**
  * title: Default usage
- * desc: using ref to listen to size change
+ * desc: useSize can receive ref parameters
  *
  * title.zh-CN: 基础用法
- * desc.zh-CN: 使用 ref 监听节点尺寸变化
+ * desc.zh-CN: useSize 可以接收 ref 参数
  */
 
 import React, { useRef } from 'react';
@@ -14,8 +14,10 @@ export default () => {
   const size = useSize(ref);
   return (
     <div ref={ref}>
-      try to resize the preview window <br />
-      dimensions -- width: {size.width} px, height: {size.height} px
+      <p>Try to resize the preview window </p>
+      <p>
+        width: {size?.width}px, height: {size?.height}px
+      </p>
     </div>
   );
 };
diff --git a/packages/hooks/src/useSize/demo/demo2.tsx b/packages/hooks/src/useSize/demo/demo2.tsx
index a7ff3727..30b2c799 100644
--- a/packages/hooks/src/useSize/demo/demo2.tsx
+++ b/packages/hooks/src/useSize/demo/demo2.tsx
@@ -1,21 +1,22 @@
 /**
  * title: pass in the DOM element
- * desc: Pass in a function that returns the DOM element.
+ * desc: useSize can receive dom parameters, and in SSR scenarios, you can pass in function `()=>dom`
  *
  * title.zh-CN: 传入 DOM 元素
- * desc.zh-CN: 传入 function 并返回一个 dom 元素。
+ * desc.zh-CN: useSize 可以接收 dom，在 SSR 场景可以传入函数 `()=> dom`
  */
 
 import React from 'react';
 import { useSize } from 'ahooks';
 
 export default () => {
-  const dom = document.querySelector('body');
-  const size = useSize(dom);
+  const size = useSize(document.querySelector('body'));
   return (
     <div>
-      try to resize the preview window <br />
-      dimensions -- width: {size.width} px, height: {size.height} px
+      <p>Try to resize the preview window </p>
+      <p>
+        width: {size?.width}px, height: {size?.height}px
+      </p>
     </div>
   );
 };
diff --git a/packages/hooks/src/useSize/index.en-US.md b/packages/hooks/src/useSize/index.en-US.md
index 85e42852..fbfce161 100644
--- a/packages/hooks/src/useSize/index.en-US.md
+++ b/packages/hooks/src/useSize/index.en-US.md
@@ -6,12 +6,13 @@ nav:
 group:
   title: Dom
   path: /dom
-legacy: /dom/use-size
 ---
 
 # useSize
 
-A hook to subscribe DOM element size change.
+<Tag lang="en-US" tags="ssr"></Tag>
+
+A hook to observe element's size change.
 
 ## Examples
 
@@ -23,7 +24,6 @@ A hook to subscribe DOM element size change.
 
 <code src="./demo/demo2.tsx" />
 
-
 ## API
 
 ```typescript
@@ -34,10 +34,10 @@ const size = useSize(target);
 
 | Property | Description                                                        | Type                   | Default |
 |---------|----------------------------------------------|------------------------|--------|
-| target | DOM element or Ref Object  | `HTMLElement` \| `(() => HTMLElement)` \| `MutableRefObject` | -      |
+| target | DOM element or ref object  | `Element` \| `(() => Element)` \| `MutableRefObject<Element>` | -      |
 
 ### Result
 
 | Property | Description                                         | Type                 |
 |----------|------------------------------------------|------------|
-| size  | size of the DOM                             | `{ width: number, height: number }`    |
+| size  | Size of the element                             | `{ width: number, height: number } \| undefined`    |
diff --git a/packages/hooks/src/useSize/index.ts b/packages/hooks/src/useSize/index.ts
index 375b91e7..9ff4b4d8 100644
--- a/packages/hooks/src/useSize/index.ts
+++ b/packages/hooks/src/useSize/index.ts
@@ -1,22 +1,19 @@
-import { useState, useLayoutEffect } from 'react';
+import { useState } from 'react';
 import ResizeObserver from 'resize-observer-polyfill';
-import { getTargetElement, BasicTarget } from '../utils/dom';
+import useIsomorphicLayoutEffect from '../useIsomorphicLayoutEffect';
+import type { BasicTarget } from '../utils/dom2';
+import { getTargetElement } from '../utils/dom2';
 
-type Size = { width?: number; height?: number };
+type Size = { width: number; height: number };
 
-function useSize(target: BasicTarget): Size {
-  const [state, setState] = useState<Size>(() => {
-    const el = getTargetElement(target);
-    return {
-      width: ((el || {}) as HTMLElement).clientWidth,
-      height: ((el || {}) as HTMLElement).clientHeight,
-    };
-  });
+function useSize(target: BasicTarget): Size | undefined {
+  const [state, setState] = useState<Size>();
 
-  useLayoutEffect(() => {
+  useIsomorphicLayoutEffect(() => {
     const el = getTargetElement(target);
+
     if (!el) {
-      return () => {};
+      return;
     }
 
     const resizeObserver = new ResizeObserver((entries) => {
@@ -28,11 +25,11 @@ function useSize(target: BasicTarget): Size {
       });
     });
 
-    resizeObserver.observe(el as HTMLElement);
+    resizeObserver.observe(el);
     return () => {
       resizeObserver.disconnect();
     };
-  }, [target]);
+  }, [typeof target === 'function' ? undefined : target]);
 
   return state;
 }
diff --git a/packages/hooks/src/useSize/index.zh-CN.md b/packages/hooks/src/useSize/index.zh-CN.md
index 4bbd47ce..2c40f32a 100644
--- a/packages/hooks/src/useSize/index.zh-CN.md
+++ b/packages/hooks/src/useSize/index.zh-CN.md
@@ -10,7 +10,9 @@ group:
 
 # useSize
 
-一个用于监听 dom 节点尺寸变化的 Hook。
+<Tag lang="zh-CN" tags="ssr"></Tag>
+
+监听 DOM 节点尺寸变化的 Hook。
 
 ## 代码演示
 
@@ -32,10 +34,10 @@ const size = useSize(target);
 
 | 参数    | 说明                                         | 类型                   | 默认值 |
 |---------|----------------------------------------------|------------------------|--------|
-| target | DOM 节点或者 Refs  | `HTMLElement` \| `(() => HTMLElement)` \| `MutableRefObject` | -      |
+| target | DOM 节点或者 Ref  | `Element` \| `(() => Element)` \| `MutableRefObject<Element>` | -      |
 
 ### 结果
 
 | 参数     | 说明                                     | 类型       |
 |----------|------------------------------------------|------------|
-| size  | dom 节点的尺寸                         | `{ width: number, height: number }`    |
+| size  | dom 节点的尺寸                         | `{ width: number, height: number } \| undefined`    |
diff --git a/packages/hooks/src/useVirtualList/index.ts b/packages/hooks/src/useVirtualList/index.ts
index 588cd8ab..5fa399f4 100644
--- a/packages/hooks/src/useVirtualList/index.ts
+++ b/packages/hooks/src/useVirtualList/index.ts
@@ -1,4 +1,4 @@
-import { useEffect, useState, useMemo, useRef, MutableRefObject } from 'react';
+import { useEffect, useMemo, useRef, useState } from 'react';
 import useSize from '../useSize';
 
 export interface OptionType {
@@ -8,7 +8,7 @@ export interface OptionType {
 
 export default <T = any>(list: T[], options: OptionType) => {
   const containerRef = useRef<HTMLElement | null>();
-  const size = useSize(containerRef as MutableRefObject<HTMLElement>);
+  const size = useSize(containerRef);
   // 暂时禁止 cache
   // const distanceCache = useRef<{ [key: number]: number }>({});
   const [state, setState] = useState({ start: 0, end: 10 });
@@ -70,7 +70,7 @@ export default <T = any>(list: T[], options: OptionType) => {
 
   useEffect(() => {
     calculateRange();
-  }, [size.width, size.height, list.length]);
+  }, [size?.width, size?.height, list.length]);
 
   const totalHeight = useMemo(() => {
     if (typeof itemHeight === 'number') {
diff --git a/packages/hooks/src/utils/dom2.ts b/packages/hooks/src/utils/dom2.ts
new file mode 100644
index 00000000..c9a65011
--- /dev/null
+++ b/packages/hooks/src/utils/dom2.ts
@@ -0,0 +1,34 @@
+import type { MutableRefObject } from 'react';
+
+export const isBrowser = !!(
+  typeof window !== 'undefined' &&
+  window.document &&
+  window.document.createElement
+);
+
+type Target<T> = T | undefined | null;
+
+export type BasicTarget<T = Element> = (() => Target<T>) | Target<T> | MutableRefObject<Target<T>>;
+
+export function getTargetElement<T>(target: BasicTarget<T>, defaultElement?: T) {
+  if (!isBrowser) {
+    return undefined;
+  }
+
+  if (!target) {
+    return defaultElement;
+  }
+
+  let targetElement: Target<T>;
+
+  if (typeof target === 'function') {
+    // @ts-ignore
+    targetElement = target();
+  } else if ('current' in target) {
+    targetElement = target.current;
+  } else {
+    targetElement = target;
+  }
+
+  return targetElement;
+}


Test patch:
diff --git a/packages/hooks/src/useSize/__tests__/index.test.ts b/packages/hooks/src/useSize/__tests__/index.test.ts
index e0ff77bb..0a4f2b3a 100644
--- a/packages/hooks/src/useSize/__tests__/index.test.ts
+++ b/packages/hooks/src/useSize/__tests__/index.test.ts
@@ -8,8 +8,6 @@ describe('useSize', () => {
   });
   it('with argument', () => {
     const hook = renderHook(() => useSize(document.body));
-    // with args, the init size should be the real size of the element, in node test env, it's 0 though
-    expect(hook.result.current.width).toEqual(0);
-    expect(hook.result.current.height).toEqual(0);
+    expect(hook.result.current).toEqual(undefined);
   });
 });