【第12回】React入門(2)

React+Tailwind CSSで簡単なポートフォリオサイトを作ってみましょう。

Homeブログ一覧【第12回】React入門(2)

はじめに

前回はReact超基本的な機能や使い方について学びました。今回は、より発展的なHookの使い方やSPAについて深堀りしていきます。

また、最後にTailwind CSSを紹介・導入し、簡単なポートフォリオサイトを作成してみましょう。

ReactにおけるSPAについて

突然ですが、質問です。

前回の講義で、ページ遷移(/aboutにアクセスなど)を学びましたが、これまでの知識で複数のページをもつサイトを作るにはみなさんならどうしますか?

考えられる回答としては、

  • index.htmlを作って、about.htmlを作って、profile.htmlを作って...<a>タグでリンクを張る...
my-website/
├── index.html
└── pages/
    ├── about.html
    └── profile.html

というようにページ毎にHTMLファイルを作成する方法が思いつくのではないでしょうか?

そうすると、

  • ユーザーがリンクをクリック
  • ブラウザがサーバーに対して、リンク先のHTMLファイルをリクエスト
  • サーバーがリンク先のHTMLファイルを返す
  • ブラウザがレンダリングして画面を表示
サーバーブラウザユーザーサーバーブラウザユーザーリンクをクリックリンク先のHTMLファイルをリクエストリンク先のHTMLファイルを返すレンダリングして画面を表示

という一連の流れが行われます。

ページごとに独立したHTMLファイルがあるためリソースを再取得するために、ページ遷移が遅くなるという問題があります。

では、Reactではどのようにページ遷移を行うのでしょうか?

前回の講義で使用したfirst-react-appプロジェクトを開き以下のコマンドを実行してみましょう。

npm run build

このコマンドは、私たちが編集しているいわゆるデバッグモードのコードを、本番環境で使えるように最適化したコードに変換するコマンドです。

distディレクトリが作成され、その中にindex.htmlassetsディレクトリが作成されていることが確認できると思います。サイトを公開する際はこのdistディレクトリの中身しか使いません。

そのなかのindex.htmlを開いてみましょう。

index.html

前回作ったWebサイトはこのようにたった1つのHTMLファイルのみで構成されており、ページ遷移も実装されています。

では、どのようにして動的なWebページを実現しているのでしょうか?

dist/assetsディレクトリの中に、.jsファイルがあるので見てみましょう。

jsfile

このように、1つの.jsファイルに全てのコードがまとめられています。このファイルがReactの機能を全うしており、たった1つのHTMLファイル、1つの.jsファイルでWebサイトを構築しているのです。

このように、1つのHTMLファイルのみで構成されたWebサイトを**SPA(Single Page Application)**と呼びます。

SPAは、クライアント側の操作によって新たに必要となった差分のデータのみをHTTPリクエストで取得し、ページ全体を再読み込みすることなく、差分のみを更新することができるため、ページ遷移が高速であるという特徴があります。ただし、JSのコードが大きくなるので、初回読み込み時のページ表示は遅くなります。

他にも、SPAではブラウザで行われていたJSの実行とHTML生成をサーバー側で行うSSR(Server Side Rendering)という手法もあり、Next.jsなどのフレームワークで提供されています。

Hooksの紹介

フック(hook)は前回も説明したように、関数コンポーネントでも簡単に「状態」を持たせたり、「動作」を追加できる仕組みです。これを使えば、複雑なクラスを書かなくても、Reactの便利な機能を利用できるようになります。

Reactには、他にも様々なフックが用意されていますが、復習も兼ねて以下のフックについて紹介します。

  • useState:状態を持たせるためのフック
  • useEffect:コンポーネントや値がマウントされたり更新されたりしたときに実行する処理を追加するためのフック
  • useMemo:値をメモリ上に保存するためのフック

useStateの使い方(おさらい)

useStateは、状態を持たせるためのフックです。(前回のおさらいなので説明は省略)

