環境
- React 19.0
- React Bootstrap 2.10
- Bootstrap 5.3
React Bootstrap の Modal を使って、モーダルダイアログ画面からの値を受け取る方法について考えてみた。ソースはこちら。
標準の書き方
React Bootstrap の Modal を使って、
- モーダルダイアログ画面を呼び出し。
- モーダルダイアログ画面で選択して、選択結果を返す。
を実装する場合、 React Bootstrap のサンプル通り props を使うだろう。
例えば、モーダルダイアログ側は下記のように実装した。
import React, { useState } from 'react'; import { Button, Form, Modal } from 'react-bootstrap'; /** * Modal sample dialog 1 */ export default function ModalSampleDialog1({ showDialog, onSelect }: { /** Modal ダイアログを表示するかどうか。 */ showDialog: boolean, /** * 選択時イベント。 * @param selectedFruit 選択した値。 */ onSelect: (selectedFruit: string) => void } ): React.JSX.Element { const [selectedFruit, setSelectedFruit] = useState<string>(''); /** * Modal 表示時の処理。 */ const handleShow = (): void => { setSelectedFruit(''); }; return ( <Modal show={showDialog} onShow={handleShow} > <Modal.Header> <Modal.Title>Modal Sample dialog 1</Modal.Title> </Modal.Header> <Modal.Body> <Form> <Form.Check label='Apple' name='fruit' type='radio' value='Apple' onChange={(event): void => setSelectedFruit(event.target.value)} /> <Form.Check label='Banana' name='fruit' type='radio' value='Banana' onChange={(event): void => setSelectedFruit(event.target.value)} /> </Form> </Modal.Body> <Modal.Footer> <Button onClick={(): void => onSelect(selectedFruit)}>Select</Button> </Modal.Footer> </Modal> ); }
props に用意した showDialog
を Modal.show
プロパティに渡して、 true だった場合にモーダルダイアログ表示する。その後、 Select ボタンクリック時に onSelect
を実行して、結果を呼び出し側に渡している。
呼び出し側では下記の実装を追加した。
import React, { useState } from 'react'; import { Button, Col, Container, Row } from 'react-bootstrap'; import ModalSampleDialog1 from './ModalSampleDialog1'; /** * Modal sample 1 */ export default function ModalSample1(): React.JSX.Element { const [showDialog, setShowDialog] = useState<boolean>(false); const [selectedFruit, setSelectedFruit] = useState<string>(''); /** * ダイアログを表示する。 */ const handleShowDialog = (): void => setShowDialog(true); /** * 選択ボタンをクリックした時の処理。 * @param selectedFruit 選択した値。 */ const handleSelect = (selectedFruit: string): void => { setSelectedFruit(selectedFruit); setShowDialog(false); }; return ( <div> <h2>Modal Sample 1</h2> <Container fluid> <Row> <Col> <Button onClick={handleShowDialog}>Show dialog</Button> <label>{selectedFruit}</label> </Col> </Row> </Container> <ModalSampleDialog1 showDialog={showDialog} onSelect={handleSelect} /> </div> ); }
handleShowDialog
でモーダルダイアログを表示させるために、呼び出し側の showDialog
に true を設定して、 handleSelect
で選択結果を受け取る。
この書き方の場合、
- 表示・非表示の状態を呼び出し側に持っている。
- 呼び出し側で表示用メソッド、選択結果受け取り(モーダルを閉じる処理含む)メソッドをそれぞれ用意する必要がある。
となるので、
- 呼び出し側はモーダルダイアログを起動するメソッドを呼び出せて、表示・非表示の状態を持たない方法。
- 呼び出し側で、モーダルダイアログ起動→選択結果の受け取りを1か所に書く方法。
を考えてみた。
コールバック関数
まずはコールバック関数で検討した。コールバック関数であれば呼び出し側は下記のように1か所に書ける。
const handleShowDialog = (): void => { モーダルダイアログ表示メソッド呼び出し((value) => { valueを設定。 }); };
これを解決するために useImperativeHandle(と forwardRef)を使用した。まず、公開したいメソッド showDialog
(引数 resultFunction
はコールバック関数) を持つインターフェイスを定義する。
export interface ModalSampleDialog2Ref { /** * ダイアログを表示する。 * @param resultFunction 表示終了後に実行する関数。 */ showdDialog: (resultFunction?: ResultFunction) => void; } type ResultFunction = (value: string) => void;
上記インターフェイスを指定した forwardRef
を定義して、
const ModalSampleDialog2 = forwardRef<ModalSampleDialog2Ref>((props, ref) => { ... }
useImperativeHandle
を呼び出す。 useImperativeHandle
の中に showDialog
を追加し、下記のように Ref インターフェイスに定義したメソッドを実装して、引数 resultFunction
をモーダルダイアログコンポーネント内に定義した useRef オブジェクトにコールバック関数の参照を渡す。
useImperativeHandle(ref, () => ({ showdDialog: (resultFunction?: ResultFunction): void => { setSelectedFruit(''); resultFunctionRef.current = resultFunction; setShowSelf(true); } }));
表示開始をモーダルダイアログの外から呼び出せるメソッド showDialog
に出来た事で、 Modal.show
プロパティを切り替えるタイミングを呼び出し側のプロパティではなく、モーダルダイアログコンポーネント自身で管理できるようになった。
const [showSelf, setShowSelf] = useState<boolean>(false); return ( <Modal show={showSelf}> ... </Modal> );
Select ボタンクリックは
- 自身を表示にするプロパティに false を設定。
- コールバック関数が参照できる場合は実行する。
という実装にして、選択結果を返すようにした。
const handleSelect = (): void => { setShowSelf(false); if (resultFunctionRef.current) { resultFunctionRef.current(selectedFruit); } };
呼び出し側では useRef で参照できるようにして、
const modalSampleDialog2Ref = useRef<ModalSampleDialog2Ref>(null); <ModalSampleDialog2 ref={modalSampleDialog2Ref} />
showdDialog
を呼び出してモーダルダイアルを表示し、コールバックで結果を受け取れる。
const handleShowDialog = (): void => { modalSampleDialog2Ref.current?.showdDialog((value) => { setSelectedFruit(value); }); };
モーダルダイアログ側の全実装は下記の通り。
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react'; import { Button, Form, Modal } from 'react-bootstrap'; export interface ModalSampleDialog2Ref { /** * ダイアログを表示する。 * @param resultFunction 表示終了後に実行する関数。 */ showdDialog: (resultFunction?: ResultFunction) => void; } type ResultFunction = (value: string) => void; /** * Modal sample dialog 2 */ const ModalSampleDialog2 = forwardRef<ModalSampleDialog2Ref>((props, ref) => { const [showSelf, setShowSelf] = useState<boolean>(false); const resultFunctionRef = useRef<ResultFunction>(undefined); const [selectedFruit, setSelectedFruit] = useState<string>(''); const handleSelect = (): void => { setShowSelf(false); if (resultFunctionRef.current) { resultFunctionRef.current(selectedFruit); } }; useImperativeHandle(ref, () => ({ showdDialog: (resultFunction?: ResultFunction): void => { setSelectedFruit(''); resultFunctionRef.current = resultFunction; setShowSelf(true); } })); return ( <Modal show={showSelf}> <Modal.Header> <Modal.Title>Modal Sample dialog 2</Modal.Title> </Modal.Header> <Modal.Body> <Form> <Form.Check label='Apple' name='fruit' type='radio' value='Apple' onChange={(event): void => setSelectedFruit(event.target.value)} /> <Form.Check label='Banana' name='fruit' type='radio' value='Banana' onChange={(event): void => setSelectedFruit(event.target.value)} /> </Form> </Modal.Body> <Modal.Footer> <Button onClick={handleSelect}>Select</Button> </Modal.Footer> </Modal> ); }); ModalSampleDialog2.displayName = ModalSampleDialog2.name; export default ModalSampleDialog2;
呼び出し側の全実装は下記の通り。
import React, { useRef, useState } from 'react'; import { Button, Col, Container, Row } from 'react-bootstrap'; import ModalSampleDialog2, { ModalSampleDialog2Ref } from './ModalSampleDialog2'; /** * Modal sample 2 */ export default function ModalSample2(): React.JSX.Element { const modalSampleDialog2Ref = useRef<ModalSampleDialog2Ref>(null); const [selectedFruit, setSelectedFruit] = useState<string>(''); /** * ダイアログを表示する。 */ const handleShowDialog = (): void => { modalSampleDialog2Ref.current?.showdDialog((value) => { setSelectedFruit(value); }); }; return ( <div> <h2>Modal Sample 2</h2> <Container fluid> <Row> <Col> <Button onClick={handleShowDialog}>Show dialog</Button> <label>{selectedFruit}</label> </Col> </Row> </Container> <ModalSampleDialog2 ref={modalSampleDialog2Ref} /> </div> ); }
Promise 化
次は Promise を検討した。コードはコールバック関数とほぼ同じ実装をになるが、 showdDialog
メソッドを Promise に変更するため、 Ref インターフェイスの showdDialog
は、コールバック関数用の引数を削除して、戻り値を Promise
に変更した。
export interface ModalSampleDialog3Ref { /** * ダイアログを表示する。 * @returns 選択した値を返す。 */ showdDialog: () => Promise<string>; }
useImperativeHandle の定義もインターフェイスと同じように変更する。 resultFunctionRef
には resolve を保持しておき、コールバック関数の時と同じように Select ボタンクリック時に呼び出す。
useImperativeHandle(ref, () => ({ showdDialog: async (): Promise<string> => { return await new Promise((resolve: ResultFunction) => { setSelectedFruit(''); resultFunctionRef.current = resolve; setShowSelf(true); }); } }));
呼び出し側では戻り値を then
で受け取って処理出来る。
const handleShowDialog = async (): Promise<void> => { await modalSampleDialog3Ref.current?.showdDialog() .then(value => { setSelectedFruit(value); }); };
モーダルダイアログ側の全実装は下記の通り。
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react'; import { Button, Form, Modal } from 'react-bootstrap'; export interface ModalSampleDialog3Ref { /** * ダイアログを表示する。 * @returns 選択した値を返す。 */ showdDialog: () => Promise<string>; } type ResultFunction = (value: string) => void; /** * Modal sample dialog 3 */ const ModalSampleDialog3 = forwardRef<ModalSampleDialog3Ref>((props, ref) => { const [showSelf, setShowSelf] = useState<boolean>(false); const resultFunctionRef = useRef<ResultFunction>(undefined); const [selectedFruit, setSelectedFruit] = useState<string>(''); const handleSelect = (): void => { setShowSelf(false); if (resultFunctionRef.current) { resultFunctionRef.current(selectedFruit); } }; useImperativeHandle(ref, () => ({ showdDialog: async (): Promise<string> => { return await new Promise((resolve: ResultFunction) => { setSelectedFruit(''); resultFunctionRef.current = resolve; setShowSelf(true); }); } })); return ( <Modal show={showSelf}> <Modal.Header> <Modal.Title>Modal Sample dialog 3</Modal.Title> </Modal.Header> <Modal.Body> <Form> <Form.Check label='Apple' name='fruit' type='radio' value='Apple' onChange={(event): void => setSelectedFruit(event.target.value)} /> <Form.Check label='Banana' name='fruit' type='radio' value='Banana' onChange={(event): void => setSelectedFruit(event.target.value)} /> </Form> </Modal.Body> <Modal.Footer> <Button onClick={handleSelect}>Select</Button> </Modal.Footer> </Modal> ); }); ModalSampleDialog3.displayName = ModalSampleDialog3.name; export default ModalSampleDialog3;
呼び出し側の全実装は下記の通り。
import React, { useRef, useState } from 'react'; import { Button, Col, Container, Row } from 'react-bootstrap'; import ModalSampleDialog3, { ModalSampleDialog3Ref } from './ModalSampleDialog3'; /** * Modal sample 3 */ export default function ModalSample3(): React.JSX.Element { const modalSampleDialog3Ref = useRef<ModalSampleDialog3Ref>(null); const [selectedFruit, setSelectedFruit] = useState<string>(''); /** * ダイアログを表示する。 */ const handleShowDialog = async (): Promise<void> => { await modalSampleDialog3Ref.current?.showdDialog() .then(value => { setSelectedFruit(value); }); }; return ( <div> <h2>Modal Sample 3</h2> <Container fluid> <Row> <Col> <Button onClick={handleShowDialog}>Show dialog</Button> <label>{selectedFruit}</label> </Col> </Row> </Container> <ModalSampleDialog3 ref={modalSampleDialog3Ref} /> </div> ); }
終わりに
useImperativeHandle(と forwardRef)を使った、メソッド外部公開化の良い勉強になった。