20210120のReactに関する記事は4件です。

【Next.js】CSR,SSG,SSR,ISRがあやふやな人へざっくり解説する

前書き

仕事でNext.jsを書いているのですがSSG SSR ISRらへんの知識があやふやだったので噛み砕いて解説してみました。間違っているところなどあれば、ご指摘していただけるとありがたいです?‍♂️

以下、本題です。

それぞれの基本的な解説

CSR(クライアントサイドレンダリング)

クライアントサイド レンダリング(CSR)は JavaScriptを使用し、直接ブラウザでページをレンダリングすることを意味します。すべてのロジック、データフェッチ、テンプレーティングやルーティングは、サーバーではなくクライアント上で扱われます。

引用元

つまりサーバーではなく、(JavaScriptによって)ブラウザ側でレンダリングする方法です。しかしCSR(クライアントサイドレンダリング)は大きいアプリケーションの場合、クライアントで処理するJavascriptの量も増えますよね。これに伴い、ユーザーにページを表示させるのが遅くなってしまいます。(ユーザーのデバイススペックに依存してしまう)

上記の問題を解決するために、SSR(サーバーサイドレンダリング)が出てきます。

SSR(サーバーサイドレンダリング)

CSR(クライアントサイドレンダリング)のようにロジックやデータフェッチをブラウザで行うのではなく、サーバー側で処理(データフェッチ等を)し、HTMLを構築してクライアント側に返す方式です。この方法ではCSR(クライアントサイドレンダリング)のようにクライアント(ユーザーデバイス)のスペックに依存せず、ハイスペックなサーバーでHTMLを構築することができます。

とはいえリクエストごとにサーバーで処理し、HTMLが構築されるため、ユーザーを待たせる時間が長くなってしまいます。そこで、SSGが出てきます。

SSG(静的サイトジェネレーション)

SSRの問題(CSRもですが)として、ユーザーのリクエストを受けてからHTMLを構築するので、時間がかかってしまう、という課題がありました。これを解決してくれるのがSSG(静的サイトジェネレーション)です。

SSG(静的サイトジェネレーション)はビルド時にHTMLを構築しておきます。この時、外部APIからのデータフェッチも行います。そしてユーザーからリクエストされた時に事前に構築してあるHTMLを表示します。また、アプリケーションサーバーからHTMLを返すのではなく、CDNにキャッシュしておくことでユーザーに対して高速にページを表示することができます。こうすることにより、SSRよりもユーザーに対して高速にページを表示させることができます。

しかし、またもや問題が出てきます。しかも今回は二つ。

一つ目は、ビルド時に大量のデータを取ってくることは現実的ではありません。例えば、アマゾンのように巨大なECサイトの場合、ビルド時に全てのデータを取ってきて、HTMLを構築することは難しいでしょう。

この問題はフォールバックが解決してくれます。

二つ目は、リソースの更新が頻繁な場合は、どうなるでしょうか?例えば、ツイッターみたいに複数人がコンテンツを更新するような場合(更新が激しい場合)は、このままのSSGでは情報を表示させることができません。だってビルド時しかデータフェッチしていないのだから。

この問題を解決するためにISRが使われます。

それぞれ解説していきます。

フォールバック

export async function getStaticPaths() {
  return {
    paths: [
      // 省略
    ],
    //ここ!!
    fallback: true
  }
}

getStaticPaths()関数内のfallbackが確認できるかと思います。この値がfalseの場合、存在しないページにアクセスした時に404ページに飛ばされます。trueにした場合、データフェッチしていない状態のHTMLが返され、その後ブラウザ側でデータフェッチが行われて、HTMLが再構築されます。

同時にサーバー側でも同様にデータフェッチが行われ、HTMLの構築が行われます。次回以降のリクエストでは、サーバー側から完全なHTML(データも含まれた)が返されます。

とはいえ、非完全な状態のHTML(データが含まれていない)でクライアントに送られてしまうので、この部分が欠点ですね。この点も解決できます。

blocking

export async function getStaticPaths() {
  return {
    paths: [
      // 省略
    ],
    //ここ!!
    fallback: 'blocking'
  }
}

fallbackの値にblockingを入れることで、データが取得されていないページにアクセスした時、サーバーから不完全な状態でHTMLが送られるなんてことはなく、データフェッチが行われてからHTMLが構築され、クライアント側にHTMLが送られます。

ISR(Incremental Static Regeneration)

export async function getStaticProps(hoge) {
  return {
    props: { hoge },
    revalidate: 10, // ここを追加
  }
}

