初めに
Next.jsでは公式がReactの基礎というチュートリアルを公開しています。Next.jsを学ぶためにはJavaScript、React、Web開発の概念に関する知識が必要となるため、Next.jsの開発を始める前にReactの基本を改めて復習するとともに記録します。
筆者はNext.jsを学習することを目的としているため、そのほかのライブラリも含めて環境構築しています。
参考までに手順をまとめましたのでよかったらどうぞ。
Reactとは
ウェブブラウザで使用するUIの開発を簡単にするためのJavaScriptライブラリです。
このライブラリにはUIを構築するための関数が用意されていて、開発者が関数を適宜使用することで効率的に開発を進めることができます。
Next.jsとは
Reactの機能を拡張したフレームワークです。サーバーサイドレンダリング(SSR)と静的サイト生成(SSG)とクライアントサイドレンダリング(CSR)をサポートしているため、サイトの表示を高速化することができるなど様々なメリットがあります。
Web開発の基本
UIのレンダリング
ユーザーがウェブサイトにアクセスするとサーバーはブラウザに対してHTMLファイルを返します。
<html>
<body>
<div>
<h1>チーム</h1>
<ul>
<li>田中太郎</li>
<li>山田花子</li>
<li>佐藤一</li>
</ul>
<button>Like (0)</button>
</div>
</body>
</html>
DOMについて
DOMとはDocument Object Modelの略称で、HTMLの要素をツリー構造で表したデータモデルのことです。
DOMメソッドを使うことで選択、追加、削除、更新などでDOMを操作することができます。
例えば上記のようなHTMLファイルが返された場合、DOMは以下のような構成になります。
DOMの操作
では早速DOMを操作してh1
タグを追加してみましょう。
まずは以下のコードでindex.html
ファイルを作成しましょう。
このコードは追加先をターゲットとして設定できるように、div
タグにapp
という一意なIDを付与しています。
また、今回はHTMLファイル内でJavaScript を実行するためにscript
タグを用意しています。
<html>
<body>
<div id="app"></div>
<script type="text/javascript"></script>
</body>
</html>
早速DOMメソッドを使用し、追加先のターゲットを選択しましょう。IDを識別して要素を取得するので使用するメソッドはgetElementById()
を使用します。
<html>
<body>
<div id="app"></div>
<script type="text/javascript">
const app = document.getElementById('app');
</script>
</body>
</html>
引き続きh1
タグの追加処理を実装していきます。
<html>
<body>
<div id="app"></div>
<script type="text/javascript">
// 'app'というIDを持つdivの要素を取得する
const app = document.getElementById('app');
// H1の要素を作成
const header = document.createElement('h1');
// H1要素に新しいテキストのノードを作成する
const text = 'Develop. Preview. Ship.';
const headerContent = document.createTextNode(text);
// テキストのノードをH1要素に追加する
header.appendChild(headerContent);
// H1要素をdivの中に配置する
app.appendChild(header);
</script>
</body>
</html>
作成したHTMLファイルを開いてみましょう。「Develop. Preview. Ship.」という文字列が表示されていれば、DOMメソッドでの追加処理は成功です。
DOM操作の面倒さ
先ほどはDOMの操作によってh1
タグを追加することができました。しかし、JavaScriptを使用してDOMを更新するのはいくつかステップが必要で少し冗長です。そこで「○○を見せたい」と一言で表して、DOMの操作をライブラリに任せることができればどうでしょうか。Reactは人気のある宣言型ライブラリでUIを構築するのに使えます。
React
Reactを使用するためにはunpkg.comという外部サイトから、2種類のReactスクリプトを読み込む必要があります。
名称 | 概要 |
---|---|
react | Reactのコアライブラリ |
react-dom | ReactのDOM操作用ライブラリ |
では早速以前作成したコードのDOM操作処理を、Reactを使用する方法に変更してみましょう。index.html
を以下のように変更していきます。
<html>
<body>
<div id="app"></div>
<!-- React Scripts -->
<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>
// 'app'というIDを持つdivの要素を取得する
const app = document.getElementById('app');
// app要素をターゲットにしてReact Componentsを表示するルートを作成
const root = ReactDOM.createRoot(app);
// ReactのコードをDOMにレンダリングする
root.render(<h1>Develop. Preview. Ship.</h1>);
</script>
</body>
</html>
上記のコードでHTMLファイルを開くと構文エラーが発生しています。
原因はこちらのコード。root.render(<h1>Develop. Preview. Ship.</h1>);
HTMLのscrpit
タグ内ではJavaScriptの構文で書く必要があります。script
タグの中では<h1>...<h1/>
のような構文はないため、エラーになっているのです。
このフォーマットのはJSX
と呼ばれるフォーマットであるため、以下の様にこのスクリプトは「JSXで書いています」ということを明記しておかなければなりません。<script type="text/jsx">
JSXとは
JavaScript XMLの略称で呼ばれる、JavaScriptの拡張構文です。HTMLのような構文でUIを記述できるため、複数のフレームワークに利用されています。
ブラウザはJSXを理解できないため、JSXからJavaScriptに変換する必要があります。
通常は、BabelなどのJavaScriptコンパイラを使用して変換します。
Babelを使用するには
Babelを使用するためには、以下のコードを追加してReactと同様にスクリプトを読み込む必要があります。<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<html>
<body>
<div id="app"></div>
<!-- React Scripts -->
<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>
<!-- Babel Script -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/jsx">
// 'app'というIDを持つdivの要素を取得する
const app = document.getElementById("app");
// app要素をターゲットにする
const root = ReactDOM.createRoot(app);
// ReactのコードをDOMにレンダリングする
root.render(<h1>Develop. Preview. Ship.</h1>);
</script>
</body>
</html>
ブラウザでHTMLファイルを開いて確認すると、ページにはDevelop. Preview. Ship.
と表示され、開発者ツールでは構文エラーがなくなっているはずです。
ここで元のコードと比較してみましょう。
<script type="text/javascript">
const app = document.getElementById('app');
const header = document.createElement('h1');
const text = 'Develop. Preview. Ship.';
const headerContent = document.createTextNode(text);
header.appendChild(headerContent);
app.appendChild(header);
</script>
<script type="text/jsx">
const domNode = document.getElementById("app")
const root = ReactDOM.createRoot(domNode);
root.render(<h1>Develop. Preview. Ship.</h1>);
</script>
6行から3行になって半分もコードを削減することができましたね。
このように面倒な手順をReactが一気に省略してくれます。
Components(コンポーネント)
ReactのUIはComponentという単位で分解することができます。コンポーネントは再利用可能でレゴブロックのようなものです。組み合わせることで大きな構造物を組み立てることができ、UIを更新する場合は特定のコンポーネントを更新する必要があります。
このようなメディアコンポーネントでもさらに細分化すると以下のコンポーネントで構成されています。
この性質により、アプリケーションの他の処理に変更を加えずに追加・更新・削除ができるので、保守性に優れるのがReactコンポーネントのメリットです。
Componentsの作り方
ReactではComponentsをUI要素を返す関数として作成する必要があります。script
タグの中にheaderという関数を作成しましょう。関数の中でもJSXを利用することができます。
<script type="text/jsx">
const app = document.getElementById("app")
function header() {
}
const root = ReactDOM.createRoot(app);
root.render(<h1>Develop. Preview. Ship.</h1>);
</script>
まずは単純にroot.render
で直接入力していたJSXをheader()に移動しましょう。
<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>
ブラウザで確認してみるとエラーが出ていますので、二点ほど修正を加えます。
一つ目はJavaScriptの関数と区別するためにReactの関数はアッパーキャメルケースで関数名を宣言する必要があります。
二つ目はReactコンポーネントはHTMLタグと同じように角括弧で囲んで使用する必要があります。
以下が修正後のコードになります。
function Header() {
return <h1>Develop. Preview. Ship.</h1>;
}
const root = ReactDOM.createRoot(app);
root.render(<Header />);
もう一度ブラウザで確認すると、エラーが消えて正常に表示できたことを確認できます。
コンポーネントのネスト
HTML要素と同じよにReactコンポーネントも入れ子にすることができます。
それではHomePageというコンポーネントを作成してみましょう。
function Header() {
return <h1>Develop. Preview. Ship.</h1>;
}
function HomePage() {
return (
<div>
{/* Headerコンポーネントをネストさせる */}
<Header />
</div>
);
}
const root = ReactDOM.createRoot(app);
root.render(<HomePage />);
コンポーネントツリー
コンポーネントをネストし続けることで以下の様なコンポーネントツリーを形作ることができます。
これは一例ですが、トップレベルのHomePageコンポーネントは、Header、Article、Footerコンポーネントを、 それらの各コンポーネントは順番にそれ自身の子コンポーネントなどを持つことができます。 例えば、Headerコンポーネントには、Logo、Title、Navigationコンポーネントを含めることができます。 このモジュール形式により、アプリ内のさまざまな場所でコンポーネントを再利用することができます。
Props(小道具)
現時点ではHeader
コンポーネントを使用すると、まったく同じ内容が表示されます。
ですが、違うテキストを渡したい場合や、動的に内容はこのままでは利用できません。
通常のHTML要素には属性があり、その属性を使って要素の動作を変更する情報を渡すことができます。例えば、img
要素のsrc
属性を変更すると、表示される画像が変わります。a
タグのhref
属性を変更すると、リンク先が変わります。
Reactでも情報を属性として渡すことができます。これをReactではPropsと呼びます。
PropsはJavaScriptの関数と同様に設計することができ、親コンポーネントから子コンポーネントに渡すことができます。
Propsの使い方
それではHeaderで表示する内容をPropsで変更できるようにしてみましょう。HTML属性を渡すようにHeaderコンポーネントにtitle
というカスタムプロパティを渡すことができます。console.log()
で出力されたオブジェクトのプロパティを見るとtitleという属性を持っていることがわかります。
function HomePage() {
return (
<div>
<Header title="React" />
</div>
);
}
function Header(props) {
console.log(props); // { title: "React" }
return <h1>Develop. Preview. Ship.</h1>;
}
propsはオブジェクトなので分割代入を使用して、関数のパラメータ内でPropsの値を明示的に指定することができます。
function Header({ title }) {
console.log(title);
return <h1>title</h1>;
}
ブラウザで確認確認するとTitleという文字が表示されているのがわかります。カスタムプロパティで<Header title="React" />
と指定したはずなのになぜでしょう。
理由はreturn <h1>title</h1>
で書いているtitle
がプレーンテキストであるためです。この「title
はJavaScript変数です」ということをReactに伝えなければいけません。
JSXで変数を使用する
title
Propを以下の様にコードを変更しましょう。この書き方はJSXの特別な書き方で、{}で囲んだ内側では通常のJavaScript
が記載できます。
function Header({ title }) {
console.log(title);
return <h1>{title}</h1>;
}
他にも以下のような書き方でも表現可能です。
ピリオドを使用したオブジェクトのプロパティ参照(解説)
function Header(props) {
return <h1>{props.title}</h1>;
}
テンプレートリテラル(解説)
function Header({ title }) {
return <h1>{`Cool ${title}`}</h1>;
}
関数の戻り値(解説)
function createTitle(title) {
if (title) {
return title;
} else {
return 'Default title';
}
}
function Header({ title }) {
return <h1>{createTitle(title)}</h1>;
}
三項演算子(解説)
function Header({ title }) {
return <h1>{title ? title : 'Default title'}</h1>;
}
function HomePage() {
return (
<div>
<Header />
</div>
);
}
リストの反復処理
リストとしてデータを表示したいケースはよくあります。同じスタイルで異なる情報を保持するUIの要素を作成するには、配列メソッドを使用してデータを操作しましょう。array.map()
を使用することで、配列に対して繰り返し処理を実行し、異なるデータの要素を複数生成することができます。array.map()
についてはこちら。
function HomePage() {
const names = ['Ada Lovelace', 'Grace Hopper', 'Margaret Hamilton'];
return (
<div>
<Header title="Develop. Preview. Ship." />
<ul>
{names.map((name) => (
<li>{name}</li>
))}
</ul>
</div>
);
}
上記のコードを実行するとReactから「リスト内の各子要素は、一意な「key」Propを持つ必要があります」という警告が出ています。Reactがどの要素を更新すべきか判断するために、表示データだけではなく属性を持つ必要があるからです。
以下の様に表示データをそのまま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 を使う場合、「ボタンを無効」、「ボタンを有効」、「成功メッセージを表示」といったコマンドを書くなど、コードから直接 UI を変更することはありません。コンポーネントのさまざまな視覚状態(「初期状態」、「入力中状態」、「成功状態」)に対して表示したい UI を先に記述し、ユーザ入力に応じて state の変更をトリガーします。
まずはstate
とevent handlers
を使用して、どのようにインタラクティビティを追加するかを確認しましょう。
以下のコードではHomePage
コンポーネントの中に「いいね!」ボタンであるbutton
要素を作成しています。
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>
<button>Like</button>
</div>
);
}
イベントのリッスン
ボタンがクリックされた際に何か実行するためにはonClick
イベントを使用します。
function HomePage() {
// ...
return (
<div>
{/* ... */}
<button onClick={}>Like</button>
</div>
);
}
イベントの処理
イベントがトリガーされるたびに「処理」する関数を定義することができます。
今回は return文の前にhandleClick()という関数を作成しトリガーされるたびに呼び出されるように設定しましょう。
function HomePage() {
// ...
function handleClick() {
console.log('increment like count');
}
return (
<div>
{/* ... */}
<button onClick={handleClick}>Like</button>
</div>
);
}
ブラウザで実行して開発者ツールを起動し、ボタンを押すとログが出力されることを確認できます。
Stateとhooks
Reactには、Hookと呼ばれる一連の関数があります。 Hookを使えば、Stateのような追加ロジックをコンポーネントに追加できます。Stateとは、UIの中で時間の経過とともに変化する情報のことで、基本的にユーザーとのインタラクションによって変化します。
例えばStateを使用して、ユーザーが「いいね!」ボタンをクリックした回数を保存し、値をインクリメントすることができます。 Stateを管理するためのReact Hookの名前はuseState()
です。では早速useState()
をプロジェクトに追加しましょう。 useState()
は配列を返すので、以下の様に配列の分割代入を使って、コンポーネント内部で配列の値にアクセスしたり使用したりすることができます
function HomePage() {
// ...
const [likes, setLikes] = React.useState(0);
function handleClick() {
setLikes(likes + 1);
}
return (
<div>
{/* ... */}
<button onClick={handleClick}>Likes ({likes})</button>
</div>
);
}
まずはconst [likes, setLikes] = React.useState(0);
をで指定している配列の第一項目には状態の名称を付けることができます。ここでは「いいね!」の数ということでlikes
と名付けています。
配列の第二項目は値を更新する関数を指定する必要があります。宣言する関数名は自由に指定できますが、setLikes
の様にset+変数名
を付けるのが一般的です。React.useState(0)
の(0)
ではStateの初期値を設定することができます。今回はボタンが押されるまでは0を設定しています。setLikes(likes + 1)
では値更新後の値を渡しています。{likes}
を使用することで、更新された値を表示することができます。
ブラウザでボタンをクリックすると「いいね!」の数が増えていくことが確認できます。
Reactのまとめ
ここまでWeb開発の基本からReactの3つの概念であるComponent・Props・Stateとは何かということを実際にコードを書きながら解説してきました。DOMの操作が省略されて楽に開発が進められることを体験できたかと思います。
しかし、拡張性の高いアプリケーションを構築するには、まだまだ作業が必要です。サーバーのコンポーネントやクライアントのコンポーネントのようなフレームワークを必要尾する新しいReact機能もあります。
Next.jsはセットアップや設定の大部分を処理し、Reactアプリケーションの構築を手助けする追加機能を持っています。Reactのチュートリアルでも軽く触れているので、ここでも紹介します。
Next.jsへの移行
Next.jsのインストール
Next.jsを使用する場合react
スクリプトやreact-dom
スクリプトをunpkg.comから読み込む必要はありません。npmなどのパッケージマネージャーからインストールすることでNext.jsを使用することができます。
今回はnpmを使用してインストールするので{}
と書かれたpackage.json
を作成します。
続いて、ターミナルからnpm install react@latest react-dom@latest next@latest
を入力して各ライブラリをインストールします。
コマンドを入力するとpackage.json
に以下のような依存関係が追加され、package-lock.json
が生成されます。
{
"dependencies": {
"next": "^14.0.3",
"react": "^18.3.1",
"react-dom": "^18.3.1"
}
}
HTMLファイルからJSファイルへの変更
HTMLで定義していたその他のタグはNext.jsが肩代わりしてくれています。
ファイル名をindex.html
からindex.js
に変更し、以下のコードに変更してみましょう。
import { useState } from 'react';
function Header({ title }) {
return <h1>{title ? title : 'Default title'}</h1>;
}
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>
);
}
トップページの作成
Next.jsはファイルシステムのルーティングを使用しているため、コードを使用してアプリケーションのルートを定義しています。以下の手順でトップページを作成しましょう。
app
フォルダを作成し、index.js
を中に入れましょうindex.js
ファイルの名前をpage.js
に変更しましょう
このファイルがアプリケーションのメインページになりますexport default
を<HomePage>
コンポーネントに追加しましょう。
Next.jsにメインコンポーネントでレンダリングするかを区別できるようにします。
import { useState } from 'react';
function Header({ title }) {
return <h1>{title ? title : 'Default title'}</h1>;
}
export default function HomePage() {
// ...
}
開発サーバーの実行
開発サーバーを実行して開発中の変更を確認できるようにしましょう。package.json
ファイルに"next dev"
スクリプトを追加しましょう。
{
"scripts": {
"dev": "next dev"
},
"dependencies": {
"next": "^14.0.3",
"react": "^18.3.1",
"react-dom": "^18.3.1"
}
}
ターミナルでnpm run dev
を実行して先ほど追加したスクリプトを実行してみましょう。
layout.js
というファイルが自動的にappフォルダ内に作成されました。これがアプリケーションのメインレイアウトになります。すべてのページで共有されるUI要素を追加するために使用できます。
また、先ほどの開発サーバーが起動されているので、ブラウザでhttp://localhost:3000/に接続して先ほどの変更を確認してみましょう。
コンパイルに失敗したというビルドエラーが出ていますね。
これはNext.jsがRSCと呼ばれるReactをサーバーでレンダリングできるようにする新機能を使用しているからです。しかしサーバーコンポーネントはuseStateをサポートしていないため、クライアントコンポーネントを使用する必要があります。
サーバーコンポーネントとクライアントコンポーネント
クライアントとサーバーという概念を理解する必要があります。Webアプリケーションでは以下の内容になります。
名称 | 説明 |
---|---|
クライアント | ユーザーのデバイス上のブラウザのこと。アプリケーションのコードに対するリクエストをサーバーに送信する。 |
サーバー | データセンターにあるPCのこと。アプリケーションのコードを保存し、クライアントからのリクエストを受け取り、計算を行い、適切なレスポンスを返す。 |
それぞれ独自の機能と制約があります。例えばレンダリングとデータ取得をサーバーに移すことで、クライアントに送信するデータ量を減らし、パフォーマンスを向上させることができます。ですが、UIをインタラクティブにするにはクライアントでDOMを更新しなければなりません。そのため、サーバーとクライアントでコードが異なるという場合があります。
ネットワークバウンダリー
ネットワークバウンダリーとは異なる環境を分ける概念的な線の名称です。Reactではコンポーネントツリーのどこにネットワークバウンダリーを配置するかを選択できます
この例ではサーバー上でデータを取得してユーザーの投稿をレンダリングし、クライアント上で投稿のインタラクティブな「いいね!」ボタンをレンダリングすることができます。同様にサーバー上でレンダリングされ、ページ間で共有されるNavコンポーネントを作成することができますが、リンクのアクティブな状態を表示したい場合はクライアント上でリンクのリストをレンダリングすることができます。
裏ではコンポーネントは2つのモジュールグラフに分割される。サーバーモジュールグラフにはサーバー上でレンダリングされるすべてのサーバーコンポーネントが、クライアントモジュールグラフにはクライアントコンポーネントが含まれます。サーバーコンポーネントがレンダリングされると、RSCペイロードと呼ばれる特殊なデータ形式がクライアントに送信されます。RSCペイロードには、サーバーコンポーネントのレンダリング結果と、クライアントコンポーネントがレンダリングされるPlaceholderや対象のJavaScript ファイルへの参照が含まれます。
Reactはこれらの情報を使用してサーバーとクライアントのコンポーネントを統合しDOMを更新しているのです。
クライアントコンポーネントへの移動
Next.jsはデフォルトでサーバーコンポーネントを使用します。アプリケーションのパフォーマンスを向上させるために追加の手順を踏む必要がないためです。
先ほどのエラーを確認すると、サーバーコンポーネント内でuseState
を使用していることを警告していました。「いいね!」ボタンはインタラクティブなボタンであるため、クライアントコンポーネントに移動することで修正することができます。
まずはapp
フォルダ内に「いいね!」ボタンコンポーネントをエクスポートするlike-button.js
を作成しましょう。<button>
要素とhandleClick()
関数そしてlikes
状態をpage.js
からLikeButton
コンポーネントに移動します。
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>;
}
そしてuse client
ディレクティブをファイルの先頭に追加しましょう。
これを追加することでこのコンポーネントはクライアントでレンダリングするようにReactに指示することができます。
'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>;
}
そしてpage.js
ファイルに戻り、LikeButton
コンポーネントをページにインポートします。
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>
);
}
変更が完了したら、ブラウザでhttp://localhost:3000/に接続してエラーが解消されたことを確認しましょう。
すでに開いている場合は変更を保存した際にページが自動で更新されたことに気が付いたと思います。これはNext.jsにあらかじめ設定されているFast Refreshと呼ばれる機能です。
まとめ
いかがでしょうか。初めはHTMLとJavaScript を使用してDOMを直接操作していました。Reactに移行してコードが減り書きやすくなったことを体験できたと思います。さらに最後ではNext.jsについても軽く触れました。Next.jsはRSCを使用してパフォーマンスを向上させるメジャーなフレームワークです。Reactについて学習した後はNext.jsについても学習することをお勧めいたします。