We want to hear from you!Take our 2020 Community Survey!

React v17.0 Release Candidate: 新機能「なし」

August 10, 2020 by Dan Abramov and Rachel Nabors
日本語版サイト (ja.reactjs.org) のブログセクションへの記事掲載には英語版サイトと比べてタイムラグがあります。 最新のブログ記事は英語版でご確認ください。
日本語版サイトでは英語版ブログに定期的に追従しつつ、2020 年以降の記事を随時翻訳しています。

本日 React 17 の最初のリリース候補を公開します。前回の React メジャーリリースから 2 年半が経過しており、これは我々の基準からしても長いものです! このブログ記事では、このメジャーリリースの役割、期待される変化、そしてどのように試すことができるのかについて説明します。

新機能 “なし”

この React 17 リリースは普段のリリースと異なっており、開発者向けの新機能が何も追加されていません。代わりに、このリリースは React 自体のアップグレードを簡単にすることに焦点を当てています。

我々は React の新機能を積極的に開発中ですが、それらの新機能はこのリリースに含まれていません。React 17 のリリースは、誰も取り残されないようにしつつ新機能を展開していくという私たちの戦略の鍵となるものです。

具体的には React 17 は、あるバージョンの React で管理されるツリー内に、別のバージョンの React で管理されるツリーをより安全に埋め込めるようにするための、「踏み台」となるリリースとなっています。

段階的なアップグレード

過去 7 年間、React のアップグレードは “all-or-nothing” 型でした。古いバージョンにとどまり続けるか、新バージョンにアプリ全体をアップグレードするかの二択です。その中間は存在しませんでした。

これまではそれで何とかなっていましたが、我々は “all-or-nothing” なアップグレード戦略の限界に直面しています。例えば、古いコンテクスト API の非推奨化のようないくつかの API 変更への対応は、自動化して行うことができません。最近に書かれたアプリのほとんどはこれを一切使っていないにも関わらず、React はこれをサポートし続けています。ずっとこれをサポートし続けるか、幾つかのアプリを古いバージョンの React のまま取り残すしかしかないのです。どちらの選択肢もあまり望ましくありません。

そこで我々はもうひとつの選択肢を用意することにしました。

React 17 は段階的な React のアップグレードを可能にします。React 15 から 16 に(そして近い将来 React 16 から 17 に)アップグレードする場合、普通はアプリ全体をまとめてアップグレードします。これは多くのアプリではうまく行きます。しかしコードが数年以上前に書かれており活発にメンテされていないような場合、だんだんと難易度が増していきます。ページ内で 2 つの React のバージョンを混在させることは可能ですが、React 17 以前にこのようなことをすると不安定になり、イベント絡みの問題が引き起こされていました。

React 17 で、これらの問題の多くを修正します。これは React 18 やもっと将来のバージョンが来たときに、とれる選択肢が増えるということを意味します。選択肢のひとつは、これまでやってきたのと同様、アプリ全体を一度にアップグレードするというものです。しかし今後は、アプリを一部分ずつアップグレードするという選択肢がとれるようになります。例えば、アプリの大部分を React 18 に移行しつつ、いくつかの遅延ロードされるダイアログやサブページを React 17 のままにしておけるようになります。

だからといって段階的に更新しないといけないという訳ではありません。今後もほとんどのアプリでは、一気にアップグレードするのがベストの選択肢です。2 つのバージョンの React をロードするというのは、たとえ片方はオンデマンドで遅延ロードするのだとしても、やはり理想的ではありません。しかし、活発にメンテされていない大きなアプリではこの選択肢は検討に値するものであり、React 17 はこのようなアプリが取り残されずに済むようにします。

段階的なアップグレードを可能にするために、React のイベントシステムに幾つかの変更を加える必要がありました。React 17 がメジャーリリースとなっているのは、これらの変更が一部互換性の問題を引き起こす可能性があるからです。実際のところは、10 万を超えるコンポーネントの中で変更する必要があったのは 20 未満でしたので、ほとんどのアプリは React 17 にトラブルなく移行できると考えています。問題があった場合は知らせてください

