У меня есть кнопка логаута:
По клику на ней должен выкатываться оверлей и закатываться обратно, если пользователь нажимает на кнопку отмены:
Реализовать это на чистом JS совершенно плевое дело, а вот с реактом не совсем ясно.
Напрашивается решение с useContext, но смущает, что у компонентов есть разные точки монтирования (у меня SSR и каркас разметки формируется на сервере). Я попробовал реализовать таким образом, чтобы один компонент рендерил другой и, судя по варнингу реакта, это в корне не верный путь:
Warning: You are calling ReactDOMClient.createRoot() on a container that has already been passed to createRoot() before. Instead, call root.render() on the existing root instead if you want to update it.
.
На всякий случай приведу здесь код черновика.
Сам оверлей определяет высотку корневого элемента, в который он должен монтироваться и, в зависимости от переданного параметра, смещается вверх:
import { useState, useEffect } from 'react';
export interface PropsOverlay {
/**
* Элемент, в который будет монтироваться оверлей.
* Должен иметь position: relative
*/
rootElementId: string;
isHidden: boolean;
content: JSX.Element;
}
export function Overlay(props: PropsOverlay) {
const [elHeight, setHeight] = useState<number>();
useEffect(() => {
const el = document.getElementById(props.rootElementId);
if (el) {
setHeight(el.offsetHeight);
}
}, []);
const hiddenStyle = {
position: 'absolute',
height: elHeight + 'px',
width: '100%',
top: `-${elHeight}px`,
backgroundColor: 'aliceblue',
visibility: 'visible',
} as React.CSSProperties;
const shownStyle = {
...hiddenStyle,
top: 0,
};
return <div style={props.isHidden ? hiddenStyle : shownStyle}>{props.content}</div>;
}
Затем я написал компонент с кнопкой, по которой оверлей должен закрываться (не закрывается):
interface PropsLogoutOverlayContent {
chancelButtonClickHandler: () => void;
}
export function LogoutOverlayContent(props: PropsLogoutOverlayContent) {
return (
<div className="_content">
<button onClick={props.chancelButtonClickHandler}>Отмена</button>
<button>Выйти</button>
</div>
);
}
И наконец вызываю все это из кнопки логаута:
import { useState, useEffect } from 'react';
import { createRoot } from 'react-dom/client';
import { LogoutOverlayContent } from './LogoutOverlayContent';
import { Overlay } from './Overlay';
export function LogoutInitButton() {
const [isHidden, setHidden] = useState(true);
const toggle = () => {
setHidden(!isHidden);
};
useEffect(() => {
const container = document.getElementById('LogoutOverlay');
if (container) {
const root = createRoot(container);
root.render(
<Overlay
isHidden={isHidden}
content={
<LogoutOverlayContent
// Это не сработает, окно не закроется
chancelButtonClickHandler={toggle}
/>
}
rootElementId="LogoutOverlay"
/>,
);
}
}, []);
return (
<button className="LogoutInitButton" onClick={toggle}>
X
</button>
);
}
То есть получилась жесть, когда компонент перемонтируется при каждом нажатии на логаут, а кнопка закрытия не работает.
Я правильно понимаю, что для подобной задачи вложенный рендеринг это неверный путь и правильным вариантом будет:
1) создать контекст страницы (например, при помощи MobX) и увязать все три компонента на этот контекст (оверлей, контент оверлея и кнопка логаута)
2) дважды вызывать функцию рендеринга, т.к. у меня две точки монтирования: кнопка логаута находится в одном DOM-узле, а оверлей в другом.
UPD. Пока готовил вопрос, пришла мысль, чтобы не городить контекст ради одного параметра isHidden, попробовать вынести состояние в кастомный useOverlayPosition(), который и дергать по onClick. Хук будет позиционировать оверлей через модификацию CSSOM. Опять же не ясно, насколько это согласуется с React best practices.