みなさん<dialog>要素を知っていますか?HTML5.2で追加された要素で、モーダルウィンドウをカンタンに実装できるステキ要素です。これをReactから使おうとしてみたのですが、Flux的なやり方ができなくて辛かったです。
…というだけの記事です。
この記事の環境
この記事は以下のバージョンのパッケージを使用しています。
- React v16.7.0
- Redux v4.0.1
- TypeScript v3.2.2
- TypeScript-FSA v3.0.0-beta-2
まず完成したコード
// actionCreator.ts import {Action} from 'redux'; import actionCreatorFactory from 'typescript-fsa'; const actionCreator = actionCreatorFactory(); export const closeDialog = actionCreator('CLOSE_DIALOG');
import * as React from 'react'; import { connect } from 'react-redux'; import ClassNames from 'classnames'; import * as styles from '../style/dialog.css'; import { closeDialog } from '../actionCreator'; const mapStateToProps = ({ opensDialog }) => ({ opensDialog }); const mapStateToDispatch = (dispatch) => ({ closeDialog() { dispatch(closeDialog()); }, }); interface Props { opensDialog: boolean; closeDialog(): void; className?: string[]; } interface State {} class Dialog extends React.Component<Props, State> { refDialog = React.createRef<HTMLDialogElement>(); constructor(props) { super(props); } componentDidUpdate(prevProps, prevState, snapshot) { // opensDialogが変更されていたらダイアログを操作する if (prevProps.opensDialog === this.props.opensDialog) return; if (this.props.opensDialog) this.refDialog.current.showModal(); else this.refDialog.current.close(); } render() { return ( <dialog ref={this.refDialog} className={ClassNames(styles.dialog, ...this.props.className)} > <form method="dialog"> name: <input type="text" /> <br /> age: <input type="number" /> <br /> <button onClick={this.props.closeDialog}>Cancel</button> <button>Confirm</button> </form> </dialog> ); } } export default connect(mapStateToProps, mapStateToDispatch)(Dialog);
よく分かる解説
このコンポーネントは、storeのopensDialogがtrue
のときにダイアログを開き、false
のときは表示しないコンポーネントです。CSSを適切に設定すれば::backdrop疑似要素も使えます。
componentDidUpdate
はReactがDOMを変更したあとに呼び出されるフックです。これは変更の前後のpropsを取得できます。ここで比較を行い、変更されていた場合にshowModal()やclose()を呼び出します。
そもそも: なんでこんなややこしい実装しているの?
実はここまでカンタンに書くこともできます。
// component/dialog.tsx import * as React from 'react'; import { connect } from 'react-redux'; import ClassNames from 'classnames'; import * as styles from '../style/dialog.css'; import { closeDialog } from '../actionCreator'; const mapStateToProps = ({ opensDialog }) => ({ opensDialog }); const mapStateToDispatch = (dispatch) => ({ closeDialog() { dispatch(closeDialog()); }, }); interface Props { opensDialog: boolean; closeDialog(): void; className?: string[]; } const Dialog: React.SFC<Props> = (props) => ( <dialog open={props.opensDialog} className={ClassNames(styles.dialog, ...props.className)} > <form method="dialog"> name: <input type="text" /> <br /> age: <input type="number" /> <br /> <button onClick={props.closeDialog}>Cancel</button> <button>Confirm</button> </form> </dialog> ); export default connect(mapStateToProps, mapStateToDispatch)(Dialog);
この実装では、componentDidUpdateによる監視もなく、さらにDialogがReactComponentではなくSFCです。いいことづくめなようですが、これはうまく動作しません。
なぜかというと、open={props.opensDialog}だと::backdrop疑似要素を使えないからです。(逆に言えば、::backdropを使わないならコレもアリです)
::backdrop疑似要素はどうやらHTMLDialogElement.showModal()を実行しないと付与されないらしく、open属性によるトグルではうまく動作してくれません。そのため、いちいちややこしいやり方しかできないようです。Flux的に実装できなくてとても苦しい…