段階的アップグレードのデモ

古いバージョンの React を必要に応じて遅延ロードするという手法をデモするためのサンプルリポジトリを用意しました。このデモは Create React App を使っていますが、他のどのようなツールでも似たアプローチが可能なはずです。他のツールを使ったデモを追加するプルリクエストを歓迎します。

補足

その他の変更は React 17 より後に延期しました。このリリースの目標は段階的なアップグレードを可能にすることです。React 17 自体へのアップグレードが難しいようでは本リリースの目的が台無しですので、そのようなことはないはずです。

イベントデリゲーションに関する変更

異なるバージョンの React で作成されたアプリをネストさせることは、技術的にはこれまでも可能でしたが、React のイベントシステムの挙動に起因して不安定なものとなっていました。

React コンポーネントでは、通常はイベントハンドラをインラインで記載します:

<button onClick={handleClick}>

このコードは素の DOM では以下と同等です:

myButton.addEventListener('click', handleClick);

しかし、ほとんどのイベントでは、実際には React はあなたが宣言した DOM ノードにイベントハンドラをアタッチするのではありません。代わりに、イベントタイプごとにハンドラを 1 つだけ、document ノードに直接アタッチします。これはイベントデリゲーション(event delegation; イベントの委譲)と呼ばれます。大きなアプリケーションツリーではパフォーマンス面で有利であるということに加え、これによりイベントのリプレイといった新機能も追加しやすくなります。

React は最初のリリース時点からこのようなイベントデリゲーションを自動的に行っていました。ドキュメントで DOM イベントが発生すると、React はどのコンポーネントを呼び出すべきか判定し、React のイベントがあなたのコンポーネントツリー内を上側に向かって「バブリング」していきます。しかしその裏側では、この時点でネイティブのイベントが既に document のレベルにバブリングし終わっているのであり、そこに React はイベントハンドラを仕込んでいるのです。

ところがこの挙動が、段階的なアップグレードにおいて問題を引き起こします。

ページ内に複数の React バージョンがあると、それらがすべてイベントハンドラをトップレベルに登録します。これにより e.stopPropagation() がおかしくなります。例えばネストされている側のツリーがとあるイベントの伝播を停止した場合でも、外側のツリーがそれを受け取ることができてしまいます。これが複数の異なるバージョンの React をネストさせるのが難しかった理由です。この懸念は架空の話ではなく、例えば Atom エディタは 4 年前に実際にこの問題に遭遇しています。

以上が、React が裏で DOM にイベントをアタッチする方法を、我々が変更しようとしている理由です。

React 17 では、React は document レベルにイベントハンドラをアタッチしないようになります。代わりに、あなたが React ツリーをレンダーしようとしているルート DOM コンテナに対してアタッチするようになります。

const rootNode = document.getElementById('root');
ReactDOM.render(<App />, rootNode);

React 16 およびそれ以前では、React はほとんどのイベントに対して document.addEventListener() のようにしていました。代わりに React 17 は、水面下で rootNode.addEventListener() という呼び出しを行うようになります。

React 17 ではイベントハンドラをドキュメントではなくルート要素にアタッチしていることを示す図

この変更のおかげで、あるバージョンの React で管理されているツリーを別バージョンの React で管理されているツリー内に埋め込むことが、より安全に行えるようになります。ただしこれがうまく働くためにはどちらの React のバージョンも 17 以上である必要があり、これが React 17 にアップグレードすることが重要である理由です。ある意味で、React 17 は、今回以降に段階的なアップグレードを行いやすくするための「踏み台」リリースであると言えます。

この変更により、他のテクノロジーで構築されたアプリ内に React を組み込むことも容易になります。例えば、外見の大部分を jQuery で書いており、その中の新しいコードが一部 React で書かれているという場合でも、今後は「React 内で e.stopPropagation() が呼ばれたイベントは jQuery のコードに到達しない」という(期待通りの)動作をするようになります。逆のことも言えます。もし React が好きではなくなってアプリを(例えば jQuery で)書き直したくなったとしても、イベント伝播処理を壊すことなく見た目部分を React から jQuery に移行することができるようになります。

