Closed8

[React18]今後のフロントエンド開発で必須知識となるReact v18の機能を丁寧に理解する メモ 

mena1mena1

React17とReact18の違い

注目すべきはindex.tsx

  • 17
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
,document.getElementById('root'));

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

  • 18
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

アプリケーションのルートを作成する方法が変わっている
新しいルートを作成する方法を使うことでreact18の各機能を使用することが可能になる

react18の状態で17のルートを使うとどうなるか

正常に起動できるが、以下のwarningが表示される

React17の状態で動き、React18の機能は使用できないから注意してねと言われている

mena1mena1

StrictMode

まずは17と18で挙動を見てみよう
App.tsxにconsole.logを追加する

App.tsx
import React from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
+  console.log('appレンダリング')
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

17の挙動

更新するとレンダリングが1回されていることがわかる

18の挙動

更新するとレンダリングが2回されていることがわかる

どうして2回レンダリングされるようになったのか?

ドキュメントには以下が記載されている

将来的に、React が state を保ったままで UI の一部分を追加・削除できるような機能を導入したいと考えています。例えば、ユーザがタブを切り替えて画面を離れてから戻ってきた場合に、React が以前の画面をすぐに表示できるようにしたいのです。これを可能にするため、React は同じ state を使用してツリーをアンマウント・再マウントします。

この機能により、React の標準状態でのパフォーマンスが向上しますが、コンポーネントは副作用が何度も登録されたり破棄されたりすることに対して耐性を持つことが必要になります。ほとんどの副作用は何の変更もなく動作しますが、一部の副作用は一度しか登録・破棄されないものと想定しています。

この問題に気付きやすくするために、React 18 は strict モードに新しい開発時専用のチェックを導入します。この新しいチェックは、コンポーネントが初めてマウントされるたびに、すべてのコンポーネントを自動的にアンマウント・再マウントし、かつ 2 回目のマウントで以前の state を復元します。

参照:React 18 アップグレードガイド

具体的には、タブ遷移みたいに画面からは一回消えるけど画面に戻った時、以前開いていた画面を復活させたい場合にその時の状態をstateを復元することで高速にアプリが使える仕組みになっている

2回レンダリングされることの注意点

2回レンダリングされることの注意としてはuseEffectが挙げられる
従来の動きでは空配列の場合、最初の1回のみのレンダリングだが、React18の場合2回レンダリングされる

useEffect(()=>{
    console.log('useEffect')
  },[])
mena1mena1

Automatic Batching

そもそもバッチ処理とは?

複数set関数が実行された際、その都度レンダリングが行われるとパフォーマンスが悪くなる

  • ex 3回set関数が更新されたら3回レンダリングされる

なので、1回のレンダリング中に複数回のstate更新リクエストを受けた場合はそれらを一括で処理する
これがバッチ処理になる (参照:コンポーネントのstate)

React17までのバッチ処理を確認しよう

イベントハンドラの挙動

挙動を確認するためにsrc配下にcomponentsフォルダを作成し、AutoBatchEventHandlerを作成する

AutoBatchEventHandler.tsx
+ export const AutoBatchEventHandler = ()=>{
+    console.log('AutoBatchEventHandler')
+    const [state1,setState1] = useState<number>(0);
+    const [state2,setState2] = useState<number>(0);

+    const onClickUpdateButton= () =>{
+       console.log(state1)
+       setState1((state1)=>state1+1)
+       console.log(state1)
+       setState2((state2)=>state2+1)
    }
+    return(
+       <div>
+            <p>Automatic Batch確認用(イベントハンドラ)</p>
+            <button onClick={onClickUpdateButton}>State更新</button>
+            <p>State1: {state1}</p>
+            <p>State1: {state2}</p>
+        </div>
+    )
+ }

App.tsxを以下のようにする

App.tsx
import './App.css';
import { AutoBatchEventHandler } from './components/AutoBatchEventHandler';

function App() {
  return (
    <div className="App">
+      <AutoBatchEventHandler/>
    </div>
  );
}

