Next.js をざっくりキャッチアップ
Next.js
Next.js はフルスタックウェブアプリを開発するための React フレームワークである。
Next.js は、React に必要な設定や構成を抽象化・自動化し、開発者がアプリ開発に集中できるようにしてくれる。
React 基礎
成果物
React と Next.js
Next.js は、フルスタックウェブアプリを作成するための構成要素を提供する React フレームワークである。
ウェブアプリの構成要素
モダンなアプリを作る上で考慮する必要のあることは、以下のようなものがある。
- User Interface (UI): ユーザーがどのようにアプリとやりとりするか
- Routing: アプリの異なるパーツ間をどのようにナビゲートするか
- Data Fetching: どこにデータが存在し、どのように取得するか
- Rendering: 静的もしくは動的なコンテンツをいつ・どこでレンダリングするか
- Integrations: サードパーティサービスをどのように統合するか
- Infrastructure: アプリをどこでデプロイ、保存、実行するか
- Performance: どのようにアプリを最適化するか
- Scalability: チーム、データ、通信の増加に、どのようにアプリを適応させるか
- Developer Experience: アプリを開発・管理する上での開発者の体験をよくできるか
React とは?
React とは、インタラクティブな UI を構築するための JavaScript ライブラリである。つまり、ユーザーに見えていて、ユーザーが操作する画面上のコンポーネントを作るために使える便利な関数 (API) を提供している。
React の成功は、UI にフォーカスしており、他の部分については自由度を持たせた拡張性に起因している。結果として、Next.js を含む様々なサードパーティツールのエコシステムが構築されている。これは同時に、React アプリをゼロから構築するのは大変である。
Next.js とは
Next.js とは、React フレームワークで、ウェブアプリを作成する上での構成要素を提供する。
Next.js は、React に必要なツールや設定を行い、追加の機能や最適化などを提供する。つまり、UI の構築に React を使い、段階的に追加の Next.js の機能を使って、アプリでよく必要になる要件 (Routing, Data Fetching, Caching など) を解決す流。
UI のレンダリング
ユーザーが Web ページを訪れた時に、サーバーはブラウザへ HTML ファイルを返す。ブラウザは HTML を読み込み、DOM (Document Object Model) を構築する。
DOM とは
DOM とは、HTML の要素を表現する方法である。コードと UI の橋渡しを行い、ツリー構造を持っている。
DOM methods や JavaScript を使って、ユーザー起因のイベントをモニタリングしたり、DOM を操作したりすることができる。
JavaScript で UI の更新
以下のコードでは、DOM methods を使って、'app' の ID がついた <div>
要素の中に、'Develop. Preview. Ship.' というテキストが入った <h1>
要素を追加している。
<html>
<body>
<div id="app"></div>
<script type="text/javascript">
// Select the div element with 'app' id.
const app = docuemtn.getElementById('app');
// Create a new H1 element.
const header = document.createElement('h1');
// Create a new text node for the H1 element.
const text = 'Develop. Preview. Ship.';
const headerContent = document.createTextNode(text);
// Append the text to the H1 element.
header.appendChild(headerContent);
// Place the H1 element inside the div.
app.appendChild(header);
</script>
</body>
</html>
HTML vs. DOM
HTML ファイルは先ほどの通りだが、DOM では <h1>
が追加された状態のものをいう。
<html>
<body>
<div id="app">
<h1>Develop. Preview. Ship.</h1>
</div>
<script type="text/javascript">...</script>
</body>
</html>
つまり、HTML は最初に渡されたファイルであり、DOM は JavaScript などによって動的に変化する現時点でのページの内容を示すものである。
JavaScript を使って DOM を更新するのは、非常に強力である一方で冗長である。つまり、<h1>
要素を1つ追加するためのだけに、何行かのコードを書かないといけない。
Imperative vs. Declarative Programming
- Imperative Programming: UI をどのように更新するかを記述する (例: シェフにピザを作るための step-by-step のレシピを渡す)
- Declerative Programming: UI がどのように表示されて欲しいかを記述する (例: どのようにピザが作られているかを気にせずに、ピザを注文する)
React は、UI を構築する上で使える Declerative なライブラリである。
React: Declerative な UI ライブラリ
React では、開発者が UI に起きて欲しいことを要求し、React がそれを実現するための方法を見つけ出して実行してくれる。
Getting Started with React
React を使うには、2つの React スクリプトを外部サイト (unpkg.com) から読み込んでくる必要がある。
-
react
: React のコアライブラリ -
react-dom
: React を DOM と一緒に使うための DOM 固有のメソッドを提供するライブラリ
React は、JSX と呼ばれる形式で記述する。これは HTML の記法を JavaScript 内で使えるようにする拡張である。しかし、ブラウザは JSX をそのまま解釈することができないので、Babel などの JavaScript コンパイラを使って、JSX のコードを JavaScript に変換する必要がある。
<html>
<body>
<div id="app"></div>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/jsx">
const app = document.getElementById('app');
const root = ReactDOM.createRoot(app);
root.render(<h1>Develop. Preview. Ship.</h1>)
</script>
</body>
</html>
Components を使った UI の構築
React のコアコンセプト
React アプリを構築する上で理解しておくべき2つのコアコンセプトがある。
- Components
- Props
- State
Components
UI は Components と呼ばれるより小さな構成要素に分解することができる。
Components は、自己完結的な再利用可能なコードを構築できる。Components を LEGO ブロックと考え、これらのブロックを組み合わせてより大きな構造を作ることができる。UI の一部を更新する必要があるとき、特定のブロックを更新すれば良い。
このようにモジュールに分割することによって、コードがよりメンテナンスしやすくなる。
Components の作成
Component を作成するには、普通の JavaScript と同じように関数宣言のシンタックスが使え、UI 要素を返すようにすれば良い。気を付けるべきは、(1) 関数の頭文字は大文字である必要がある点と (2) Component を利用する時には通常の HTML タグと同じように <>
で囲う必要がある点である。
<html>
<body>
<div id="app"></div>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/jsx">
const app = document.getElementById('app');
function Header() {
return (<h1>Develop. Preview. Ship.</h1>);
}
const root = ReactDOM.createRoot(app);
root.render(<Header />);
</script>
</body>
</html>
Props を使ってデータを表示する
<Header />
Component を再利用した場合、同じコンテンツが表示される。
function Header() {
return <h1>Develop. Preview. Ship.</h1>;
}
function HomePage() {
return (
<div>
<Header />
<Header />
</div>
);
}
外部ソースからデータを取ってくる必要があり、事前に何を表示したいかがわからなく、テキストをその場に応じて表示したい場合にどうするか。
React では、Properties (props
) を介して、Component を定義する関数にデータを渡すことができる。
function Header(props) {
return <h1>{props.title}</h1>;
}
function Header({ title }) {
return <h1>{title}</h1>
呼び出し側では以下のように書く。
function HomePage() {
return (
<div>
<Header title="React" />
<Header title="A new title" />
</div>
);
}
リストの各要素に対して同じ操作を適用したい場合には、map()
を使用することができる。
なお、React では、DOM 要素が更新されたかを一意に認識するために、key
を渡す必要がある。この例では names
の要素内容自体が一意なので、そのまま key
に使うことができる。
function HomePage() {
const names = ['Ada Lovelace', 'Grace Hopper', 'Margaret Hamilton'];
return (
<div>
<Header title="Develop. Preview. Ship." />
<ul>
{names.map((name) => (
<li key={name}>{name}</li>
))}
</ul>
</div>
);
State を使ってインタラクティブに
React が、State と Event Handler を使って、インタラクティブな操作を定義することができる。
イベントの監視
以下のように onClick
を使って、ボタン要素のクリックのイベントを認識することができる。
function HomePage() {
// ...
return (
<div>
{/* ... */}
<button onClick={}>Like</button>
</div>
);
}
イベントの処理
以下のようにしてイベントをハンドリングすることができる。
function HomePage() {
// ...
function handleClick() {
console.log("increment like count");
}
return (
<div>
{/* ... */}
<button onClick={handleClick}>Like</button>
</div>
);
}
State と Hooks
React に Hooks と呼ばれる関数群があり、Hooks によって Component に State のようなロジックを追加で加えることができる。State とは、ユーザーのインタラクションなどによって時間と共に変化する UI に関する情報のことである。
例えば、"Like" ボタンがクリックされた回数を保存し、クリックの度に数を1増やしたいとする。この場合、以下のように行うことができる。React.useState()
関数は、変数とその変数を更新するための関数のペアが返され、引数としてその変数の初期値を与える。作られた変数 likes
を handleClick()
内でインクリメントして setLikes()
でセットしなおすことによって、その likes
の数を画面に反映させることができる。
function HomePage() {
// ...
const [likes, setLikes] = React.useState(0);
function handleClick() {
setLikes(likes + 1);
}
return (
<div>
{/* ... */}
<button onClick={handleClick}>Like ({likes})</button>
</div>
);
}
React から Next.js へ
とりあえず、これまでに構築したものは以下の通り。
<html>
<body>
<div id="app"></div>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/jsx">
const app = document.getElementById("app")
function Header({ title }) {
return <h1>{title ? title : "Default title"}</h1>
}
function HomePage() {
const names = ["Ada Lovelace", "Grace Hopper", "Margaret Hamilton"]
const [likes, setLikes] = React.useState(0)
function handleClick() {
setLikes(likes + 1)
}
return (
<div>
<Header title="Develop. Preview. Ship." />
<ul>
{names.map((name) => (
<li key={name}>{name}</li>
))}
</ul>
<button onClick={handleClick}>Like ({likes})</button>
</div>
)
}
const root = ReactDOM.createRoot(app);
root.render(<HomePage />);
</script>
</body>
</html>
React は UI を構築するのに優れている一方で、完全に機能するスケーラブルなアプリを構築するのには手間がかかる。Nest.js は、React アプリを構築するのを助ける様々な追加の機能があり、設定や構成の多くを処理してくれる。
Next.js のインストール
プロジェクトで Next.js を使う場合には、もう unpkg.com から react
と react-dom
を手動でロードする必要はない。代わりに、npm
やその他のパッケージマネージャーを使って、これらのパッケージをインストールすることが可能である。
まず初めに、index.html
が置いてある同じディレクトリに、追加で package.json
ファイルを空のオブジェクトの状態で作成する。
{}
次にターミナルから以下を実行する。
npm install react@latest react-dom@latest next@latest
インストールが完了すると、package.json
が以下のように変わっている。
{
"dependencies": {
"next": "^14.0.4",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
同様に package-lock.json
も構築されており、各パッケージの正確なバージョンの情報が列挙されている。
最初のページの作成
その上で、app/
ディレクトリを作成し、その中に page.jsx
ファイルを作成し、index.html
から必要のない要素を取り除いた以下の内容を保存する。
import { useState } from 'react';
function Header({ title }) {
return <h1>{title ? title : "Default title"}</h1>
}
export default function HomePage() {
const names = ["Ada Lovelace", "Grace Hopper", "Margaret Hamilton"]
const [likes, setLikes] = useState(0)
function handleClick() {
setLikes(likes + 1)
}
return (
<div>
<Header title="Develop. Preview. Ship." />
<ul>
{names.map((name) => (
<li key={name}>{name}</li>
))}
</ul>
<button onClick={handleClick}>Like ({likes})</button>
</div>
)
}
その上で package.json
に以下のコマンドを追加する。
{
"scripts": {
"dev": "next dev"
},
"dependencies": {
"next": "^14.0.4",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
その上で、そのコマンドを実行する。
$ npm run dev
> dev
> next dev
▲ Next.js 14.0.4
- Local: http://localhost:3000
✓ Ready in 2.4s
対象の URL にアクセスすると、以下のエラーが表示される。
./app/page.jsx
ReactServerComponentsError:
You're importing a component that needs useState. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.
Learn more: https://nextjs.org/docs/getting-started/react-essentials
╭─[/Volumes/workplace/test/my-first-nextjp-app/app/page.jsx:1:1]
1 │ import { useState } from 'react';
· ────────
2 │
3 │ function Header({ title }) {
4 │ return <h1>{title ? title : "Default title"}</h1>
╰────
Maybe one of these should be marked as a client entry with "use client":
./app/page.jsx
エラーにある通り、useState
は Client Component のみで動作するものである。デフォルトの設定は Server Component である。
Server / Client Components
Server と Client
- Client とは、ユーザーのデバイス上で動作するブラウザで、Server へリクエストを送信する。Server からのレスポンスを UI に反映させる。
- Server とは、データセンター上のコンピュータのことで、アプリのコードを実行し、Client からのリクエストを処理し、レスポンスを返す。
ネットワーク境界 (Network Boundary)
Network Boundary とは、異なる環境を分離する概念的な線のことである。
React では、Component Tree 上でどこに Network Boundary を配置するか選択することができる。例えば、Server 上でデータの取得とユーザーのポストのレンダリングを行い、Client 側で各ポストごとのインタラクティブな LikeButton
をレンダリングするなどを決定できる。
その背景では、Component は2つのモジュールグラフに分けられる。Server Module Graph はサーバー上でレンダリングされる全ての Server Component を含んでおり、Client Module Graph は全ての Client Component を含んでいる。
Server Component がレンダリングされた後は、React Server Component (RSC) Payload というフォーマットでクラインとにデータが送信される。RSC には、Server Component のレンダリングした結果と、Client Component がレンダリングされるはずの場所のプレースホルダーが含まれる。
Client Components を使う
Next.js はデフォルトで Server Component を使っている。
先ほどの章で見たエラーは、インタラクティブな "Like" ボタンを Client Component に移動させることで解消することができる。
まずは app/
ディレクトリ配下に新しいファイル like-button.jsx
を作成し、LikeButton
コンポーネントを Client Component としてエクスポートする。具体的には 'use client';
ディレクティブをファイルの先頭に置く。
'use client';
import { useState } from "react";
export default function LikeButton() {
const [likes, setLikes] = useState(0);
function handleClick() {
setLikes(likes + 1);
}
return <button onClick={handleClick}>Like ({likes})</button>
}
それに合わせて、app/page.jsx
を変更する。
import LikeButton from "./like-button"
function Header({ title }) {
return <h1>{title ? title : "Default title"}</h1>
}
export default function HomePage() {
const names = ["Ada Lovelace", "Grace Hopper", "Margaret Hamilton"]
return (
<div>
<Header title="Develop. Preview. Ship." />
<ul>
{names.map((name) => (
<li key={name}>{name}</li>
))}
</ul>
<LikeButton />
</div>
)
}