この新しい挙動により、React コードと非 React コードの統合に関して 過去 数年に わたって 報告 されて きた 様々な 問題が 解決 される ことが分かっています。

補足

この変更によりルートコンテナ外にあるポータルの動作がおかしくなるのではと思っているかもしれません。が、React はポータルのコンテナでもイベントをリッスンするようになるため、問題は生じません。

問題が出た場合の修正方法

どのような破壊的変更もそうですが、コードの一部は調整する必要があるかもしれません。Facebook では(何千ものモジュールがあるうち)合計で約 10 個のモジュールを、今回の変更に合わせて調整する必要がありました。

例えば document.addEventListener(...) という形で手動で DOM リスナを登録すれば、それにより React のすべてのイベントが捕捉できると期待するかもしれません。React 16 以前では、React のイベントハンドラ内で e.stopPropagation() をコールしていたとしても、document にあるあなたのカスタムのリスナはこのイベントを受け取っていました。なぜならネイティブのイベントは、ドキュメントのレベルに既に到達していたからです。React 17 以降は、イベントの伝播は(指示された通りに!)止まるため、あなたが document に書いたハンドラは呼ばれなくなります。

document.addEventListener('click', function() {
  // This custom handler will no longer receive clicks
  // from React components that called e.stopPropagation()
});

このようなコードを修正するには、イベントリスナがキャプチャフェーズで呼ばれるようにコードを変更します。そのためには document.addEventListener の第 3 引数として { capture: true } を渡します:

document.addEventListener('click', function() {
  // Now this event handler uses the capture phase,
  // so it receives *all* click events below!
}, { capture: true });

この方針は、全体的にはむしろ障害に強くなるものだということに注意してください。この変更で、例えば、e.stopPropagation() が React イベントハンドラ外で呼ばれた場合に発生する既存のバグもおそらく修正されることでしょう。言い換えると、React 17 でのイベント伝播は普通の DOM に近くなったということです。

その他の破壊的変更

React 17 での破壊的変更は最小限にとどめてあります。例えば、前のバージョンで非推奨化されたメソッドの削除は行っていません。ただし、我々の経験で比較的安全であった破壊的変更が、少数のみ含まれています。我々のコンポーネントでこれらにより修正する必要があったコンポーネントは 10 万以上あるうちの 20 未満でした。

ブラウザとの整合性向上

イベントシステムに幾つかの小さな変更を加えました:

  • よくある誤解を防ぐため、onScroll イベントはバブリングしないようになりました。
  • React の onFocusonBlur イベントはネイティブの focusinfocusout イベントを裏で使うように変更されました。これらは React の既存の挙動とより合致しており、また追加の情報を有していることがあります。
  • キャプチャフェーズのイベント(onClickCapture など)は本物のブラウザのキャプチャフェーズのリスナを使うようになります。

これらの変更は React の挙動をブラウザの挙動に近づけて相互運用性を改善するものです。

補足

React 17 は onFocus イベント用に focus ではなく focusin を使うよう裏で変更されましたが、バブリングの挙動に影響はないということに注意してください。React において onFocus イベントは常にバブリングしていましたし、React 17 でも同様です。通常はこれがより有用なデフォルトの動作です。この sandbox で特定のユースケースのために加えることのできるチェックについて見ることができます。

イベントプーリングの廃止

React 17 では「イベントプーリング」による最適化が取り除かれています。モダンブラウザではパフォーマンス向上にならず、経験のある React ユーザですらこの挙動に混乱していました:

function handleChange(e) {
  setData(data => ({
    ...data,
    // This crashes in React 16 and earlier:
    text: e.target.value
  }));
}

上記の問題が起こるのは、React が古いブラウザでのパフォーマンス改善のために複数の異なるイベント間でイベントオブジェクトを再利用しており、その際にイベントのフィールドを null にセットしていたためです。React 16 以前では、イベントを正しく使うために e.persist() を呼ぶか、本来必要になるより先にプロパティを読み出しておく必要がありました。

React 17 では、上記のコードは本来期待される通りに動作するようになります。古いイベントプーリングによる最適化は完全に取り除かれており、必要なときにいつでもイベントのフィールドを読み出せるようになります。