export default App;

挙動は以下の通りになる

  1. setStateの前後のconsole.logの値は変わっていないように見えている
  2. onClickUpdateButtonの処理が終わったら、一度だけ再レンダリングが起きている

イベントハンドラ以外の挙動

componentsフォルダにAutoBatchOtherを作成する

AutoBatchOther.tsx
+ import { useState } from "react";

+ type Todo = {
+  userId: number;
+  id: number;
+  title: string;
+  completed: boolean;
+ };
+ export const AutoBatchOther = () => {
+  console.log('AutoBatchOther')
+  const [todos, setTodos] = useState<Todo[] | null>(null);
+  const [isFinishApi, setIsFinishApi] = useState<boolean>(false);

+  const onClickExecuteApi = () => {
+    fetch("https://jsonplaceholder.typicode.com/todos")
+      .then((res) => res.json())
+      .then((data) => {
+        setTodos(data);
+        setIsFinishApi(true);
+      });
+ };
+  return (
+    <div>
+      <p>Automatic Batching確認用(その他)</p>
+      <button onClick={onClickExecuteApi}>API実行!</button>
+      <p>isFinishAPi: {isFinishApi ? "true" : "false"}</p>
+      {todos?.map((todo) => (
+        <p key={todo.id}>{todo.title}</p>
+      ))}
+    </div>
+  );
+ };

App.tsxを以下のようにする

App.tsx
import "./App.css";
- import { AutoBatchEventHandler } from './components/AutoBatchEventHandler';
+ import { AutoBatchOther } from "./components/AutoBatchOther";
function App() {
  return (
    <div className="App">
-      <AutoBatchEventHandler/>
+      <AutoBatchOther />
    </div>
  );
}

export default App;

挙動は以下の通りになる

  1. AutoBatchOtherが2回表示されている

React18でのバッチ処理を確認しよう!

イベントハンドラ内での挙動はReact17以前と同様なので、イベントハンドラ以外の挙動を見てみよう
コードの内容は先ほどと同様のものを使用する

挙動は以下の通りになる


イベントハンドラ以外の処理でもset関数が複数ある場合レンダリングがまとめて行われている

flushSync

flushSyncとは?

  • バッチ処理をしないでください!と明示的に指定できるも

挙動は以下の通りになる

  • 2回レンダリングされていることが確認できる
  • 意図的にパフォーマンスを下げているので使う機会はあまりないかも?

まとめ

  • バッチ処理とは1回のレンダリング中に複数回のstate更新リクエストを受けた場合はそれらを一括で処理することである
  • 18ではイベントハンドラ以外でset関数を複数呼んでも一括で処理してくれる
mena1mena1

Transition

本章では以下のアプリを用いて解説する(作成手順は割愛)

挙動は以下の通りになる

  • A,B,Cをクリックすると担当が割り振られているA,B,Cのタスクがソートされる
  • リセットをクリックするとすべてのタスクが表示される

Transitionとは?

緊急性の高い更新と高くない更新を区別するためのもの

  • 緊急性の高い更新とはタイプ、クリック、プレスといったユーザー操作を直接反映させるもの

先ほどのアプリを例にTransitionを使用するとどのような課題が解決できるのでしょうか?

Transitionが解決する課題

先ほどのアプリは正しく挙動していたと思いますが、仮にPCの処理性能が低かったらどのような挙動になるでしょう?

ボタンを押してから数秒後に一覧が表示されている

  • 一覧表示するまでに時間はかかるのは処理性能上仕方ない

これを解決するのがstartTransiton

該当箇所にstartTransition関数を追加する

Transition.tsx
...
const onClickAssignee = (assignee: string) => {
    setSelectedAssignee(assignee);
    //緊急性が高くない更新の関数を入れる
   startTransition(() => {
      setTaskList(filteringAssignee(assignee));
    });
  };
...

挙動は以下の通りになる

優先度をつけることによりボタンを「押した時」のステートの反映が即座に行われる
時間がかかるソートの処理は時間差で更新されている

