docs: Modal hooks bug (#39708)

This commit is contained in:
二货爱吃白萝卜 2022-12-21 17:14:22 +08:00 committed by GitHub
parent 78592a5596
commit f1c6350119
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 334 additions and 2 deletions

View File

@ -0,0 +1,166 @@
---
title: Funny Modal hook BUG
date: 2022-12-21
author: zombieJ
---
Recently we encountered an [issue](https://github.com/ant-design/ant-design/issues/39427), saying that when `contextHolder` of `Modal.useModal` is placed in different positions, `modal.confirm` popup location will be different:
```tsx
import React from 'react';
import { Button, Modal } from 'antd';
export default () => {
const [modal, contextHolder] = Modal.useModal();
return (
<div>
<Modal open>
<Button
onClick={() => {
modal.confirm({ title: 'Hello World' });
}}
>
Confirm
</Button>
{/* 🚨 BUG when put here */}
{contextHolder}
</Modal>
{/* ✅ Work as expect when put here */}
{/* {contextHolder} */}
</div>
);
};
```
Workable version:
![Normal](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*VJJUTL88uM4AAAAAAAAAAAAADrJ8AQ/original)
Bug version:
![BUG](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*a_ulS7EaylkAAAAAAAAAAAAADrJ8AQ/original)
From the figure above, we can see that when `contextHolder` is placed inside `Modal`, the pop-up position of the hooks call is incorrect.
### Why?
antd's Modal internal calls the `rc-dialog` component library, which accepts a `mousePosition` attribute to control the pop-up position([Dialog/Content/index.tsx](https://github.com/react-component/dialog/blob/79649e187ee512be6b3eb3b76e4a6b618b67ebc7/src/Dialog/Content/index.tsx#L43))
```tsx
// pseudocode
const elementOffset = offset(dialogElement);
const transformOrigin = `${mousePosition.x - elementOffset.left}px ${
mousePosition.y - elementOffset.top
}px`;
```
The `offset` method is used to obtain the coordinate position of the form itself([util.ts](https://github.com/react-component/dialog/blob/79649e187ee512be6b3eb3b76e4a6b618b67ebc7/src/util.ts#L28))
```tsx
// pseudocode
function offset(el: Element) {
const { left, top } = el.getBoundingClientRect();
return { left, top };
}
```
Through breakpoint debugging, we can find that the value of `mousePosition` is correct, but the value of `rect` obtained in `offset` is wrong:
```json
{
"left": 0,
"top": 0,
"width": 0,
"height": 0
}
```
This value obviously means that the form component has not been added to the DOM tree at the animation start node, so we need to check the logic added by Dialog.
### createPortal
`rc-dialog` creates a node in the document through `rc-portal`, and then renders the component to this node through `ReactDOM.createPortal`. For the different positions of `contextHolder` and different interactive, it can be speculated that there must be a problem with the timing of creating nodes in the document, so we can take a closer look at the part of adding nodes by default in `rc-portal`([useDom.tsx](https://github.com/react-component/portal/blob/85e6e15ee97c70ec260c5409d9d273d6967e3560/src/useDom.tsx#L55))
```tsx
// pseudocode
function append() {
// This is not real world code, just for explain
document.body.appendChild(document.createElement('div'));
}
useLayoutEffect(() => {
if (queueCreate) {
queueCreate(append);
} else {
append();
}
}, []);
```
Among them, `queueCreate` is obtained through `context`, the purpose is to prevent the situation that the child element is created before the parent element under the nesting level:
```tsx
<Modal title="Hello 1" open>
<Modal title="Hello 2" open>
<Modal>
<Modal>
```
```html
<!-- Child `useLayoutEffect` is run before parent. Which makes inject dom before parent -->
<div data-title="Hello 2"></div>
<div data-title="Hello 1"></div>
```
Use `queueCreate` to add the `append` of the child element to the queue, and then use `useLayoutEffect` to execute:
```tsx
// pseudocode
const [queue, setQueue] = useState<VoidFunction[]>([]);
function queueCreate(appendFn: VoidFunction) {
setQueue((origin) => {
const newQueue = [appendFn, ...origin];
return newQueue;
});
}
useLayoutEffect(() => {
if (queue.length) {
queue.forEach((appendFn) => appendFn());
setQueue([]);
}
}, [queue]);
```
### Resolution
Due to the above queue operation, the dom of the portal will be triggered in the next `useLayoutEffect` under nesting. This causes the `uesLayoutEffect` timing of the animation to start in `rc-dialog` after the node behavior is added, resulting in the element not being in the document and unable to obtain the correct coordinate information.
Since Modal is already enabled, it does not need to be executed asynchronously through `queue`, so we only need to add a judgment if it is enabled, and execute `append` directly:
```tsx
// pseudocode
const appendedRef = useRef(false);
const queueCreate = !appendedRef.current
? (appendFn: VoidFunction) => {
// same code
}
: undefined;
function append() {
// This is not real world code, just for explain
document.body.appendChild(document.createElement('div'));
appendedRef.current = true;
}
// ...
return <PortalContext value={queueCreate}>{children}</PortalContext>;
```
That's all.

View File

@ -0,0 +1,166 @@
---
title: Modal hook 的有趣 BUG
date: 2022-12-21
author: zombieJ
---
最近我们遇到了一个 [issue](https://github.com/ant-design/ant-design/issues/39427),说是 `Modal.useModal``contextHolder` 在放置不同的位置时,`modal.confirm` 弹出位置会不一样:
```tsx
import React from 'react';
import { Button, Modal } from 'antd';
export default () => {
const [modal, contextHolder] = Modal.useModal();
return (
<div>
<Modal open>
<Button
onClick={() => {
modal.confirm({ title: 'Hello World' });
}}
>
Confirm
</Button>
{/* 🚨 BUG when put here */}
{contextHolder}
</Modal>
{/* ✅ Work as expect when put here */}
{/* {contextHolder} */}
</div>
);
};
```
正常版本:
![Normal](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*VJJUTL88uM4AAAAAAAAAAAAADrJ8AQ/original)
有问题版本:
![BUG](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*a_ulS7EaylkAAAAAAAAAAAAADrJ8AQ/original)
从上图可以看到当 `contextHolder` 放在 `Modal` 内部时hooks 调用的弹出位置不正确了。
### 思路整理
antd 的 Modal 底层调用的是 `rc-dialog` 组件库,其接受一个 `mousePosition` 属性,用于控制弹出位置([Dialog/Content/index.tsx](https://github.com/react-component/dialog/blob/79649e187ee512be6b3eb3b76e4a6b618b67ebc7/src/Dialog/Content/index.tsx#L43)
```tsx
// pseudocode
const elementOffset = offset(dialogElement);
const transformOrigin = `${mousePosition.x - elementOffset.left}px ${
mousePosition.y - elementOffset.top
}px`;
```
其中 `offset` 方法用于获取窗体本身的坐标位置([util.ts](https://github.com/react-component/dialog/blob/79649e187ee512be6b3eb3b76e4a6b618b67ebc7/src/util.ts#L28))
```tsx
// pseudocode
function offset(el: Element) {
const { left, top } = el.getBoundingClientRect();
return { left, top };
}
```
通过断点调试,我们可以发现 `mousePosition` 的值是正确的,但是 `offset` 中获取的 `rect` 的值是错误的:
```json
{
"left": 0,
"top": 0,
"width": 0,
"height": 0
}
```
这个值很明显代表窗体组件在动画启动节点尚未添加到 DOM 树中,所以我们需要查看一下 Dialog 添加的逻辑。
### createPortal
`rc-dialog` 通过 `rc-portal` 在 document 中创建一个节点,然后通过 `ReactDOM.createPortal` 将组件渲染到这个节点上。对于 `contextHolder` 位置不同而出现表现不同可以推测,一定是在 document 创建节点的时序出现了问题,于是我们可以进一步看一下 `rc-portal` 中默认添加节点的部分([useDom.tsx](https://github.com/react-component/portal/blob/85e6e15ee97c70ec260c5409d9d273d6967e3560/src/useDom.tsx#L55))
```tsx
// pseudocode
function append() {
// This is not real world code, just for explain
document.body.appendChild(document.createElement('div'));
}
useLayoutEffect(() => {
if (queueCreate) {
queueCreate(append);
} else {
append();
}
}, []);
```
其中 `queueCreate` 是通过 `context` 获取,目的是为了防止在嵌套层级下,子元素创建先于父元素的情况:
```tsx
<Modal title="Hello 1" open>
<Modal title="Hello 2" open>
<Modal>
<Modal>
```
```html
<!-- Child `useLayoutEffect` is run before parent. Which makes inject dom before parent -->
<div data-title="Hello 2"></div>
<div data-title="Hello 1"></div>
```
通过 `queueCreate` 将子元素的 `append` 加入队列,然后再通过 `useLayoutEffect` 执行:
```tsx
// pseudocode
const [queue, setQueue] = useState<VoidFunction[]>([]);
function queueCreate(appendFn: VoidFunction) {
setQueue((origin) => {
const newQueue = [appendFn, ...origin];
return newQueue;
});
}
useLayoutEffect(() => {
if (queue.length) {
queue.forEach((appendFn) => appendFn());
setQueue([]);
}
}, [queue]);
```
### 问题分析
由于上述的队列操作,使得 portal 的 dom 在嵌套下会在下一个 `useLayoutEffect` 触发。这导致添加节点行为后于 `rc-dialog` 启动动画的 `uesLayoutEffect` 时机,导致元素不在 document 中而无法获取正确的坐标信息。
由于 Modal 已经是开启状态,其实不需要通过 `queue` 异步执行,所以我们只需要加一个判断如果是开启状态,直接执行 `append` 即可:
```tsx
// pseudocode
const appendedRef = useRef(false);
const queueCreate = !appendedRef.current
? (appendFn: VoidFunction) => {
// same code
}
: undefined;
function append() {
// This is not real world code, just for explain
document.body.appendChild(document.createElement('div'));
appendedRef.current = true;
}
// ...
return <PortalContext value={queueCreate}>{children}</PortalContext>;
```
以上。

View File

@ -1,7 +1,7 @@
---
title: about antd test library migration
date: 2022-12-20
author: zombieJ,li-jia-nan
author: li-jia-nan,zombieJ
---
Hello, I am **[@li-jia-nan](https://github.com/li-jia-nan)**. It is also a new Collaborator who joined antd in the past few months. Fortunately, as one of the Collaborators, I developed the **[FloatButton component](/components/float-button)** and **[QRCode component](/components/qrcode)**, as well as some other maintenance work. Let me share the migration of the antd test library son~

View File

@ -1,7 +1,7 @@
---
title: antd 测试库迁移的那些事儿
date: 2022-12-20
author: zombieJ,li-jia-nan
author: li-jia-nan,zombieJ
---
大家好,我是 **[@li-jia-nan](https://github.com/li-jia-nan)**。也是前几个月新加入 antd 的 Collaborator, 有幸作为 Collaborators 之一,我开发了 **[FloatButton](/components/float-button-cn)** 组件和 **[QRCode](/components/qrcode-cn)** 组件,以及一些其它维护工作,下面分享一下 antd 测试库迁移的那些事儿~