【MapLibre GL JS + React】useEffect多用からの脱却を試みた話

cover image from Unsplash

Photo by James Lee on Unsplash

本記事の概要

Before(useEffectを使用したパターン)

以下は、地形レイヤーの3D/2D表示の状態管理を行うためのカスタムフックである。

export default function use3DView({ map }: Props) {
  const [is3DView, setIs3DView] = useState(true);
 
  useEffect(() => {
    if (!map) return;
    
    // 地図のスタイル読み込みが終わっているかの確認
    // 省略
 
    // 地形レイヤーを3D/2Dに切り替える
    map.setTerrain({
      source: "terrain",
      exaggeration: is3DView ? 1 : 0,
    });
  }, [map, is3DView]);
 
  // 3D/2D切り替えボタンに渡す
  return { is3DView, setIs3DView };
}

このコードにはいくつかの問題がある。
まず、useEffectの実行タイミングによっては、mapが存在していなかったり、スタイルの読み込みが完了する前にsetTerrainが実行されてしまう可能性がある。
そのため、毎回mapの存在チェックやisStyleLoadedの確認といったコードを先頭に書く必要があり、実装が煩雑になりやすい。

加えて、アプリの規模が大きくなるにつれて地図レイヤーの数も増え、それに伴いuseEffectの依存配列も複雑化していった。 結果として、「いつ、何によって、どの副作用が実行されるのか」が把握しづらくなり、予期しない挙動が発生しやすい状態になっていた。

After(useEffectを使用しないパターン)

Reactの公式ドキュメントのサンプルコードを参考に、副作用をイベントハンドラー内で記述するやり方に変えた。

export default function use3DView({ map }: Props) {
  const [is3DView, setIs3DView] = useState(true);
 
  const updateTerrainView = useCallback(
    (nextIs3DView: boolean) => {
      if (!map) return;
 
      // ユーザー操作に応じて、地形レイヤーの3D/2D表示を切り替える
      map.setTerrain({
        source: "terrain",
        exaggeration: nextIs3DView ? 1 : 0,
      });
    },
    [map]
  );
 
  // 3D/2D切り替えボタン用イベントハンドラ
  const onToggle3DView = useCallback(
    (nextIs3DView: boolean) => {
      setIs3DView(nextIs3DView);
      updateTerrainView(nextIs3DView);
    },
    [updateTerrainView]
  );
 
  // 3D/2D切り替えボタンに渡す
  return { is3DView, onToggle3DView };
}

この書き方により、3D/2D表示の切り替え処理の実行タイミングが明示的になり、意図しないタイミングでの発火を防ぐことができるようになった。

まとめ

参考にしたReact公式ドキュメント 関連ページ