getStaticProps()関数のreturn内にrevalidateの値として任意の数字を入れてください。その秒数以降にリクエストがきた時に、サーバー側でデータフェッチを再度行い、HTMLを再構築します。ここでポイントになってくるのが、リクエストしたユーザーにはキャッシュしていたHTMLを返すということです。

一定期間ごとにサーバーサイドレンダリングを行うことで、高速なページ描画を実現しています。こうすることにより、表示されるデータの更新頻度が高くても新しいデータが表示されるようになりました。

とはいえ、ISRは常に最新のものがユーザーに届けられるわけではありません。(最初のリクエスト時にはキャッシュされたHTMLが返されるので。)使い分けとしては下記のようなイメージで良いと思います。

  • リクエスト時に最新の情報でなくても良い:ISR
  • リクエスト時に最新の情報が出てほしい:SSRSWR

以上です?‍♂️

参考記事

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

reactでデジタルクロックを作る

書いた理由

class componentで実装していたデジタルクロックをfunctional conponentで実装し直したらバグってしまって、直すのに時間がかかったので自分用に書き置きしておく。

※筆者はReact初心者なので、この記事の内容は間違ってる可能性があります

解決法

functional componentはstateを更新すると、コンポーネント関数自身が再実行(?)されてしまうので、stateを変更する処理を一回文書くだけで現在時刻の更新が繰り返し行える。

実装

class component

See the Pen class component digital clock by fazerog02 (@fazerog02) on CodePen.

functional component

See the Pen bGwZMov by fazerog02 (@fazerog02) on CodePen.

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReactRouterをネストさせて共通パーツの再描画を防ぐ

実現したいこと

ReactRouterでページのルーティングを実装しているが、
ルーティングする度にサイトのヘッダー/フッター/サイドナビなど共通パーツまで
各ページで描画していて全然SPAではなかった

共通パーツはルーティングが走っても再描画されないようにしたい

環境

React v17.0.1

やったこと

はじめる前

index.js

ReactDOM.render(
  <React.StrictMode>
    <BrowserRouter>
      <Switch>
        <Route exact path="/aaa/page1">
          <Page1 />
        </Route>
        <Route exact path="/bbb/page2">
          <Page2 />
        </Route>
        <Route exact path="/bbb/page3">
          <Page3 />
        </Route>
      </Switch>
    </BrowserRouter>
  </React.StrictMode>,
  document.getElementById("root")
);

pages.tsx

export function Page1() {
  return (
    <>
     <Header />
     <p>Page1</p>
     <Footer />
    </>
  );
}


export function Page2() {
  return (
    <>
     <Header />
     <SideNav />
     <p>Page2</p>
     <Footer />
    </>
  );
}


export function Page3() {
  return (
    <>
     <Header />
     <SideNav />
     <p>Page3</p>
     <Footer />
    </>
  );
}

上記のようなページを3つ持つサイトで以下の特徴を持ちます

  • どのページもHeaderとFooterのコンポーネントを持つ
  • /bbb/* のページ(Page2, Page3)は更に<SideNav>コンポーネントも共通で持つ

これらの共通的な3つのコンポーネントを再描画しないようなRouterを作っていきたい

形だけでもRouterをネストさせる

まず、形の上でもRouterをネストさせてみる
参考にするのは ReactRouterの公式Doc

exampleのコードを見ると<Switch>で分岐させた各Routeのchildren要素の中で更にを使ってRouteの分岐をさせている
真似して書いてみると以下のように

index.js

ReactDOM.render(
  <React.StrictMode>
    <BrowserRouter>
      <Switch>
        <Route exact path="/aaa/page1">
          <Page1 />
        </Route>

        {/*今回変更した部分*/}
        <Route exact path="/bbb">
          <Switch>      
            <Route exact path="/bbb/page2">
              <Page2 />
            </Route>
            <Route exact path="/bbb/page3">
              <Page3 />
            </Route>
          </Switch>
        </Route>

      </Switch>
    </BrowserRouter>
  </React.StrictMode>,
  document.getElementById("root")
);

まず /bbb でRouteを分岐させることでPage2とPage3をまとめられた
まだ、共通化はしていないので動作としては変わっていない

共通部分をRouterの外に出す

Page2とPage3を比べると差分は<p>要素のみに見える(残りは共通)
そこでRouterを使って描画する部分を<p>要素のみにする

pages.tsx