これは振る舞いの変化であるため破壊的変更としてマークしてありますが、実際上は Facebook 内でこれにより壊れたものは何もありませんでした。(むしろ幾つかのバグがいつの間にか修正されたかもしれません!)e.persist() は React イベントオブジェクト内に残りますが、今後は何もしなくなります。

副作用クリーンアップのタイミング

useEffect のクリーンアップ用関数のタイミングをより一貫性のあるものにしました。

useEffect(() => {
  // This is the effect itself.
  return () => {    // This is its cleanup.  };});

ほとんどの副作用は画面の更新を遅延させる必要がないので、React は副作用を画面に更新が反映された直後に非同期的に実行します。(ツールチップのサイズ測定や位置合わせなど、副作用が画面の更新をブロックする必要がある稀なケースでは、useLayoutEffect を使うべきです)

しかし、コンポーネントがアンマウントされる際、副作用のクリーンアップ関数の方は同期的に実行されていました(クラスコンポーネントでの componentWillUnmount が同期的であるのと同様です)。これは大きなアプリでは望ましくないということが分かりました。大きな画面遷移(タブの切り替えなど)がある場合に遅くなってしまうのです。

React 17 では、副作用のクリーンアップ関数が常に非同期的に実行されます。例えば、コンポーネントがアンマウントされる時、クリーンアップ関数は画面が更新された後で実行されます。

これは副作用の本体側がどのように実行されるかに合わせたものです。同期的に実行されることに依存しているような稀なケースでは、useLayoutEffect に変更することができます。

補足

アンマウントされたコンポーネントにおける setState の警告を修正できなくなってしまうのではと心配されているかもしれません。心配は要りません。React はこのケースに対して特別なチェックを行っており、アンマウントとクリーンアップとの間の小さな時間帯に setState の警告を発生させないようになっています。リクエストやインターバルをキャンセルするためのコードはほぼ常に変えずに済みます。

加えて、React 17 は(全コンポーネントにわたる)すべてのクリーンアップ関数を、新しい副作用より前に実行するようになります。React 16 ではコンポーネント内でのみこの順番が保証されているに過ぎませんでした。

起きる可能性のある問題

この変更により動作しなくなったコンポーネントは数個のみでしたが、再利用可能なライブラリではより注意深くテストする必要があるでしょう。問題を引き起こすコードの例は以下のようなものです:

useEffect(() => {
  someRef.current.someSetupMethod();
  return () => {
    someRef.current.someCleanupMethod();
  };
});

問題は someRef.current は書き換え可能であるため、クリーンアップ関数が実行される段階では null に変わっている可能性がある、ということです。解決方法としては、副作用の内部で書き換わる可能性のある値をキャプチャしておきます:

useEffect(() => {
  const instance = someRef.current;
  instance.someSetupMethod();
  return () => {
    instance.someCleanupMethod();
  };
});

我々の eslint-plugin-react-hooks/exhaustive-deps lint ルール(是非使うようにしましょう!)は常にこれを警告してきましたので、このような問題はあまり起きないと思います。

Undefined を返した場合の一貫性のあるエラー

React 16 以前から、コンポーネントが undefined を返すことは常に間違いでした:

function Button() {
  return; // Error: Nothing was returned from render
}

こうなっている理由の一部は、うっかり undefined を返してしまいがちだから、というものです:

function Button() {
  // We forgot to write return, so this component returns undefined.
  // React surfaces this as an error instead of ignoring it.
  <button />;
}

これまで、クラスおよび関数コンポーネントではこのチェックを行っていましたが、forwardRefmemo コンポーネントではこのような返り値のチェックを行っていませんでした。これは我々のコーディングミスによるものです。

React 17 では、forwardRefmemo によるコンポーネントの振る舞いが通常の関数・クラスコンポーネントの振る舞いと合致するようになります。これらから undefined を返すことはエラーになります。

let Button = forwardRef(() => {
  // We forgot to write return, so this component returns undefined.
  // React 17 surfaces this as an error instead of ignoring it.
  <button />;
});

let Button = memo(() => {
  // We forgot to write return, so this component returns undefined.
  // React 17 surfaces this as an error instead of ignoring it.
  <button />;
});

意図的に何もレンダーしたくないという場合には、null を返すようにしてください。

ネイティブのコンポーネントスタック

ブラウザであなたがエラーをスローすると、ブラウザは JavaScript の関数名とそれらの位置を含んだスタックトレースを表示します。しかし、問題を診断するのに JavaScript のスタックトレースでは不十分で React ツリーの階層構造も同じくらい重要だ、ということがよくあります。Button がエラーをスローしたということだけでなく、その ButtonReact ツリーのどこにあるのかが知りたいでしょう。

この問題を解決するために React 16 で、エラーがあった場合に「コンポーネントスタック」を表示するようにしました。しかし、これはネイティブの JavaScript スタックと比べて劣ったものでした。具体的には、React はソースコード内のどこにその関数が宣言されているのか分からないため、コードをクリックすることができませんでした。また、本番モードではほぼ使いものにならないという問題もありました。minify された JavaScript におけるスタックはソースマップさえあれば自動的に元の関数名に戻せますが、React コンポーネントのスタックにおいては、本番モードでも使えるようにするのかバンドルサイズを小さくするのか選ばなければいけませんでした。

React 17 では、コンポーネントスタックの生成方法が別のメカニズムに変更され、ネイティブ JavaScript スタックと繋ぎ合わせて表示されるようになりました。これにより、本番環境においても完全に名前付きで React のコンポーネントスタックトレースを得られるようになります。

React がこれを実装している方法はいくぶん特殊なものです。現在のところ、ブラウザは関数のスタックフレーム(ソースファイルと位置)を取得する方法を提供していません。このため、React がエラーをキャッチした場合、可能な場合は React は上流にあるコンポーネントのそれぞれから一時エラーをスロー(およびキャッチ)することでコンポーネントスタックを再構成するようになります。これによりクラッシュ時に小さなパフォーマンス低下が起きますが、コンポーネントの型につき 1 度のみです。

興味があれば詳細についてこのプルリクエストで見ることができますが、このメカニズムそのものは、ほぼあなたのコードに影響しません。開発者側から見てこの新機能が意味するのは、コンポーネントスタックをクリック可能になったということ(ネイティブのブラウザのスタックフレームに依存するようになったため)と、本番環境でもそれを普通の JavaScript エラーと同様に読めるようになった、ということです。

これが破壊的変更となっているのは、これを実現するために、React がエラー捕捉後にスタックの上流にある React 関数や React クラスコンストラクタのうちいくつかを再実行する必要があるからです。レンダー関数やクラスコンストラクタは副作用を持つべきではないため(これはサーバレンダリングにおいても重要です)、実際上の問題は起きないはずです。

プライベートなエクスポートの削除

ここで言及すべき最後の破壊的変更は、他のプロジェクトのために公開されていた React の内部構造を一部削除した、ということです。特に、React Native for Web はイベントシステムの内部実装の一部に依存していたのですが、そのような依存は不安定であり、実際によく壊れていました。

React 17 では、このようなプライベートなエクスポートが削除されています。我々の知る限り、React Native for Web がそれを使っていた唯一のプロジェクトであり、既にプライベートなエクスポートに依存しない別の手法にコードの移行を完了しています。

つまり React Native for Web の古いバージョンは React 17 で動作せず、新しいバージョンのみが動作するということです。実際には、React 内部の実装の変化に対応するため React Native for Web は何にせよ新バージョンをリリースする必要があった訳で、大きく話が変わるものではないでしょう。

加えて、ReactTestUtils.SimulateNative ヘルパメソッドも削除しています。これはドキュメントされたこともなく、名前から期待される通りの動作をしたこともなく、イベントシステムに我々が加えた変更によりうまく動作しなくなりました。テストでネイティブのブラウザイベントを発生させる便利な方法が欲しい場合は、代わりに React Testing Library をチェックしてください。

インストール

React 17.0 リリース候補を試してみて、移行作業中に遭遇する問題について issue を報告してください。リリース候補版は安定リリースと比べてバグがある可能性が高いため、本番環境への投入はまだしないでください

React 17 RC を npm でインストールするには以下のようにします:

npm install react@17.0.0-rc.3 react-dom@17.0.0-rc.3

React 17 RC を Yarn でインストールするには以下のようにします:

yarn add react@17.0.0-rc.3 react-dom@17.0.0-rc.3

CDN 経由で React の UMD ビルドも提供しています:

<script crossorigin src="https://unpkg.com/react@17.0.0-rc.3/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17.0.0-rc.3/umd/react-dom.production.min.js"></script>

詳細なインストール手順についてはドキュメントを参照してください。

Changelog

React

React DOM

  • Delegate events to roots instead of document. (@trueadm in #18195 and others)
  • Clean up all effects before running any next effects. (@bvaughn in #17947)
  • Run useEffect cleanup functions asynchronously. (@bvaughn in #17925)
  • Use browser focusin and focusout for onFocus and onBlur. (@trueadm in #19186)
  • Make all Capture events use the browser capture phase. (@trueadm in #19221)
  • Don’t emulate bubbling of the onScroll event. (@gaearon in #19464)
  • Throw if forwardRef or memo component returns undefined. (@gaearon in #19550)
  • Remove event pooling. (@trueadm in #18969)
  • Stop exposing internals that won’t be needed by React Native Web. (@necolas in #18483)
  • Attach all known event listeners when the root mounts. (@gaearon in #19659)
  • Disable console in the second render pass of DEV mode double render. (@sebmarkbage in #18547)
  • Deprecate the undocumented and misleading ReactTestUtils.SimulateNative API. (@gaearon in #13407)
  • Rename private field names used in the internals. (@gaearon in #18377)
  • Don’t call User Timing API in development. (@gaearon in #18417)
  • Disable console during the repeated render in Strict Mode. (@sebmarkbage in #18547)
  • In Strict Mode, double-render components without Hooks too. (@eps1lon in #18430)
  • Allow calling ReactDOM.flushSync during lifecycle methods (but warn). (@sebmarkbage in #18759)
  • Add the code property to the keyboard event objects. (@bl00mber in #18287)
  • Add the disableRemotePlayback property for video elements. (@tombrowndev in #18619)
  • Add the enterKeyHint property for input elements. (@eps1lon in #18634)
  • Warn when no value is provided to <Context.Provider>. (@charlie1404 in #19054)
  • Warn when memo or forwardRef components return undefined. (@bvaughn in #19550)
  • Improve the error message for invalid updates. (@JoviDeCroock in #18316)
  • Exclude forwardRef and memo from stack frames. (@sebmarkbage in #18559)
  • Improve the error message when switching between controlled and uncontrolled inputs. (@vcarl in #17070)
  • Keep onTouchStart, onTouchMove, and onWheel passive. (@gaearon in #19654)
  • Fix setState hanging in development inside a closed iframe. (@gaearon in #19220)
  • Fix rendering bailout for lazy components with defaultProps. (@jddxf in #18539)
  • Fix a false positive warning when dangerouslySetInnerHTML is undefined. (@eps1lon in #18676)
  • Fix Test Utils with non-standard require implementation. (@just-boris in #18632)
  • Fix onBeforeInput reporting an incorrect event.type. (@eps1lon in #19561)
  • Fix event.relatedTarget reported as undefined in Firefox. (@claytercek in #19607)
  • Fix “unspecified error” in IE11. (@hemakshis in #19664)
  • Fix rendering into a shadow root. (@Jack-Works in #15894)
  • Fix movementX/Y polyfill with capture events. (@gaearon in #19672)
  • Use delegation for onSubmit and onReset events. (@gaearon in #19333)
  • Improve memory usage. (@trueadm in #18970)

React DOM Server

  • Make useCallback behavior consistent with useMemo for the server renderer. (@alexmckenley in #18783)
  • Fix state leaking when a function component throws. (@pmaccart in #19212)

React Test Renderer

Concurrent Mode (Experimental)

Is this page useful?このページを編集する