🔥

React Monaco Editorのイベントハンドラに登録する際の初期化のタイミング

に公開

背景

React でファイルベースのエディタアプリを開発する際、ファイル構造ツリーからファイルを選択し、Monaco Editor でその内容を編集できるような構成は一般的です。
この記事では以下のようなコンポーネント構成を前提に、自動保存の実装時にハマったことの備忘録。(戒めです)

コンポーネント構成

  • OuterLayout(親コンポーネント)
    • アプリ全体のレイアウトを管理し、エディタに渡す状態(selectedFile)を保持します。
  • FileTreeComponent
    • ファイル構造を表示し、ファイル選択時にそのパスと内容を取得して selectedFile を更新して親に通知します。
  • Editor
    • Monaco Editor をラップしたコンポーネントで、ファイルの編集機能を提供します。
    • onChangeonBlur を通じて編集内容を親に通知します。

実装要件

Monaco Editor 上で入力後、エディタのフォーカスが外れたタイミングで、親コンポーネント(OuterLayout)が検知してそのファイル内容を自動保存する。

問題

EditoronBlur ハンドラで呼ばれる保存関数 handleSave(newContent) があります。
その中でselectedFile.pathを呼んでいるにもかかわらず、保存先が content/README.md に固定されてしまう。
ログを出してみると、なぜか selectedFile.path が意図したファイルでなく、以前選択していたファイルのパスのままです。

原因

この問題の本質は、Editor コンポーネントが selectedFile.path に値が入る前にマウントされていたことにあります。つまり、React は最初のレンダリングで selectedFile.path === '' の状態で Editor を描画し、その後の更新を正しく反映してい
ませんでした。

この結果、onBlur の中で参照していた selectedFile.path が常に空、または古いパスのままになっていました。

解決策

ライフサイクルの流れを親から正確に整えてあげる!!!(当たり前体操)

条件付きレンダリングで対応

EditorselectedFile.path がセットされたあとにだけ描画するように、条件付きレンダリングを追加しました:

<Box flexGrow={1} display="flex" flexDirection="column">
  {selectedFile.path && (
    <Editor
      filePath={selectedFile.path}
      content={selectedFile.content}
      onChange={(newContent) => {
        setSelectedFile((prev) => ({ ...prev, content: newContent }));
      }}
      onBlur={(newContent) => {
        handleSave(selectedFile.path, newContent);
      }}
    />
  )}
</Box>

この変更により、ファイルが選択されてから初めて Editor がマウントされるようになり、常に最新の selectedFile.pathonBlur 時に取得できるようになりました。

補足:useEffectでもなく条件分岐で対応する理由

useEffect を使って再設定する方法も考えられますが、MonacoEditor のように内部状態が複雑なコンポーネントの場合は、マウント時点の props が重要になるケースが多く、props 更新ではなく再マウントが求められる場面があります。

原因がわかると初心者みたいなミスだなぁと思いますが笑
Reactのレンダリングのタイミングとライフサイクルに関する理解が重要だと再認識しました。。。

まとめ

  • onBlur で親コンポーネントが状態を参照する場合、その値が正しいことを保証しなければならない。
  • 特にエディタのような重いコンポーネントでは、初期レンダリング時点の props が継続使用されることに注意が必要
  • 条件付きレンダリングによって、想定された状態のもとで確実にコンポーネントを初期化できる。

Monaco Editor に限らず、状態を props 経由で渡す非制御コンポーネントでは、似たような罠に注意する必要があります。

Discussion