export function Page1() {
  return (
    <>
     <Header />
     <p>Page1</p>
     <Footer />
    </>
  );
}


export function Page2() {
  return (
    <>
     <p>Page2</p>
    </>
  );
}


export function Page3() {
  return (
    <>
     <p>Page3</p>
    </>
  );
}

そして共通化させたい<Header>/<Footer>/<SideNav>コンポーネントは
Page2とPage3を分岐させるRouterの上位に配置する

index.js

ReactDOM.render(
  <React.StrictMode>
    <BrowserRouter>
      <Switch>
        <Route exact path="/aaa/page1">
          <Page1 />
        </Route>

        {/*今回変更した部分*/}
        <Route exact path="/bbb">
          <Header />
          <SideNav />
          <Switch>      
            <Route exact path="/bbb/page2">
              <Page2 />
            </Route>
            <Route exact path="/bbb/page3">
              <Page3 />
            </Route>
          </Switch>
          <Footer />
        </Route>

      </Switch>
    </BrowserRouter>
  </React.StrictMode>,
  document.getElementById("root")
);

これで /bbb 以下のページ遷移(Page2<>Page3)では共通化した
<Header>/<Footer>/<SideNav>コンポーネントは再描画されなくなった

最後に<Header>/<Footer>/コンポーネントはPage1でも共通なので更に共通化させる

index.js

ReactDOM.render(
  <React.StrictMode>
    <BrowserRouter>
      <Header />
      <Switch>
        <Route exact path="/aaa/page1">
          <Page1 />
        </Route>
        <Route exact path="/bbb">
          <SideNav />
          <Switch>      
            <Route exact path="/bbb/page2">
              <Page2 />
            </Route>
            <Route exact path="/bbb/page3">
              <Page3 />
            </Route>
          </Switch>
        </Route>
      </Switch>
      <Footer />
    </BrowserRouter>
  </React.StrictMode>,
  document.getElementById("root")
);

完成

当初やりたかったことは実装できた
ほとんど公式チュートリアルにあった通りだが、実際に使ってみた方が理解しやすいなぁ

個人的にはRouterのchildren要素はスッキリさせたいので/bbb以下のchildren要素をコンポーネント化するかと思う

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初めて使うReact入門

はじめに

こんにちわ。
この記事は、開発経験は手続き型を少々なプロダクトオーナーがりあクト! TypeScriptで始めるつらくないReact開発 第3版を読んで、四苦八苦しながらReactを使えるようになろうと苦戦しながら勉強したものを備忘録したものです。
1回では終わらないので何回か続くと思ので、お付き合いください。

因みに、この記事では端折っていますが、本の中では何故そのような技術を使っているのかという背景を踏まえて語られており、非常に理解しやすい内容です。ただ、動かすだけとは段違いです。
少しでもこの記事を見て面白そうだと思った方は、是非とも一度読んでみることをお勧めします。

普段は、TESTRUCTUREというテスト設計を支援するツールを作っていたりします。

実施環境

  • Windows 10 pro
  • core i7-8850H
  • Visual Studio Code
  • Ubuntu 20.04 LTS

環境準備

まずは、環境を準備します。

Node.jsのインストール

nodenvのインストール

本ではMacなのでこちらも合わせて参考にインストール。

まずは、anyenvをインストールします。

bash
git clone https://github.com/riywo/anyenv ~/.anyenv
echo 'export PATH="$HOME/.anyenv/bin:$PATH"' >> ~/.bash_profile
echo 'eval "$(anyenv init -)"' >> ~/.bash_profile
anyenv install --init
exec $SHELL -l

anyenvのインストールが完了したら、nodenvをインストールします。

bash
anyenv install nodenv
exec $SHELL -l

続いて、プラグインをインストールします。

bash
mkdir -p $(anyenv root)/plugins
git clone https://github.com/znz/anyenv-update.git $(anyenv root)/plugins/anyenv-update
mkdir -p "$(nodenv root)"/plugins
git clone https://github.com/nodenv/nodenv-default-packages.git "$(nodenv root)/plugins/nodenv-default-packages"
touch $(nodenv root)/default-packages
/.anyenv/envs/nodenv/plugins/nodenv-default-packages

ここにあるdefault-packagesのファイルの中身を以下にする。

yarn
typescript
ts-node
typesync

Node.jsのインストール

これでようやく準備が整ったので、Node.jsをインストールします。

bash
nodenv install -l
nodenv install 14.4.0
nodenv global 14.4.0

以上で、インストールが完了です。

Reactを動かしてみる

