Open16

React/Next.jsのメモ

shunshun

Refオブジェクト

  • コンポーネントがマウントされたときからアンマウントされるときまで存在し続ける、書き換え可能なオブジェクト
  • クラスコンポーネントでしか生成できない(createRefを使う)
  • 関数コンポーネントで使うときはuseRefを使う

ref

  • DOMにアクセスできる
    • ref.current にDOMの参照がセットされるため、DOMの関数などを呼び出せる
  • 子コンポーネントのインスタンスにアクセスできる
    • ref.currentでデータを読み出したり書き換えたりできる

【React hooks】意外と知らないrefの使い方 - Qiita

shunshun

使い方

参考:https://zenn.dev/yubachiri/articles/7a11545d8d7ae6

親コンポーネントにて

  • useRefでrefを生成
  • 子コンポーネント(この場合はinput)にrefを渡す
  • ref.current.focusやref.current.valueなどを使って子コンポーネントのDOMを操作する

というイメージ

import React, { useRef } from "react";
    
const App = () => {
  const ref = useRef();

  return (
    <div>
      <button
        onClick={() => {
          ref.current.focus();
        }}
      >
        focus
      </button>

      <button
        onClick={() => {
          ref.current.blur();
        }}
      >
        blur
      </button>

      <button
        onClick={() => {
          ref.current.value = "filled";
        }}
      >
        fill
      </button>

      <input type="text" ref={ref} />
    </div>
  );
};

export default App;
shunshun
  • useRef は毎回のレンダーで同じ ref オブジェクトを返すため、親の変更によって子が再レンダーされても、以前の値を保持できる
  • 上記の性質から、描画に関係ないデータを保持するのに使える
  • useStateやuseReducerは更新のたびに再描画が発生する

useRefのより実践的なサンプル

import React, { useRef, useState } from "react";

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

const UPLOAD_DELAY = 5000;

export const ImageUploader = () => {
  // スタイル定義でhidden、つまり見えなくしたInput要素にアクセスするためのref
  const inputImageRef = useRef<HTMLInputElement | null>(null);
  // 選択されたファイルデータを保持するref
  const fileRef = useRef<File | null>(null);
  const [message, setMessage] = useState<string | null>("");

  // 「画像を選択する」というテキストがクリックされた時のコールバック
  const onClickText = () => {
    if (inputImageRef.current !== null) {
      // inputのDOMにアクセスし、クリックイベントを発火する
      inputImageRef.current.click();
    }
  };

  // ファイルが選択された後に呼ばれるコールバック
  const onChangeImage = (e: React.ChangeEvent<HTMLInputElement>) => {
    const files = e.target.files;
    if (files !== null && files.length > 0) {
      // fileRef.currentに値を保存する。もしfileRef.currentが変わっても再描画されない
      fileRef.current = files[0];
    }
  };

  // アップロードボタンがクリックされたときに呼ばれるコールバック
  const onClickUpload = async () => {
    if (fileRef.current !== null) {
      await sleep(UPLOAD_DELAY); // 疑似的に一定時間待つ、通常はAPIを呼び出す
      setMessage(`${fileRef.current.name} のアップロードが正常に完了しました`);
    } else {
      setMessage("ファイルが選択されていません");
    }
  };

  return (
    <div>
      <p style={{ textDecoration: "underline" }} onClick={onClickText}>
        画像を選択する
      </p>
      <input
        ref={inputImageRef}
        type="file"
        accept="image/*"
        onChange={onChangeImage}
        style={{ visibility: "hidden" }}
      />
      <br />
      <button onClick={onClickUpload}>アップロードする</button>
      {message !== null && <p>{message}</p>}
    </div>
  );
};

上記の流れ

  1. 「画像を選択する」が押される

  2. onClickTextが発火

  3. input要素がclickされる

  4. inputに定義されたonChangeイベントが発火

  5. onChangeImageが発火

  6. fileRefにファイルが保存される

  7. 「アップロードする」が押される

  8. onClickUploadが発火

  9. fileRefを参照して処理を実行する