App.jsx
import { useState } from "react";

function App() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
  }
  
  return (
    <div className="App">
      <h1>useState</h1>
      <p>{count}</p>
      <button onClick={handleClick}>+</button>
    </div>
  );
};

export default App;

useEffectの使い方

useEffectは、第1引数にコールバック関数、第2引数に依存する値の配列を受け取ります。値が更新されたりコンポーネントがマウント(初回レンダリング時)、アンマウント(レンダリングの対象外となったとき)されたときに実行する処理を追加するためのフックです。

簡単に言うと、処理の実行を自由に制御できるフックです。

import { useEffect } from "react";

useEffectをインポートしたうえで以下のコードを見てみましょう。

useEffect(() => {
  console.log('Hello, useEffect');
}, []);

第2引数[]の部分が空になっている時は、ページ全体がリロードされたときに実行されます。

実際に試してみよう

App.jsxに以下のコードを追加して、コンソールログで確認してみましょう。

App.jsx
import { useEffect } from 'react';

function App() {
  useEffect(() => {
    console.log('Hello, useEffect');
  },[]);

  return (
    <div className="App">
      <h1>useEffect</h1>
      <p>コンソールログで確認してみましょう</p>
    </div>
  );
};

export default App;

それでは、useStateuseEffectを使ってcountの値が変更されたときにログを出力する処理を書いてみましょう。

App.jsx
import { useState, useEffect } from 'react';

function App() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
  }

  useEffect(() => {
    console.log('ボタンが押されました');
  },[count]);

  return (
    <div className="App">
      <h1>useState, useEffect</h1>
      <p>{count}</p>
      <button onClick={handleClick}>+</button>
    </div>
  );
};

export default App;

countの値が変わったときuseEffectが実行されるようになりました。(ボタンを押したときに処理が実行されているという訳ではないので注意)

このように、何かの値や状態が変わったときに処理が実行されるので、useEffectは副作用と呼ばれることがあります。

useMemoの使い方

useMemoをブラウザのメモリ上に保存するためのフックです。(関数自体をメモリ上に保存するuseCallbackもあります)

まずは、以下のコードを見てみましょう。

App.jsx
import { useState } from "react";

function App() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  const square = () => {
    return count2 * count2;
  }

  return (
    <div className="App">
      <h1>useMemo</h1>
      <p>Count 1: {count1}</p>
      <p>Count 2: {count2}</p>
      <p>square:{square()}</p>
      <button onClick={() => setCount1(count1 + 1)}>Increment Count 1</button>
      <button onClick={() => setCount2(count2 + 1)}>Increment Count 2</button>
    </div>
  );
};

export default App;

count1count2の2つのカウンターと、count2の値を2乗する処理が書かれています。実際に動かしてみましょう。

ここで、square関数内に重い処理を追加してみましょう。

const square = () => {
  let i = 0;
  while (i < 1000000000) {
    i++;
  }
  return count2 * count2;
}

実際に動かしてみると、count2だけでなく、count1の値が増えた時も処理が重たくなってしまいます。

これは、仮想DOMにおいてcount1の値が更新されたときにも、その差分を計算するためにsquare関数を注目してしまうためです。

そこで、useMemoを使ってsquare関数の処理をメモ化し、再度呼ばれたときは保存した(メモ化した)値を返すようにしましょう。

import { useMemo } from "react";

useMemoをインポートしたうえで、メモ化したい処理をuseMemoで囲み、第2引数に依存する値を指定します。

App.jsx
import { useMemo, useState } from "react";

function App() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  const square = useMemo(() => {
    let i = 0;
    while (i < 1000000000) {
      i++;
    }
    return count2 * count2;
  }, [count2]);

  return (
    <div className="App">
      <h1>useMemo</h1>
      <p>Count 1: {count1}</p>
      <p>Count 2: {count2}</p>
      <p>square:{square}</p>
      <button onClick={() => setCount1(count1 + 1)}>Increment Count 1</button>
      <button onClick={() => setCount2(count2 + 1)}>Increment Count 2</button>
    </div>
  );
};

