- 投稿日:2021-01-20T17:16:11+09:00
【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
- リクエスト時に最新の情報が出てほしい:
SSR
かSWR
以上です?♂️
参考記事
- 投稿日:2021-01-20T16:38:48+09:00
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.
- 投稿日:2021-01-20T16:15:32+09:00
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の公式Docexampleのコードを見ると<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要素をコンポーネント化するかと思う
- 投稿日:2021-01-20T10:52:36+09:00
初めて使う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をインストールします。
bashgit 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 -lanyenvのインストールが完了したら、nodenvをインストールします。
bashanyenv install nodenv exec $SHELL -l続いて、プラグインをインストールします。
bashmkdir -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 typesyncNode.jsのインストール
これでようやく準備が整ったので、Node.jsをインストールします。
bashnodenv install -l nodenv install 14.4.0 nodenv global 14.4.0以上で、インストールが完了です。
Reactを動かしてみる
まず、お試しで、Hallo Worldしてみる。
bashnpx create-react-app hello-world --tempate typescript
インストールが完了したら、次のコマンドで動作を見る。
bashcd 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最終的には、以下のファイルを書き換えて保存して、更新してみると、〇×ゲームが完成しています。
ここまでは、書いてある通りにやっていくので、途中は端折ります。
追加課題
そして、追加課題があるのでいくつかやってみようと思います。
追加課題を仕様変更だととらえると、各課題という名の仕様変更は、Reactのコンポーネントベースな考え方が、機能単位で分割されていて、それぞれが独立性が高く、ソースコードをいじりやすい気がしました。各移動の場所を移動履歴リストに(col, row)形式で表示します。
どこのボタンを押したかを判別するために押した場所を記憶するstateを追加します。
また、移動履歴リストは、gameクラスのrenderに書かれているので、次ように書き換えます。
正方形は次のように配置されているので、
0 1 2 3 4 5 6 7 8 col:3で割った余りを記載します。
row:3で割った商を記載します。index.jsclass 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> ); }); ... }移動リストで現在選択されている項目を太字にします。
太字のスタイルを追加します。
index.cssbutton.button1 { font-weight: bold; }gameクラスのrenderで返す値を、現在選択されたものと一致している場合は、太字のボタンを使用するように指定します。
index.jsrender() { 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> ); });誰も勝てない場合は、引き分けの結果についてのメッセージを表示します。
勝利ステータスの処理のところで、勝者が決まっておらず、既に9回動いていた場合は、drawを表示してあげるようにします。
index.jslet status; if (winner) { status = "Winner: " + winner; } else if (this.state.stepNumber === 9) { status = "draw"; } else { status = "Next player: " + (this.state.xIsNext ? "X" : "O"); }まとめ
ここまでで、最低限のReactの環境構築と、ソースコードがかけるようになりました。
今後は、中身の要素技術や、応用的な使い方について触れたいと思います。