shunshun

forwardRef

refを定義したコンポーネント外でそのrefを使いたい時にforwardRefを使う。

さっきの例を書き換えると↓
(一部ボタンを減らした)

import React, { useRef } from "react";
    
const App = () => {
  const ref = useRef();

  return (
    <div>
      <button
        onClick={() => {
          ref.current.focus();
        }}
      >
        focus
      </button>

      <Input ref={ref}/>

    </div>
  );
};

const Input = forwardRef((props, ref) => {
  return (
    <div>
      <input type="text" ref={ref} />
     </div>
    )
});

export default App;
  • refを子コンポーネントに渡す
  • 子コンポーネントはforwardRedで引数にrefを指定する
  • 子コンポーネントのinputタグにrefを渡す
shunshun

Reactがブラウザに表示されるまでの流れ

  1. public/index.html が読み込まれる
  2. ブラウザがJSのコードを取得し、実行する
  3. src/index.tsxを読み込む
  4. renderの中のコンポーネントを描画していく
shunshun
  • Reactで使われているJSXのコードはブラウザでは解釈できない

  • webpackによって、JavaScriptのコードに変換される

  • 変換されたコードをブラウザが読み込む

  • JavaScriptからブラウザの内容を書き換えるときにDOMにアクセスする

  • Reactでは仮想DOM(メモリ上に保存されたDOM)を利用する

    • まず仮想DOMを書き換える
    • 前回の仮想DOMとの違いを抽出
    • その違いだけ実際のDOMを更新する
  • こうすることで、高速な描画を実現している

→ 結局仮想であっても一旦DOMを構築するのであれば、描画速度は同じなんじゃないのか…??

shunshun

Reactのコンポーネントは次のタイミングで再描画される

  • propsや内部状態が更新されたとき
  • コンポーネント内で参照しているContextの値が更新されたとき
  • 親コンポーネントが再描画されたとき

3つ目の「親コンポーネントの再描画」によって、それ以下のコンポーネントは全て再描画される
その再描画の伝播を止めたい場合は、メモ化を利用する。

→ そもそもReactとしてそういう仕組みにできないもの…?
→ 親が変わっても子に影響にないなら何もしないで欲しいけど、DOMの性質とかで難しい??

shunshun

Nextのはじめ方

  • create-next-app を利用して環境を構築

    npx create-next-app@latest --ts next-sample
    cd next-sample
    npm run dev
    

    ブラウザに Welcome to Next.js! と表示されれば OK。

shunshun

Nextのレンダリング

  • ページごとにレンダリング手法を切り替えられる
  • レンダリング手法は以下4つ
    • SSG: Static Site Generation
    • CSR: Client Side Rendering
    • SSR: Server Side Rendering
    • ISR: Incremental Static Regeneration

SSG

  • 仕組み
    • ビルド時にAPIなどからデータを取得し、ページを描画し、静的ファイルを生成する
    • ページにアクセスがあったら、上記で生成した静的ファイルをブラウザは描画する
    • 初期描画後は普通のReactアプリと同様に、APIなどからデータを取得し描画を動的に変更する
  • 特徴
    • 初期描画が高速
    • ビルド時にAPIを稼働させるため、初期描画時には古いデータの場合がある
    • そのためリアルタイム性が求められるようなページには適さない
    • ビルド後に内容が変更されない、もしくは初期描画以降にデータを表示するようなページに適している

CSR

  • 仕組み
    • ビルド時にAPIは稼働させず、ページを描画して静的ファイルを生成する
    • ページにアクセスがあったら、上記で生成した静的ファイルを描画し、APIなどを実行する
  • 特徴
    • 必要なデータは後から取得するため、SEOにはあまり有効ではない
    • リアルタイム性が重要なページに適している

