Reactで編集Modalを実装するときは型定義を分けても良いかもしれない。
Reactでモーダル を扱う場合は
<Modal show={show} onHide={onHide} />
通常上記のような書き方になると思う。というか世の中のライブラリ全般のインターフェースがそうなっている。
ここでTODOアプリケーションを考えてみる。
TODO一覧があり表示されており、編集をタップすると編集するモーダル が出てくる。
これを普通に実装すると、以下のようになるのではないだろうか。
const TodoApp = () => (
const [show, setShow] = useState(false);
const [targetTodo, setTargetTodo] = useState<Todo | null>(null);
const showEditModal = (todo: Todo) => {
setShowEditModal(true)
setTargetTodo(todo)
} ;
/* 機能や実装の仕方によっては状態がもっと増えここが肥大化する */
return (
<Wrapper>
{ /* TODO 一覧のコンポーネント。編集ボタンがあり押下時にonEditが呼ばれる。 */ }
<ToDoList todos={todos} onEdit={showEditModal}/>
<ToDoEditModal show={showEditModal} onHide={() => setShow(false)} todo={targetTodo} />
</Wrapper>)
);
}
これについて考えたことをメモする。
型定義
シンプルに定義するとこうなるだろうか?
interface EditTodoModalProps {
show: boolean;
onHide: () => void;
todo:? Todo;
}
だが、showがtrueの時はtodoが存在し、showがfalseの時はtodoは存在しないだろう。
これは交差型を使って以下のように定義できそうである。
type HiddenModalProps = {
show: false,
onHide: () => void;
}
type VisibleModalProps = {
show: true,
onHide: () => void;
todo: Todo
}
type EditModalProps = HiddenModalProps | VisibleModalProps
Modal側の実装はこうゆう感じだろうか。
export const EditModal: VFC<Props> = props => {
return (
<Modal show={props.show} onHide={props.onHide}>
{props.show && <EditModalContent {...props} />}
</Modal>
);
};
const EditModalContent: VFC<VisibleModalProps> = () => (
// ここではTodoがnullじゃないことが型で保証されていて、不要なnullチェックが防げる。
// todo?.title || '' 等のフォールバックやif(todo) {} といった分岐である。
useEffect(() => {}, [todo])
)
コードが冗長になるというデメリットがあるが、
- nullチェックが不要になり扱い安い。(絶対存在するとわかっているのに、if文での条件判定や、|| での空指定をするのは微妙だし、かといって
!
で強制アンラップもしたくない。) - 非表示の時に余計なコンポーネントがマウントしない。ライブラリによってはlazyオプションとかがあったりもする。
とうのメリットはありそうだ。
カスタムhookのインターフェース
今の所こうゆうインターフェースのhookを作るのが良いと思っている。
const [editModalProps, showEditModal] = useEditTodoModal();
return (
<Wrapper>
<ToDoList todos={todos} onEdit={openEditModal}/>
<EditToDoModal {...editModalProps}/>
</Wrapper>
)
const useEditModal = (): [EditModalProps, (todo: Todo) => void] => {
const [todo, setTodo] = useState<Todo | null>(null)
// この例に限ってはshowという状態は不要で導出可能であり不要。
const show = editingTodo === null;
const onHide = () => setTodo(null)
const showEditModal = (todo: Todo) => setTodo(todo)
const props = show ? {
show,
todo: editingTodo,
onHide
} : {
show,
onHide
} // この辺が少し冗長にはなってしまうが..
return [props, showEditModal];
}
この場合、モーダルを閉じるにはeditingTodoをnullにする必要がある。
これは開けた人が閉めるを強制できて実は良いと思っている。
最初の例だと開く時に指定すれば十分であり、実相によってはモーダルを閉じたあとも編集中のtodoという状態は保持していることもあるだろう。(ファイルの読み書きや、購読処理等であれば不要なタイミングで開放すると思う)
そうゆうメリットはあるかもしれない。
またモーダルを管理するのに必要なロジックや状態をhookの中に閉じ込めることができるのは嬉しいと思う。
これはシンプルなモーダルの例だが、実際にはもっとpropsが増えたりするだろう。
モーダルだって複数表示するかもしれない。(この例に限って言えば作成と編集モーダルを共通化する実装方針も考えられるが...)
きちんとカプセル化されていると、そういった時に嬉しいと思う。
以上、実装しながら考えてたことメモでした。
Discussion