さらなる課題

  • startTransitionを使うことでユーザー体験が良くなることがわかったが、一覧をソートする処理に関して、この処理がいつ終わるのかがわかりにくい
  • 「今ソートしてますよ!」というインタラクションをつければさらにユーザーにわかりやすくなる。

useTransition

以下をTransition.tsxに追加する

Transition.tsx
+ import {useTransition} from "react"
...
//isPendingはstartTransitionで更新している関数が反映されているかどうかを見る
// true:更新中 ,false:通常時
+ const [isPending,startTransition] = useTransition()
...
//先ほどの関数は変更なし
const onClickAssignee = (assignee: string) => {
    setSelectedAssignee(assignee);
    //緊急性が高くない更新の関数を入れる
   startTransition(() => {
      setTaskList(filteringAssignee(assignee));
    });
  };
...
 {taskList.map((task) => (
        <div
          key={task.id}
          style={{
            width: "300px",
            margin: "auto",
            background: "lavender",
+          opacity: isPending ? 0.5 : 1,
          }}
        >
          <p>タイトル:{task.title}</p>
          <p>担当: {task.assignee}</p>
        </div>
      ))}

挙動は以下の通りになる

ユーザー体験がさらに良くなったことがわかる

詳細:useTransition

startTransitionとuseDeferredValue

useDeferredValueとは

useTransitionと機能はほぼ変わらず、緊急性の高い処理が先に反映され緊急性の低い処理がその後に反映されるものであるが、使われる用途が違う

違い

例えば以下のように、先ほどのtaskListを表示する処理をTaskListというcomponentで切り分けたとする

Transition.tsx
...

- {taskList.map((task) => (
-       <div
-         key={task.id}
-         style={{
-           width: "300px",
-           margin: "auto",
-           background: "lavender",
-          opacity: isPending ? 0.5 : 1,
-         }}
-       >
-         <p>タイトル:{task.title}</p>
-         <p>担当: {task.assignee}</p>
-       </div>
-     ))}

...

TaskList.tsx
+ import { Task } from "./Transition";

+ type Props = {
+   taskList: Task[];
+ };

+ export const TaskList = ({ taskList }: Props) => {
+   return (
+    <>
+       {taskList.map((task) => (
+         <div
+           key={task.id}
+           style={{ width: "300px", margin: "auto", background: "lavender" }}
+         >
+           <p>タイトル:{task.title}</p>
+           <p>担当: {task.assignee}</p>
+         </div>
+       ))}
+     </>
+   );
+ };

startTransitionではset関数を囲む必要があるので、切り分けた場合は実装させるのが難しい
そのような時にuseDerredValueを用いる

以下をTasklist.tsxに追加する

TaskList.tsx
import { memo, useDeferredValue } from "react";
import { Task } from "./Transition";

type Props = {
  taskList: Task[];
};

export const TaskList = memo(({ taskList }: Props) => {
+ const deferrdTaskList = useDeferredValue(taskList);
  return (
    <>
      {deferrdTaskList.map((task) => (
        <div
          key={task.id}
          style={{ width: "300px", margin: "auto", background: "lavender" }}
        >
          <p>タイトル:{task.title}</p>
          <p>担当: {task.assignee}</p>
        </div>
      ))}
    </>
  );
});

  • set関数の結果変わってくるstate(緊急性が高くない値)を入れる
  • 緊急性の高い更新(ボタンクリック)終了時に子コンポーネントを再レンダリングを防ぐのにmemo関数を使用 (E参照:useDeferredValue)

挙動は以下の通りになる

  • componentを切り分けていてもtransitionが実装できているのがわかる

まとめ

  • Transitionとは緊急性の高い更新と高くない更新を区別するためのもの
  • 緊急性が高い操作として、主に直接反映させるユーザー操作である
  • Transitionを実装することでユーザー体験が向上する!
mena1mena1

Suspense part1 (実装について)

React18以前のSuspense

React.lazyと一緒に使われる

機能

  • ファイル分割を行える (lazy)
  • ファイルが読み込まれたときに Suspenseでloading処理を行う (Suspense)

