PLAY DEVELOPERS BLOG

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

React 17 から React 19 への段階的なバージョンアップと Legacy Context の話

皆様はじめまして。
24卒で新卒入社いたしました、OTTサービス技術部の篠原です。

本稿では、React 17 から React 19 へのバージョンアップに伴い実施した対応について紹介します。

前提

本プロジェクトでは、約450ファイル・総行数約43,700行の規模の React アプリケーションを、React 17 から React 19 へバージョンアップしました。
React 19 では多くのレガシーな機能が廃止され、それに伴い大規模なコード修正が必要となり、最終的に全体のおよそ 3 分の 1 にあたる、113 ファイルを変更、1000 行以上を書き換えました。 中でも Legacy Context の移行は影響範囲が広く、対応箇所が多かったため、本稿ではその詳細にもついて解説します。

React 19 への移行のポイント

バージョンアップする前に、移行に必要な修正を済ませておくことが重要です。

全体の移行方針

削除予定の API (findDOMNode, Legacy Context など) を React 17 の時点で修正しました。
一度に移行せず、React 17 → React 18 → React 19 の順に段階的にバージョンアップし、各バージョンで動作確認しながら進めました。

この移行方法を採用した背景

最初に React 17 → React 19 へ直接バージョンアップを試みましたが、100件以上の型エラーが発生し、ビルドができない状態になりました。
TypeScriptを使用している場合は、Reactのバージョンアップと同時に @types/react のバージョンアップも行うため、その影響による型エラーが発生していました。
ビルドが通らないと動作確認ができず、修正作業が困難になります。

また、同時にその他のエラーも多数発生しており、エラーの原因の切り分けが困難な状態になりました。
そのため、まず React 17 で修正できる箇所を事前対応し、ビルド可能な状態を維持しながら段階的に移行する方針に変更することでスムーズに移行できました。

React 19 への移行で発生した問題と対応

React 17 の時点で対応した修正

childContextTypesgetChildContext() が廃止され、createContext() へ移行しました。
影響範囲が広く、多くのコンポーネントで修正が必要でした。
これについては、本稿の後半で紹介します。

React 17 → React 18 への移行で発生した問題

型エラーが大量に発生し、ビルドが通らなくなりました。

原因

@types/react をバージョンアップしたことで、既存の型定義と互換性がなくなってしまったことで発生しました。

対応

すぐに対応可能な型エラーについてはその場で修正対応を行いました。
一部の込み入ったエラーについては、一時的に @ts-ignore を使用してスキップしつつ、その後段階的にエラーを解消していくことで対応しました。

React 18 → React 19 への移行で発生した問題

アプリケーションが依存しているパッケージの内部で、React 19 で削除される React の機能が使われていました。

原因

本プロジェクトではポップアップ表示に使用していた「react-transition-group」というライブラリが、React 19 に対応していなかったため、ポップアップが表示されないという事象が発生しました。
具体的には、「react-transition-group」の内部で React 19 で削除された「findDOMNode」が使用されており、これにより CSS Transition を用いたアニメーションが正常に動作しない状態となっていました。

対応

「react-transition-group」には nodeRef という props があり、これを指定すれば内部的に findDOMNode は使われません。
よって、以下のように CSSTransition に nodeRef を指定する対応を行いました。
この対応により、React 19 環境でもポップアップ機能が正常に動作するようになりました。

return (
  <CSSTransition
    nodeRef={this.popCardRef}
    classNames="pop-bounce"
    timeout={{ exit: 400, enter: 400 }}
  >
    {children}
  </CSSTransition>
);

codemod を使った自動移行の試み

codemod とは

Reactが提供するバージョンアップ支援スクリプトです。
手作業でコードを書き換えなくても、React 19 への移行に伴う非推奨コードの修正を自動適用できます。

React 18→19 向けには以下の codemod が用意されています。

  1. npx codemod@latest react/19/migration-recipe (2から6のcodemode を全て実行)
  2. npx codemod@latest react/prop-types-typescript
  3. npx codemod@latest react/19/replace-string-ref
  4. npx codemod@latest react/19/replace-act-import
  5. npx codemod@latest react/19/replace-reactdom-render
  6. npx types-react-codemod@latest preset-19 ./path-to-app

codemod の実行結果

codemod の適用範囲は限定的で、結局多くの修正を手作業で行う必要がありました。

codemod 実行の成功箇所

unmountComponentAtNode() のみ変更されました。それ以外の非推奨は全て手動で対応しました。
npx codemod@latest react/19/replace-reactdom-render を実行し以下のように自動変換されました。

// before
ReactDOM.unmountComponentAtNode(this.mountNode);
// after
const root = createRoot(this.mountNode);
root.unmount();