export default App;

これで、count1の値が更新されたときには、square関数は再計算されず、メモ化された値が返されるようになりました。

useMemoは、アプリケーションのパフォーマンスを向上させるために使われることが多く、大規模なアプリケーションを作る際に不要な処理を省く際に使われます。個人開発などの小規模なアプリケーションではあまり使う機会はないかもしれません。

では、useMemoをたくさん使えばいいのかというとそうではありません。useMemoを使いすぎるとブラウザ上のメモリを圧迫してしまい、かえってパフォーマンスが悪くなってしまうので注意しましょう。

Tailwind CSSの導入

Tailwind CSSは、CSSを書かずにデザインを整えることができるCSSフレームワークです。

まずは、first-react-appプロジェクトにTailwind CSSを導入してみましょう。

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

tailwind.config.jsファイルが作成されるので、以下のように設定を追加します。

tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};

index.cssを以下のように書き換えます。

index.css
@tailwind base;
@tailwind components;
@tailwind utilities;

これで、Tailwind CSSを使う準備が整いました。

Tailwind CSSの使い方

Tailwind CSSは、HTML要素にクラスを追加するだけでデザインを整えることができます。例えば、以下のようなコードを書くことで、ボタンを作成することができます。

<button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
  Button
</button>
  • bg-blue-500:背景色を青色にする
  • hover:bg-blue-700:マウスを乗せたときに背景色を濃い青色にする
  • text-white:文字色を白色にする
  • font-bold:文字を太字にする
  • py-2:上下のpaddingを0.5rem(8px)にする
  • px-4:左右のpaddingを1rem(16px)にする
  • rounded:角を丸くする

ドキュメントに使い方が詳しく書かれているので、参考にしてみてください。

基本的に、コンポーネント要素のデザインを指定する際は、className属性の部分を書き換えるだけで大丈夫です。

ポートフォリオサイトを作ってみよう

それでは、ReactとTailwind CSSを使って簡単なポートフォリオサイトを作成してみましょう。

まず、srcディレクトリ内にComponentsディレクトリを作成し、その中にHeader.jsxFooter.jsxProjects.jsxContact.jsxを作成します。

ヘッダーの作成

Components/Header.jsx
export default function Header() {
  return (
    <header className="bg-gray-800 text-white text-center py-10">
      <div className="max-w-4xl mx-auto">
        <h1 className="text-4xl font-bold">My Portfolio</h1>
        <p className="mt-4 text-lg">
          Welcome to my personal portfolio website built with React and Tailwind
          CSS.
        </p>
      </div>
    </header>
  );
}

フッターの作成

Components/Footer.jsx
export default function Footer() {
  return (
    <footer className="bg-gray-800 text-white text-center py-4">
      <div className="max-w-4xl mx-auto">
        <p className="mb-0">
          &copy; 2024 My Portfolio. All Rights Reserved.
        </p>
      </div>
    </footer>
  );
}

コンテンツ部分の作成

Components/Projects.jsx
import { Link } from "react-router-dom";