SSR

  • 仕組み
    • ページへのアクセスがある度にサーバでページを描画しクライアントへ渡す
    • アクセスごとにデータを取得するため、常に最新のデータを描画できる
  • 特徴
    • 常に最新のデータを表示させたい場合に適している
    • 他の手法に比べるとパフォーマンスが劣る

ISR

  • 仕組み
    • SSGと同じように事前にAPIを含めたページを生成し、有効期限を設定する
    • ページにアクセスがあると、有効期限を確認し、切れていなければ事前にレンダリングしたデータをクライアントに渡す
    • 有効期限が切れていた場合は、再度ページを生成し、結果をクライアントに渡す
  • 特徴
    • SSGとSSRの中間の特徴を持つ
shunshun

どうやってレンダリング手法を決められるか

  • 各ページの関数や返す値によって決まる
    • 例えばデータ取得に使う関数(Nextが用意している)によってレンダリング手法を決めることができる
      • SSG: getStaticProps : ビルド時にデータ取得
      • SSR: GetSErverSideProps : ユーザーのリクエスト時にサーバにてデータ取得
      • ISR: revalidateを返すgetStaticProps : ビルド時にデータ取得
      • CSR: 上記以外の関数 : ユーザーのリクエスト時にブラウザにてデータ取得
  • 各ページがどのタイプかはビルド時の結果で確認できる
    • create-next-appで作った環境で npm run build すると以下のように表示される
    Route (pages)                              Size     First Load JS
    ┌ ○ /                                      5.34 kB        83.3 kB
    ├   └ css/ae0e3e027412e072.css             707 B
    ├   /_app                                  0 B              78 kB
    ├ ○ /404                                   182 B          78.2 kB
    └ λ /api/hello                             0 B              78 kB
    + First Load JS shared by all              78.3 kB
      ├ chunks/framework-09a2284fdc01dc36.js   45.5 kB
      ├ chunks/main-017a64f48d901a37.js        31.2 kB
      ├ chunks/pages/_app-fcb935ebbac35914.js  497 B
      ├ chunks/webpack-cc9c69bc14c8e1bc.js     750 B
      └ css/ab44ce7add5c3d11.css               247 B
    
    λ  (Server)  server-side renders at runtime (uses getInitialProps or getServerSideProps)(Static)  automatically rendered as static HTML (uses no initial props)
    
    • λ (Server)がSSR、○ (Static)がSSGを表している
shunshun

SSG

  • getStaticPropsという関数を作り、その型としてSSGさせたいデータを指定する

  • 関数内でSSGさせたいデータを生成するための処理を書く

  • 生成したデータはreturnすることで、他のコンポーネントから利用できる

  • そのpropsを使いたいコンポーネントは通常通り利用するだけ

    // SSGを実装するための関数 getStaticProps を定義
    // getStaticPropsはexportとasyncは必須の定義
    // 引数に与えられるcontextにはビルド時に利用できるデータ(パスパラメータやlocaleなど)が含まれている
    export const getStaticProps: GetStaticProps<SSGProps> = async (context) => {
      const timestamp = new Date().toLocaleString();
      const message = `${timestamp} にgetStaticPropsが実行されました`;
      console.log(message);
      return {
        // ここで返したpropsを元にページコンポーネントが描画される
        props: {
          message,
        },
      };
    };
    
  • pagesは以下の1tsxファイルが1つのパスになっているため、ユーザーごとのページがこのままだと作れない

  • その場合は、Nextに備わっている動的ルーティング機能を使う

    • 複数のページをパスパラメータを使って1ファイルで生成できる
    • getStaticPropsの前に呼ばれるgetStaticPathsを使う
    • ファイル名を[id].tsxのように作成し、[id]をgetStaticPathsで定義する
    • getStaticPropsにてcontextを使って[id]を利用し、ページを生成する
shunshun