codemod 実行の注意点

propTypes の移行

npx codemod@latest react/prop-types-typescript を実行すると、propTypes を interface に自動変換できます。
しかし、propTypes の定義が不足していたり適切でなかったりした場合、不完全な interface が作成され、その結果型エラーが発生する場合があります。
そのため、今回のタイミングでは codemod を使用せず、バージョンアップとは別のタイミングで手動で型を適切に設定しながら interface へ移行する方針としました。

Legacy Context 移行対応

React の Context とは

ReactのContext は、コンポーネント間でデータを共有する仕組みです。
Contextを使えば、props を何度も渡さずに、コンポーネントでデータを受け取れます。
ただし、コンテキストは作成したコンポーネントより下の階層でしか使えず、作成したコンポーネントより上の階層ではデータを取得できません。

Legacy Context の廃止

React 19 では、以下の Legacy Context に関する API が廃止されました。

  • getChildContext()
  • childContextTypes
  • contextTypes

Context の新旧比較

Legacy Context と新しい Context の比較表

項目 Legacy Context 新しい Context
宣言と型定義 childContextTypes を使って値の型を明示する必要がある createContext() でコンテキストの定義が必要だが値の型定義は不要(TypeScript を使うなら別途定義)
値の渡し方 getChildContext() を使用 React.createContext() および <Context> を使用
利用方法 contextTypes を指定してアクセス contextType を指定してアクセス、または useContext() を使用して取得
依存関係の管理 供給側と利用側は contextTypes のキーが一致すればよい 利用側はコンテキストオブジェクトの import が必要
柔軟性 どこで定義されたかを意識せずに利用可能(逆に言えば、キー名が衝突すると意図しない値が受け渡しされる可能性がある) 利用側がコンテキストオブジェクトを明示的に指定する必要がある(別のコンテキストと混じるおそれがない)

従来の getChildContext() を使用したコンテキストの受け渡しは廃止され、代わりに React.createContext() を使用することが推奨されます。

旧来の Legacy Context の使い方

旧来の Legacy Context では、値を渡す側のコンポーネントごとに getChildContext() を定義し、値を受け取る側のコンポーネントも contextTypes を明示的に指定します。

親コンポーネント

  • getChildContext() で、子コンポーネントへ渡すコンテキストを定義
  • childContextTypes を使用して型を定義
class ParentContextProvider extends Component {
  static childContextTypes = {
    hoge: PropTypes.string,
  };

  getChildContext() {
    return {
      hoge: "hogeValue",
    };
  }

  render() {
    return (
      <div>{this.props.children}</div>
    );
  }
}

子コンポーネント

  • contextTypes を使用して、親コンポーネントから受け取るコンテキストの型を明示
class Child extends Component {
  static contextTypes = {
    hoge: PropTypes.string,
  };

  render() {
    return <div>{this.context.hoge}</div>;
  }
}

新しいコンテキストの使い方

新しい Context では、createContext() を使って Context を作成し、<Context> コンポーネントでラップすることで全ての子コンポーネントでコンテキストを渡します。

親コンポーネント

  • React.createContext() を使用して HogeContext を定義
  • <HogeContext> コンポーネントでラップしてコンテキストを供給
  • 子コンポーネントへ渡す値は value プロパティに設定
export const HogeContext = React.createContext({});

class ParentContextProvider extends Component {
  hogeContextValue = {
    hoge: "hogeValue",
  };

  render() {
    return (
      <HogeContext value={hogeContextValue}>
        <div>{this.props.children}</div>
      </HogeContext>
    );
  }
}

なお、React 18 までは <HogeContext.Provider> と書く必要がありましたが、React 19 では <HogeContext> だけで書けるようになり、<HogeContext.Provider> という書き方は今後非推奨となります。

子コンポーネント

  • static contextType = HogeContext; でコンテストを受け取る
import { HogeContext } from './ParentContextProvider';

class Child extends Component {
  static contextType = HogeContext;

  render() {
    return <div>{this.context.hoge}</div>;
  }
}

まとめ

段階的な移行がおすすめ
React 17 → 18 → 19 の順に進めることで、各段階で動作確認を行いながら問題を切り分けられます。

codemod の適用範囲は限定的
期待通り動かない箇所があり、手動で対応する必要があります。

Legacy Context の移行が大変
Legacy Context を使用していた場合は、childContextTypesgetChildContext() の削除により、多くのコンポーネントで修正が必要となります。

最後に

本稿では、React 17 から React 19 へのアップグレードに伴い実施した対応について紹介しました。 React 19 へのバージョンアップを検討されている方は、ぜひ参考にしてください!