【React/TypeScript】export default function のホイスティングで発生するエラーと落とし穴まとめ
React でコンポーネントを定義するとき、多くの人が当たり前のように使うexport default function。
しかしホイスティング[1] により、予期しない動作やバグにつながるケースがあります。
本記事では、実際に開発現場で発生しがちなexport default functionを使ったことで起こる落とし穴を、React/Next.js、TypeScriptの観点からわかりやすく解説します。
キーワード
- Reactホイスティング
-
export default functionエラー - React関数コンポーネント順序
-
async、useEffect不具合 - TypeScriptコンポーネント型推論
- アロー関数 vs function どっち
なぜ export default function が危険になり得るのか?
export default functionは「関数宣言」扱いになるため、ファイル読み込み時点でホイスティングされます。この仕様により本来意図しない順序でコンポーネントや関数が実行され、Reactのライフサイクルやhooksの挙動と衝突することがあります。
以下では実際に発生しやすい 4 つの問題を具体例とともに解説します。
1. コンポーネントの定義順序に依存したバグが発生しやすい
export default functionはホイスティングされるため、定義より前に参照されてもエラーになりません。
これは一見便利ですが、大規模プロジェクトでは依存関係が複雑になり、次のような問題が起こり得ます。
- どのタイミングでコンポーネントが評価・実行されているかが不明瞭になる
- 動的importや条件分岐が絡むと評価順がズレ、予期しない動作に繋がる
MyComponent.tsx
export default function MyComponent() {
return <div>Hello, World!</div>;
}
使用例(別ファイル)
import MyComponent from './MyComponent';
function App() {
return (
<div>
<MyComponent /> {/* ここでは問題なく動作する */}
</div>
);
}
export default App;
小規模なら問題なしですが、「インポート順・評価順に依存している」状態を放置すると、後々のバグの温床になります。[2]
2. 非同期処理(useEffect / API 呼び出し)でレースコンディションが起きやすくなる
次のようなコードはよく書かれますが、いくつか問題があります。
useEffect(() => {
fetchData(); // ← ホイスティングされた関数を参照
}, []);
const fetchData = async () => {
const response = await fetch('/api/data');
setData(await response.json());
};
問題点
-
fetchData関数がホイスティングされ、評価・参照タイミングが曖昧になる - 状態更新と非同期の完了タイミングが競合(レースコンディション[3])する
- 結果として意図しない再レンダリング、古い
stateの参照などが起き得る
特に非同期処理の前提が「定義順依存」で壊れるため、バグ調査が非常に難しくなります。[4]
TypeScriptの型推論が弱くなり、型定義が冗長になりがち
export default function MyComponent(): JSX.Elementのように書くと、戻り値型の指定が必須になるケースが増えます。
export default function MyComponent(): JSX.Element {
return <div>Hello, World!</div>;
}
一方、アロー関数ならprops型や戻り値の推論が強力に働き、よりシンプルになります。
const MyComponent: React.FC = () => {
return <div>Hello, World!</div>;
};
型周りのミスが減るため、TypeScriptではアロー関数の方が実用的です。
4. React Hooks(useState / useEffect)との相性が悪く、副作用がズレることがある
Hooksは呼び出し順・レンダリング順が極めて重要です。
ところがexport default functionのホイスティングにより、下記が意図せず前後する可能性があります。
- 参照される関数のタイミング
-
stateの更新順 -
useEffectの発火タイミング
import React, { useEffect, useState } from 'react';
export default function App() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('Effect runs');
}, [count]);
// コンポーネント内で関数を呼び出すときに予期しない挙動が発生する可能性がある
const increment = () => setCount(count + 1);
return (
<div>
<button onClick={increment}>Increment</button>
</div>
);
}
このコード自体は正しく動きますが、別ファイルから関数を参照したり、順番が複雑になるとどのタイミングでstateが更新されるのか分かりにくくなるため、不具合の温床になります。
結論:React + TypeScript ではアロー関数が安全でベストプラクティス
まとめるとexport default functionによる主なリスクは次の4点です:
- コンポーネントの定義順序に依存し、予期しない動作が発生しやすい
- 非同期処理や
useEffectの実行タイミングがズレ、バグの原因になる - TypeScriptの型推論が弱くなり、型定義が冗長になる
- Hooksの挙動とホイスティングが噛み合わず、意図しない状態更新が起きる
推奨:アロー関数でコンポーネントを定義する
const MyComponent: React.FC = () => {
return <div>Hello</div>;
};
export default MyComponent;
アロー関数なら、下記が保障されます。
- ホイスティングが起こらない
- 評価順が明確
- hooksの順序が保証される
- TypeScriptの型推論が強力
- バグの再現性が高くなる(デバッグしやすい)
React + TypeScript の実務ではほぼアロー関数一択といっても過言ではありません。
おわりに
Reactは便利な反面、JavaScriptの仕様に深く依存するため、なんとなく動くコードのまま開発が進むと、後々手痛いバグに繋がります。
本記事が開発中の謎バグ解決の糸口になれば幸いです。
-
関数宣言がスコープの先頭に持ち上がるJavaScriptの仕様 ↩︎
-
例えば、コンポーネントの定義が動的に変わる場合、関数宣言に依存していると、意図しないタイミングでコンポーネントが呼ばれることがあり得ます。特に、大規模なアプリケーションでは、コンポーネントがファイル間でインポートされる順番や、依存関係が複雑になると、ホイスティングによって呼び出し順が問題になることがあります。 ↩︎
-
Race Condition。複数の処理が同時に行われた際に競合状態によって予期しない状態が引き起こされる問題を指す ↩︎
-
コードでは、
fetchData関数をuseEffect内で呼び出していますが、もし非同期コードのタイミングがうまく合わないと、コンポーネントの再レンダリングや状態更新が予期しないタイミングで発生することがあります。関数宣言がホイスティングされると、非同期処理の呼び出しタイミングが明確でないため、意図しない結果が返されることが考えられます。 ↩︎
Discussion