SSR

  • getServerSidePropsという関数を作り、SSRさせたい処理を書く
  • あとはSSGと同じ仕組み
  • contextに関してはSSGのものに加え、reqresといったHTTPリクエスト関連の情報などを参照できる
shunshun

ISR

  • 基本はSSGと同じ

  • getStaticPropsのreturnでrevalidateパラメタを設定することで有効期限を設定できる

    export const getStaticProps: GetStaticProps<ISRProps> = async (context) => {
      const timestamp = new Date().toLocaleString();
      const message = `${timestamp} にgetStaticPropsが実行されました`;
      console.log(message);
      return {
        props: {
          message,
        },
        revalidate: 5,
      };
    };
    
shunshun

onClickへの渡し方の違い

以下の2つの違い

  1. onClick={() => handleDelete(item)}
  2. onClick={handleDelete(item)}

1. onClick={() => handleDelete(item)}

  • この記述方法では、onClickイベントがトリガーされたときに、アロー関数 () => handleDelete(item) が呼び出される。アロー関数はその場で定義され、実際のイベントが発生した際に handleDelete(item) を実行する。
  • この方法はイベントハンドラーに引数を渡す必要がある場合(この例では item)に便利。
  • ただし、アロー関数を使用するため、毎回新しい関数が生成されることになり、これがパフォーマンス上の懸念点となるケースもある(特に大量の要素があるリストや頻繁に更新されるコンポーネント内で)。

2. onClick={handleDelete(item)}

  • この記述方法では、コンポーネントがレンダリングされる時点で handleDelete(item) が実行され、その結果(戻り値)が onClick イベントハンドラーとして設定される。
  • 通常、onClickにはイベントハンドラーとして関数を渡すことが期待されているが、このケースでは handleDelete(item) がイベントハンドラーとして直接呼び出され、その戻り値がイベントハンドラーとして使われることになる。
  • この記述が意図した動作をするためには、handleDelete 関数が別の関数を戻り値として返す必要があり、それが実際のイベントハンドラーとなる。もし handleDelete が関数を返さない場合、これはエラーになるか、意図しない動作を引き起こす可能性がある。
  • TypeScriptを利用している場合、handleDeleteが関数を返さない記述をすると、事前に型エラーとして気づくことができる。
shunshun

LinkとuseRouterを使い分ける

  • Next.jsでページ遷移を実装する際には、Link コンポーネントと useRouter フックの2種類の方法がある
  • どちらもソフトナビゲーション(クライアントサイドナビゲーション)なので、ページ間の遷移が発生するときにブラウザが完全にページをリロードするのではなく、ページの一部分のみが更新される
  • 主に静的なリンクやクライアントサイドのナビゲーションに使う
  • メニューとして複数のリンク先を一覧に並べる時などにシンプルに実装できる
import Link from 'next/link';

function HomePage() {
  return (
    <div>
      <Link href="/about">
        <a>About Us</a>
      </Link>
    </div>
  );
}

2. useRouter フック

  • useRouter フックは、プログラム的なページ遷移や、条件に応じた動的なルーティングに適している
  • 例えば、フォームの送信後や特定のイベントが発生した後にページ遷移を行いたい場合など
  • useRouter を使用すると、pushreplace などのメソッドを通じて、より詳細な制御が可能になる
import { useRouter } from 'next/router';

function MyComponent() {
  const router = useRouter();

  function handleClick() {
    router.push('/about');
  }

  return (
    <button onClick={handleClick}>Go to About Page</button>
  );
}

どう使い分けるか

  • シンプルなリンクで遷移するだけの場合は Link コンポーネントを、より複雑なロジックや条件に基づく遷移が必要な場合は useRouter フックを使う
  • 以下の使い分けが良さそう
    • Link はユーザーが画面を遷移したいと思ってボタンをクリックするときに使う
    • useRouter はユーザーが何か操作をした時に結果としてプログラムによる遷移が必要なときに使う