詳細:React v16.6.0: lazy, memo and contextType

この機能で解決できること

アプリ立ち上げ時にすべてのファイルが読み込まれるが、数が多ければ多いほど読み込み時に時間がかかってしまう

これを解決するために用いる

以下が実装例

import React, {lazy, Suspense} from 'react';
const OtherComponent = lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <OtherComponent />
    </Suspense>
  );
}

この設定をすることで以下の挙動が実現可能になる

  1. ページごとにファイルが分割される。
  2. あるページにアクセスする。
  3. ページコンポーネントを含むファイルが非同期で取得される。
  4. 取得している間、fallback に指定したコンポーネントが表示される。

React18でのSuspense

Suspenseを学ぶための準備

Suspenseを学ぶための準備として、以下のアプリを実装する(実装については割愛)
※React Queryを使用

挙動は以下の通りになる

データの一覧を取得中は「ローディング中」と表示され、取得出来次第表示される

こちらをSuspenseで実装するとどうなるでしょうか?

Suspenseでの実装

以下のように書き換えよう

index.tsx
...
export const ReactQuery = () => {
  //取得するデータの返り値の型を設定
  const {  isError, data } = useQuery<Album[]>(
    ["albums"],
    fetchAlbums
  );
   if (isError) return <p>エラーです</p>;
-   if (isLoading) return <p>ローディング中</p>;
  return (
    <div>
      <p>ReactQuery</p>
      {data?.map((album) => (
        <p key={album.id}>{album.title}</p>
      ))}
    </div>
  );
};

App.tsx
...
function App() {
  return (
    <div className="App">
+    <Suspense fallback={<p>ローディング中</p>}>
        <ReactQuery />
+    </Suspense>
    </div>
  );
}

...

Suspenseでfallback等を使用するにはSupenseに対応した仕組みがライブラリ側で実装されている必要がある(ReactQueryは対応されている)
以下にSuspenseを使用するためのOptionを記述する

index.tsx
...
const query = new QueryClient({
+ defaultOptions: {
+   queries: {
+     suspense: true,
+   },
+ },
});
...

挙動は先ほどと同じ

ErrorBoundary

ErrorBoundaryを用いてエラー時の処理を行えるようにするもの
実際に実装を行い、挙動を確認してみよう!

//以下をインストール
yarn add react-error-boundary

以下のように書き換えよう

App.tsx
...
function App() {
  return (
    <div className="App">
+    <ErrorBoundary fallback={<p>エラーです</p>}>
        <Suspense fallback={<p>ローディング中</p>}>
          <ReactQuery />
        </Suspense>
+    </ErrorBoundary>
    </div>
  );
}
...

###挙動は以下の通りになる(エラーを確認するためエンドポイントはわざと間違ったものにする)

複数のSuspenseを組み合わせる

準備として以下のアプリを作成する (作成手順は割愛)

挙動は以下の通りになる

データ取得中はローディング中と表示され、取得が完了したらアルバムとTODO、サイドバーが表示される

原因

なぜこの挙動になってしまうのでしょう?
Susepenseを利用しているApp .tsxを見てみよう

App.tsx

...

