目次
概要
React 19が正式リリースされ、多くのプロジェクトでマイグレーションを検討していることと思います。今回のブログ記事では、7つのアプリと共有コンポーネントライブラリを含む大規模モノレポをReact 19にアップグレードした実践経験を共有します。
70個以上のファイルが変更されましたが、ほとんどは@types/react@19の型変更への対応であり、ランタイム動作が変わるコードはほぼありませんでした。
主要な変更点の整理
forwardRefの削除 — refが通常のpropに昇格
React 19で最も目立つ変更はforwardRefの廃止です。refを通常のpropのように直接渡せるようになりました。
Before (React 18)
import { forwardRef, memo } from 'react';
type ActionButtonProps = {
readonly label: string;
readonly onClick: () => void;
};
const _ActionButton = forwardRef<HTMLButtonElement, ActionButtonProps>(
({ label, onClick }, ref) => {
return (
<button ref={ref} onClick={onClick}>
{label}
</button>
);
}
);
export const ActionButton = memo(_ActionButton);
After (React 19)
import { memo, type Ref } from 'react';
type ActionButtonProps = {
readonly ref?: Ref<HTMLButtonElement>;
readonly label: string;
readonly onClick: () => void;
};
const _ActionButton = ({ ref, label, onClick }: ActionButtonProps) => {
return (
<button ref={ref} onClick={onClick}>
{label}
</button>
);
};
export const ActionButton = memo(_ActionButton);
forwardRefラッパーがなくなり、コードがすっきりしました。コンポーネント内部で分離されていた_ComponentとforwardRefラッピングパターンが、シンプルな関数コンポーネント + memoに整理されます。
MutableRefObject → RefObjectに統合
@types/react@19でMutableRefObjectが廃止され、RefObjectに統合されました。以下のように型宣言で単純に置換すれば大丈夫です。
Before
import { type MutableRefObject } from 'react';
type TableContextType = {
readonly fetchError: MutableRefObject<boolean>;
readonly tableHeaderRefs: readonly MutableRefObject<unknown>[];
};
After
import { type RefObject } from 'react';
type TableContextType = {
readonly fetchError: RefObject<boolean>;
readonly tableHeaderRefs: readonly RefObject<unknown>[];
};
useRefの初期値必須化 + RefObjectの型変更
React 19ではuseRef()に初期値を必ず渡す必要があります。また、RefObject<T>がRefObject<T | null>に変更され、型にnullを明示的に含める必要があります。
Before
// 初期値なしで呼び出し(React 18では許容)
const previousDepsRef = useRef<DependencyList>();
// RefObject<T>にnull未包含
const stageRef: RefObject<Konva.Stage> = useRef(null);
After
// 初期値を明示的に渡す
const previousDepsRef = useRef<DependencyList | undefined>(undefined);
// RefObject<T | null>でnullを含める
const stageRef: RefObject<Konva.Stage | null> = useRef(null);
この変更は、特にCanvas/Stage関連のフックが多いプロジェクトで数十個のファイルに影響を及ぼしました。
カスタムフックへのジェネリック型の追加
RefObjectの型変更に伴い、カスタムフックにもジェネリック型を導入する必要がありました。useIntersectionObserverのケースを見てみましょう。
Before
export const useIntersectionObserver = (callback: () => void) => {
const observerRef = useRef<IntersectionObserver | null>(null);
const targetRef = useCallback((node: Element | null) => {
if (observerRef.current) {
observerRef.current.disconnect();
}
if (node) {
observerRef.current = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) callbackRef.current();
});
observerRef.current.observe(node);
}
}, []);
return targetRef;
};
After
export const useIntersectionObserver = <T extends Element>(
callback: () => void
) => {
const observerRef = useRef<IntersectionObserver | null>(null);
const targetRef = useCallback((node: T | null) => {
if (observerRef.current) {
observerRef.current.disconnect();
}
if (node) {
observerRef.current = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) callbackRef.current();
});
observerRef.current.observe(node);
}
}, []);
return targetRef;
};
修正後、使用する側では以下のようにジェネリック型を明示する必要があります。
const targetRef = useIntersectionObserver<HTMLDivElement>(loadMore);
グローバルJSX名前空間の廃止
React 19でグローバルJSX名前空間が廃止され、React.JSXに移動しました。カスタムWeb Componentsを使用している場合、以下のように型宣言の方式を変更する必要があります。
Before
declare global {
namespace JSX {
interface IntrinsicElements {
readonly 'custom-header': unknown;
}
}
}
After
declare module 'react' {
namespace JSX {
interface IntrinsicElements {
readonly 'custom-header': unknown;
}
}
}
ReactElement.propsがunknownに変更
@types/react@19でReactElementのpropsの型がanyからunknownに変更されました。isValidElementやcloneElementを使用する際には、以下のように型引数を明示する必要があります。
Before
if (isValidElement(child)) {
return cloneElement(child, { index: columnIndex++ });
}
After
if (isValidElement<{ index: number }>(child)) {
return cloneElement(child, { index: columnIndex++ });
}
関連ライブラリのアップグレード
React 19にアップグレードすると、関連ライブラリも合わせてアップグレードする必要があります。主な変更点は以下のとおりです。
| ライブラリ | 変更 |
|---|---|
react / react-dom | ^19 |
@types/react / @types/react-dom | ^19 |
@testing-library/react | ^16.0.0(React 19対応) |
react-konva(Canvas使用時) | ^19 |
| Storybook | ^8.5.0 |
プロジェクト別の影響度分析
モノレポでのマイグレーションは、共有パッケージの変更がすべてのアプリに影響を及ぼすため、事前に影響範囲を把握することが重要です。
| 変更カテゴリ | 影響範囲 | ファイル数 |
|---|---|---|
| 共有コンポーネント(forwardRef、RefObject等) | すべてのアプリ | 約30個 |
| Canvas/Stage関連フック(RefObject null) | Canvas使用アプリ | 約17個 |
| useIntersectionObserverジェネリック | 無限スクロール使用アプリ | 約11個 |
| JSX名前空間 | Web Components使用アプリ | 3個 |
| package.json | すべてのアプリ | 7個 |
マイグレーション戦略
ステップ1:共有ライブラリから
共有コンポーネントライブラリを最初に修正します。forwardRefの削除、RefObject型の統合、カスタムフックへのジェネリック型追加など、基盤作業を完了させます。
ステップ2:アプリごとに順次対応
共有ライブラリの変更後、各アプリで発生する型エラーを順次修正します。ほとんどは機械的な型修正なので、IDEの一括置換機能を活用できます。
ステップ3:テストとスナップショットの更新
forwardRefの削除により、コンポーネントのレンダリング結果が微妙に変わる可能性があるため、スナップショットテストを再生成します。
マイグレーションのヒント
-
TypeScriptエラーをガイドとして活用:
@types/react@19を先にインストールすると、変更が必要なすべての箇所で型エラーが発生します。これをチェックリストとして活用してください。 -
forwardRefの検索で範囲を把握:プロジェクト全体でforwardRefを検索すれば、修正が必要なコンポーネントを一目で把握できます。 -
MutableRefObjectの一括置換:単純な置換なので、IDEのFind & Replaceで素早く処理できます。 -
yarn.lockの変更は大きくても心配無用:今回のマイグレーションでは
yarn.lockだけで約5,000行の変更がありましたが、これは依存関係ツリーの再構成による自然な結果です。
おわりに
React 19マイグレーションのポイントは、ほとんどの変更が型レベルであるということです。ランタイム動作が変わるコードはほぼなく、TypeScriptのコンパイルエラーを一つずつ解決していけば、自然とマイグレーションが完了します。
forwardRefの廃止は最初は大きな変化に感じるかもしれませんが、むしろコードがよりシンプルで直感的になるポジティブな変化です。モノレポ環境では、共有パッケージ → 個別アプリの順序で進めると体系的に対応できます。
React 19がもたらすその他の新機能(Actions、use()フック、Server Componentsなど)は、マイグレーション後に段階的に導入することをお勧めします。まずは安定的にアップグレードを完了し、新機能は必要に応じて一つずつ適用していくのが安全なアプローチです。
React 19のアップグレードが完了した後は、React Compilerの導入を検討してみてください。React Compilerを使えば、手動のメモ化なしでも自動的にレンダリングが最適化されます。
私のブログが役に立ちましたか?下にコメントを残してください。それは私にとって大きな大きな力になります!
アプリ広報
Dekuが開発したアプリを使ってみてください。Dekuが開発したアプリはFlutterで開発されています。興味がある方はアプリをダウンロードしてアプリを使ってくれると本当に助かります。