최근에 Modal.useModal의 contextHolder가 다른 위치에 배치될 때 다른 위치에 팝업되는 발견했습니다:
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>
);
};
일반 버전:
문제가 있는 버전:
위 이미지에서 볼 수 있듯이 컨텍스트홀더가 모달 내부에 배치되면 훅 호출이 올바른 위치에 나타나지 않습니다.
아이디어
antd의 기본 모달은 마우스 위치 프로퍼티를 받아 팝업 위치를 제어하는 rc-dialog 컴포넌트 라이브러리 (Dialog/Content/index.tsx )를 호출합니다:
// pseudocode
const elementOffset = offset(dialogElement);
const transformOrigin = `${mousePosition.x - elementOffset.left}px ${
mousePosition.y - elementOffset.top
}px`;
오프셋 메서드는 양식 자체의 좌표 위치를 가져오는 데 사용됩니다:
// pseudocode
function offset(el: Element) {
const { left, top } = el.getBoundingClientRect();
return { left, top };
}
중단점을 디버깅하면 마우스 위치의 값은 정확하지만 오프셋에서 얻은 rect의 값이 잘못되었다는 것을 알 수 있습니다:
{
"left": 0,
"top": 0,
"width": 0,
"height": 0
}
이 값은 양식 컴포넌트가 애니메이션 시작 노드의 DOM 트리에 추가되지 않았음을 의미하므로 다이얼로그 추가의 로직을 살펴봐야 합니다.
createPortal
를 통해 문서에 노드를 생성한 다음 ReactDOM.createPortal 통해 해당 노드에 컴포넌트를 렌더링합니다. contextHolder의 위치에 따라 동작이 달라지는 것은 문서에서 노드를 생성하는 타이밍에 문제가 있음을 시사하므로 기본적으로 노드를 추가하는 rc-portal의 부분 (useDom.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();
}
}, []);
를 사용하여 중첩된 계층 구조에서 하위 요소가 부모 요소보다 먼저 생성되는 것을 방지합니다:
<Modal title="Hello 1" open>
<Modal title="Hello 2" open>
<Modal>
<Modal>
<!-- Child `useLayoutEffect` is run before parent. Which makes inject dom before parent -->
<div data-title="Hello 2"></div>
<div data-title="Hello 1"></div>
// 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]);
문제 분석
위의 대기열 조작으로 인해 포털의 돔은 중첩될 때 다음 useLayoutEffect에서 트리거됩니다. 이로 인해 노드 추가 동작이 rc 다이얼로그 시작 애니메이션의 uesLayoutEffect 타이밍 이후에 추가되어 요소가 문서에 없고 올바른 좌표 정보를 얻지 못하는 결과가 발생합니다.
모달이 이미 켜져 있으므로 대기열을 통해 비동기적으로 실행할 필요가 없으므로 모달이 켜져 있으면 직접 추가를 실행할 수 있다는 판단만 추가하면 됩니다:
// 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>;
위.



