Reactで編集Modalを実装するときは型定義を分けても良いかもしれない。

3 min read読了の目安(約3000字

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が増えたりするだろう。
モーダルだって複数表示するかもしれない。(この例に限って言えば作成と編集モーダルを共通化する実装方針も考えられるが...)

きちんとカプセル化されていると、そういった時に嬉しいと思う。


以上、実装しながら考えてたことメモでした。