フロントエンド開発の過程で直面するバグには大きく分けて二つある。一つは論理的な誤りやタイプミスのような「コードの問題」、もう一つは「タイミングの問題」である。

前者はデバッグが明確である。コードを修正すれば解決する。しかし、後者は 次元が異なる。 コードに誤りはないのに動作しない。今回の記事はDjangoベースのサービスでEasyMDEとAlpine.jsを連携させた際に遭遇した、フロントエンド開発の難題であるタイミングバグの追跡記録である。

「原因はコードではなく、実行時点だった。」


💥 症状:テキストは表示されるが制御不能



Djangoテンプレート上でMarkdownエディタのEasyMDEとDOM制御のためのAlpine.jsを同時に使用していた。特に「下書きの復元」機能を実装している際に致命的な問題が発生した。

サーバーから受信した raw_markdown データは正常にエディタにロードされた。しかしその直後にエディタがフリーズしてしまった。

  • カーソル移動不可: エディタ内部をクリックしてもカーソルが表示されない。
  • 入力不可: キーボードイベントが全く反応しない。
  • UIの崩れ: エディタ上部に不明な余白が発生。
  • コンソールエラー: クリック時に次のような致命的エラーが発生。
Uncaught TypeError: can't access property "map", r is undefined
Error: Incorrect contents fetched, please reload.

このエラーはEasyMDEのコアであるCodeMirrorがDOMとの同期を失ったときに発生する。つまり、ライブラリが制御しようとするDOM要素が予想と異なる状態にあるということである。


🔍 仮説と検証

仮説1: インスタンスの重複初期化?

EasyMDEは同一要素に対して二度初期化されると内部状態が混乱する傾向がある。ログを通じて初期化時点を確認した。

[postEditor] Creating new EasyMDE instance
[postEditor] EasyMDE already initialized

重複防止ロジックを追加したが、問題は依然として残った。特に「新規作成」時には正常に動作し、「下書き復元」時にのみエラーが発生する点から、単なる初期化の問題ではなかった。

仮説2: Textarea値の注入方法の問題?

サーバーサイドレンダリング時に <textarea> 内部に値を直接埋め込む方法が問題であるか疑ったが、これは標準的な使い方でありEasyMDEはこれを正常にパースする必要がある。これも原因ではなかった。


🔍 原因分析: Alpine.jsとEasyMDEの速度差



問題の核心は**DOM生成とライブラリ初期化の間の「競争状態(Race Condition)」**であった。

Alpine.jsのレンダリングプロセス

  1. x-data 初期化およびJavaScriptオブジェクトの生成。
  2. DOMパースおよび x-ifx-showclass バインディングの適用。
  3. 非同期的に実際のDOM更新を行う。

EasyMDEの初期化条件

  1. new EasyMDE() が実行される瞬間にターゲット <textarea> がDOMツリーに存在しなければならない。
  2. 対象要素のサイズ(幅/高さ)と位置が計算可能でなければならない (隠れていない状態である必要がある)。
  3. 初期化直後にDOM構造が変更されてはいけない。

失敗した実行フロー

私のコードは次のように動いていた。

  1. Alpine: DOMを構成し属性を変更中。
  2. JS: init() 関数が実行され new EasyMDE() を呼び出す。
  3. EasyMDE: 不完全なDOMを基に内部座標計算を完了。
  4. Alpine: 後れを取ってDOMレンダリングを完了 (属性変更等)。
  5. EasyMDE: 「初期化時点のDOMと現在のDOMが異なる」と判断してクラッシュ。

⭐ 解決: $nextTick()で時点同期

解決策はライブラリの初期化時点を**「DOMレンダリングが完全に終わった後」**に遅らせることだった。Alpine.jsはこれを行うために$nextTick()マジックメソッドを提供する。

// 修正されたコード
this.$nextTick(() => {
    this.initEditor();
    this.initDropzone();
    this.loadFromLocalStorage();
});

このコードはAlpineに次のよう指示する。

「現在進行中のDOM更新サイクルが全て終わったら、その次のティック(Tick)でこの関数を実行して。」

正常化された実行フロー

  1. Alpineが全てのDOM操作を完了。
  2. $nextTick() コールバックが実行。
  3. 完全な状態の <textarea> 上でEasyMDEを初期化。
  4. 正常に動作。

image

🎯 重要な要約と教訓

今回のデバッグを通じてフロントエンドライブラリ使用時に必ず考慮すべき原則を再確認した。

  1. タイミングが全て: コードが論理的に正しくても実行順序が間違っていればバグである。特にDOM操作フレームワーク(Alpine, Vue, React)と外部UIライブラリ(Editor, Sliderなど)を一緒に使う際には注意が必要だ。
  2. 初期化はレンダリング後に: UIライブラリは必ずターゲットDOMが安定した状態で初期化しなければならない。
  3. NextTick活用: フレームワークが提供するレンダリング完了シグナル($nextTick, useEffect, onMounted)を積極的に活用して実行時点を制御すべきだ。

バックエンドがフローを制御する領域であれば、フロントエンドは数多くの非同期イベントとレンダリングサイクルの調和が必要な領域であることを再認識させられる経験であった。