まず、お試しで、Hallo Worldしてみる。

bash
npx create-react-app hello-world --tempate typescript

インストールが完了したら、次のコマンドで動作を見る。

bash
cd hello-world
yarn start

http://localhost:3000 にブラウザでアクセスすると動いてるのが確認できます。
動いているのが見れると少し感動。
「Edit src/App.js and save to reload.」と書いてある通り、
App.jsを編集すると編集した内容を見ることができます。

Tutorial: Intro to Reactをやってみる

Hollo worldにある「Learn React」を押すとチュートリアルに案内されるので折角なのでやってみる。
Tutorial: Intro to React

最終的には、以下のファイルを書き換えて保存して、更新してみると、〇×ゲームが完成しています。

src/index.css
src/index.js

ここまでは、書いてある通りにやっていくので、途中は端折ります。

追加課題

そして、追加課題があるのでいくつかやってみようと思います。
追加課題を仕様変更だととらえると、各課題という名の仕様変更は、Reactのコンポーネントベースな考え方が、機能単位で分割されていて、それぞれが独立性が高く、ソースコードをいじりやすい気がしました。

各移動の場所を移動履歴リストに(col, row)形式で表示します。

どこのボタンを押したかを判別するために押した場所を記憶するstateを追加します。
また、移動履歴リストは、gameクラスのrenderに書かれているので、次ように書き換えます。
正方形は次のように配置されているので、

0 1 2
3 4 5
6 7 8

col:3で割った余りを記載します。
row:3で割った商を記載します。

index.js
class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [
        {
          squares: Array(9).fill(null),
        }
      ],
      onClickHistory: [
        {
          onClickNum: 0,
        }
      ],
      stepNumber: 0,
      xIsNext: true
    };
  }

  handleClick(i) {
    const history = this.state.history.slice(0, this.state.stepNumber + 1);
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    const onClickHistory = this.state.onClickHistory.slice(0, this.state.stepNumber + 1);
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? "X" : "O";
    this.setState({
      history: history.concat([
        {
          squares: squares
        }
      ]),
      onClickHistory: onClickHistory.concat([
        {
          onClickNum: i
        }
      ]),
      stepNumber: history.length,
      xIsNext: !this.state.xIsNext
    });
  }

  ...

  render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    const winner = calculateWinner(current.squares);
    const historyNum = this.state.onClickHistory;

    const moves = history.map((step, move) => {
      const col = historyNum[move].onClickNum % 3;
      const row = (historyNum[move].onClickNum - col) / 3;
      const desc = move ?
        'Go to move #' + move + ' (' + col + ',' + row + ')' :
        'Go to game start';
      return (
        <li key={move}>
          <button onClick={() => this.jumpTo(move)}>{desc}</button>
        </li>
      );
    });

  ...
}

このように各ボタンに列と行が表示されていれば成功です。
reactadd1.png

移動リストで現在選択されている項目を太字にします。

太字のスタイルを追加します。

index.css
button.button1 {
  font-weight: bold;
}

gameクラスのrenderで返す値を、現在選択されたものと一致している場合は、太字のボタンを使用するように指定します。

index.js
  render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    const winner = calculateWinner(current.squares);
    const historyNum = this.state.onClickHistory;

    const moves = history.map((step, move) => {
      const col = historyNum[move].onClickNum % 3;
      const row = (historyNum[move].onClickNum - col) / 3;
      const desc = move ?
        'Go to move #' + move + ' (' + col + ',' + row + ')' :
        'Go to game start';
      return move === this.state.stepNumber ?
        (
          <li key={move}>
            <button class="button1" onClick={() => this.jumpTo(move)}>{desc}</button>
          </li>
        ) : (
          <li key={move}>
            <button onClick={() => this.jumpTo(move)}>{desc}</button>
          </li>
        );
    });

太字になりました。
reactadd2.png

誰も勝てない場合は、引き分けの結果についてのメッセージを表示します。

勝利ステータスの処理のところで、勝者が決まっておらず、既に9回動いていた場合は、drawを表示してあげるようにします。

index.js
    let status;
    if (winner) {
      status = "Winner: " + winner;
    } else if (this.state.stepNumber === 9) {
      status = "draw";
    } else {
      status = "Next player: " + (this.state.xIsNext ? "X" : "O");
    }

引き分けになりました。
reactadd3.png

まとめ

ここまでで、最低限のReactの環境構築と、ソースコードがかけるようになりました。
今後は、中身の要素技術や、応用的な使い方について触れたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む