function App() {
  return (
    <div className="App">
      <ErrorBoundary fallback={<p>エラーです</p>}>
        <Suspense fallback={<p>ローディング中</p>}>
          <ReactQuery />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

export default App;

SusepnseはReactQueryコンポーネントを囲っている
こうするとSuspense配下でどこかしらでデータ取得が発生すると、fallbackが発生する仕組みになるので、ReactQueryを囲むとデータ取得に関係ないサイドバーまでローディング中となってしまう

改善

ではどうすれば改善するだろうか?

ReactQuery.tsxにSuspenseを追加する

ReactQuery.tsx
...

export const ReactQuery = () => {
  //取得するデータの返り値の型を設定
  return (
    <div style={{ display: "flex", padding: "16px" }}>
      <Sidebar />
+     <ErrorBoundary fallback={<p>Listエラーです</p>}>
+       <Suspense fallback={<p>Listローディンング中</p>}>
           <div style={{ flexGrow: 1 }}>
            <AlbumList />
           <TodoList />
          </div>
+      </Suspense>
+     </ErrorBoundary>
   </div>

...
  );
};

挙動は以下の通りになる

さらに適切に分けるには

例えば、TODOのデータ取得に5秒かかるとすると、今のままではアルバムの取得はすでに終わっているのでTODOに引っ張られて、TODO取得後に表示されてしまう

なので、AlbumListとTodoList個別にSuspenseを追加する
そうすることで取得が完了したコンポーネントから表示される

まとめ

Suspenseを使うことで

  • API実行時のローディング状態の記述がより宣言的になる
  • Suspenseを適切に分割することでUXの向上を狙える
mena1mena1

Suspense Part2(本質)

Suspenseはローディングを表示させるためだめなのか?

本質を理解するためには

  • SSR
  • Streaming HTML
  • Hydrationの理解が必要

CSRとSSR

CSRとは

画面表示の流れ

  1. ユーザーが何かしらのアプリケーションに訪れる(URLをwebサーバーに問い合わせ)
  2. 何かしらのページが表示される(サーバーが必要なJS/CSS/HTMLを返却)

データ取得の流れ

  1. データ取得が必要な画面に遷移した(バックエンドサーバーにデータのリクエスト)
  2. (dbからマッチしたデータをレスポンスする)
  3. 画面に表示される(取得したデータをもとにレンダリングされて表示される)

ユーザー体験

画面が表示されてからデータのリクエスト->レスポンス->レンダリングを実行するので、その間ユーザーにはローディングアイコン等が表示される

SSRとは

画面表示の流れ +データ取得の流れ

  1. ユーザーが何かしらのアプリケーションに訪れる(URLをwebサーバーに問い合わせ)
  2. (webサーバー側でデータの取得が必要な画面等を認識してバックエンドサーバーにデータをリクエスト)
  3. (データが必要な画面に対象のデータを取得する)
  4. (dbからマッチしたデータをwebサーバーにレスポンスする)
  5. (webサーバー上でユーザーに表示するHTMLの画面を構築する)
  6. 何かしらのページが表示される(構築されたHTMLが表示される)

ユーザー体験

真っ白な画面や遷移前の画面が表示され続ける(サーバー側でデータ取得やHTMLの構築を実行中)
SSR対象のページを表示するにはデータ取得、HTMLの構築がサーバー側で必要なので全てが完了するまで何も表示ができない

これまでのSSR

メリット

  • 最初にすべてのJSを読み込む必要がなくなるので初回起動が早くなる
  • クライアント側のPCスペックに依存しない
  • SEOに有利
  • 動的にOGP設定を行える

デメリット

リクエスト毎にサーバー側でデータ取得、HTMLの構築→返却が行われるので画面遷移した際など、コンテンツの表示が遅くなる

->このデメリットを解決してくれるのがSuspense!!!!!!!

Streaming HTML

SSR 改善の余地

  • 理想としてはSSRであってもデータ取得中はフォールバック(ローディング)コンテンツを表示したい
  • 同じ仮面でもデータの取得が不要な箇所やデータ取得やレンダリングに時間がかかる箇所が存在する
  • 任意の範囲毎にSSRができればいいのに...

ここで出てくるのがSuspense

具体的にはデータ取得がない要素はすぐにSSRでコンテンツを返却
データ取得が必要な箇所はまずフォールバックコンテンツをSSRで返却

データ取得とHTMLの構築が出来次第、Suspense単位でHTML要素を返却
SSRなのに特定の範囲毎にHTMLの変更が行われる
→これがStreaming HTML

Selective Hydration

Hydrationとは?

  • サーバー側で生成されたHTMLにJSの各ロジックを接続していくこと

Selective Hydrationとは?

  • 特定のハイドレーション処理を一時中断、’別の箇所のハイドレーション処理を優先的に進める機能
このスクラップは2022/10/18にクローズされました