余談
最近、仕事で React.js と Next.js を使い始めました。 Vue.js -> Nuxt.js -> React.js -> Next.js ときて、これでひと通り(?)コンプです。
双方扱ってみて感じたのは、ささっと作ることを楽しむなら Vue.js / Nuxt.js 、考り方を楽しんで作るなら React.js / Next.js で、車に例えると、Vue.js はオートマ車、 React.js はマニュアル車のような感じでした。それぞれに面白いです。
ただ、Typescript を採用するなら今のところ、React.js / Next.js です。
目的
結果
<iframe src="https://codesandbox.io/embed/empty-sun-42mr3?fontsize=14&hidenavigation=1&theme=dark" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" title="empty-sun-42mr3" allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-autoplay allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"></iframe>
解説
codesandbox上のソースコードを元に解説。
概要
モーダルのステート (isOpenModal)
モーダルの開閉処理 (toggleModal)
のうち、toggleModal をpropsとして、Modalコンポーネントとその children (Panelコンポーネント) に渡していく。
Modal コンポーネント
親のAppコンポーネントから受け取った toggleModal の処理を
パネル外をクリックすると実行する。
子コンポーネントにも渡す。
props
type Props = {
close: (e: any) => void;
children: React.ReactNode;
};
cloneElement
const Modal: React.FC<Props> = props => {
return (
<div onClick={props.close}> {
<div>{children}</div>
</div>
);
としたいところだけど、これだと children に props を渡せない。。。propsが渡せないと、Panel コンポーネントからモーダルを閉じることができない。。。しょうがないので、App から直接 Panel に props を渡せばいいかと思ったけど、ちょっとググったら children の代わりに cloneElement() というのがあるらしいので使ってみた。 const Modal: React.FC<Props> = props => {
return (
<div onClick={props.close}>
<div>
{React.cloneElement(props.children as any, {
close: props.close
})}
</div>
</div>
);
};
これで、children にModalコンポーネントが持つ props.close (=toggleMpdal) を渡すことができた。
Panel コンポーネント
受け取った toggleModalの処理を必要な箇所で呼び出す。
props
Modal コンポーネントから モーダルを閉じる処理を受け取るので、
type Props = {
close?: (e: any) => void;
};
モーダルを閉じる
キャンセルボタンはonClickから直接props.closeを
<button type="button" onClick={props.close}>Cancel</button>
<button type="submit" onClick={submit}>OK</button>
const submit = e => {
e.preventDefault();
{
if (props.close) {
props.close(e);
}
};
Appコンポーネント
useState() でモーダルの開閉状態を扱うステートを用意する。
const [isOpenModal, setIsOpenModal] = useState(false);
ステートを切り替える関数 (toggleModal) を用意する。
const toggleModal = e => {
if (e.target === e.currentTarget) {
setIsOpenModal(!isOpenModal);
}
};
button と Modal コンポーネントに toggleModal を渡す。Modal コンポーネントは IsOpenModal ステートに応じてON/OFFにする。
return (
<div className="App">
<button type="button" onClick={toggleModal}>
Open!
</button>
{isOpenModal && (
<Modal close={toggleModal}>
<Panel />
</Modal>
)}
</div>
);
Portal コンポーネント
これまででモーダルはできているけど、Modalコンポーネントを配置した位置にDOMが生成されるのが、なんとなく気持ち悪い。できれば、他からの影響の少ない上層の任意の場所に生成させたい。
そんな時は Portal を使うらしい。 Portal.tsximport ReactDOM from "react-dom";
const Portal = ({ children }) => {
const element = document.querySelector("#root");
return element ? ReactDOM.createPortal(children, element) : null;
};
export default Portal;
ちなみにdocument.querySelector はクライアント側の処理になるので、Next.js でやる場合は
if (process.browser) {}
で囲って、サーバー側では無視するようにしないとエラーになる。
あとはこの Portal を Modal 内に仕込む。
Modal.tsximport React from "react";
import Portal from "./Portal";
import styles from "./Modal.module.scss";
type Props = {
close: (e: any) => void;
children: React.ReactNode;
};
const Modal: React.FC<Props> = props => {
return (
<Portal>
<div className={styles.modal} onClick={props.close}>
<div>
{React.cloneElement(props.children as any, {
close: props.close
})}
</div>
</div>
</Portal>
);
};
export default Modal;
これで所定のDOM(ここでは#root、Next.js の場合は #__next)配下に Modal を生成するようになる。