export default function Projects() {
  return (
    <section id="projects" className="py-10 bg-gray-100">
      <div className="max-w-6xl mx-auto">
        <h2 className="text-3xl font-bold text-center mb-6">My Projects</h2>
        <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
          <div className="bg-white shadow-md rounded-lg overflow-hidden">
            <img
              src="https://via.placeholder.com/150"
              alt="Project 1"
              className="w-full h-48 object-cover"
            />
            <div className="p-4">
              <h5 className="text-xl font-bold">Project 1</h5>
              <p className="mt-2 text-gray-600">
                A brief description of the project.
              </p>
              <Link
                href="#"
                className="mt-4 inline-block text-indigo-500 hover:underline"
              >
                View Project
              </Link>
            </div>
          </div>
          <div className="bg-white shadow-md rounded-lg overflow-hidden">
            <img
              src="https://via.placeholder.com/150"
              alt="Project 2"
              className="w-full h-48 object-cover"
            />
            <div className="p-4">
              <h5 className="text-xl font-bold">Project 2</h5>
              <p className="mt-2 text-gray-600">
                A brief description of the project.
              </p>
              <Link
                href="#"
                className="mt-4 inline-block text-indigo-500 hover:underline"
              >
                View Project
              </Link>
            </div>
          </div>
          <div className="bg-white shadow-md rounded-lg overflow-hidden">
            <img
              src="https://via.placeholder.com/150"
              alt="Project 3"
              className="w-full h-48 object-cover"
            />
            <div className="p-4">
              <h5 className="text-xl font-bold">Project 3</h5>
              <p className="mt-2 text-gray-600">
                A brief description of the project.
              </p>
              <Link
                href="#"
                className="mt-4 inline-block text-indigo-500 hover:underline"
              >
                View Project
              </Link>
            </div>
          </div>
        </div>
      </div>
    </section>
  );
}

問い合わせフォームの作成

Components/Contact.jsx
export default function Contact() {
  return (
    <section id="contact" className="py-10">
      <div className="max-w-4xl mx-auto">
        <h2 className="text-3xl font-bold text-center mb-6">Contact Me</h2>
        <form className="space-y-6">
          <div>
            <label htmlFor="name" className="block text-gray-700 font-medium">
              Name
            </label>
            <input
              type="text"
              id="name"
              className="w-full mt-2 p-3 border border-gray-300 rounded-lg"
              placeholder="Your name"
            />
          </div>
          <div>
            <label htmlFor="email" className="block text-gray-700 font-medium">
              Email
            </label>
            <input
              type="email"
              id="email"
              className="w-full mt-2 p-3 border border-gray-300 rounded-lg"
              placeholder="Your email"
            />
          </div>
          <div>
            <label
              htmlFor="message"
              className="block text-gray-700 font-medium"
            >
              Message
            </label>
            <textarea
              id="message"
              rows="5"
              className="w-full mt-2 p-3 border border-gray-300 rounded-lg"
              placeholder="Your message"
            ></textarea>
          </div>
          <button
            type="submit"
            className="w-full bg-indigo-500 text-white py-3 rounded-lg hover:bg-indigo-600"
          >
            Send
          </button>
        </form>
      </div>
    </section>
  );
}

トップページの仕上げ

分割したコンポーネントをApp.jsxに組み込みます。

import Header from "./Component/Header";

このように記述することで、別のファイルに分割されたコンポーネントを読み込むことができます。

ヘッダー、フッター、コンテンツ部分、問い合わせフォームを読み込んで、トップページを完成させましょう。

Pages/App.jsx
import Header from "./Component/Header";
import Footer from "./Component/Footer";
import Contact from "./Component/Contact";
import Projects from "./Component/Projects";

function App() {
  return (
    <div>
      <Header />
      <section id="about" className="py-10">
        <div className="max-w-4xl mx-auto text-center">
          <h2 className="text-3xl font-bold mb-6">About Me</h2>
          <p className="text-lg">
            Hello! I am a web developer with a passion for creating beautiful
            and functional websites.
          </p>
        </div>
      </section>
      <Projects />
      <Contact />
      <Footer />
    </div>
  );
}

export default App;

ブラウザ上でサイトが表示できることを確認したら、適宜機能を追加したり、デザインを変えてみましょう。

ドキュメントを参考にしてデザインを変えてみましょう。

paddingやmarginなどの余白の指定方法:

widthやheightなどのサイズ指定方法:

まとめ

今回は、ReactのHookやSPA,SSRについて学び、Tailwind CSSを導入して簡単なポートフォリオサイトを作成しました。

次回のWeb研の講義は、冬休み明けになります... それでは、良いお年を!