実務1年目駆け出しエンジニアがLaravel&ReactでWebアプリケーション開発に挑戦してみた!(その23)
0. 初めに
こんにちは!
このシリーズでは、Webアプリケーションの作り方を一から解説しています!
前回からフロントエンド実装編ということで、画面レイアウトに従って、画面を作り始めました。
今回は、ヘッダーを作成したいと思います!
ヘッダーとは
ヘッダーはその名通り、Webページにおいて画面の最上位に位置する領域で、ページのタイトルやロゴ、検索バーや通知、ログイン中のユーザーアイコンなどを表示します。
これらは、Webアプリケーション内のどのページを見ていてもすぐに使いたくなるようなものです。
一番上にこれらを配置することによって、ユーザーの使いやすさや快適さ、いわゆるUI/IXの向上につながります!
例: YouTube
https://www.youtube.com/

1. ブランチ運用
今日も、ローカルのdevelopを最新化して、新規ブランチを切って作業します。
ブランチ名は、feature/frontend/layout-base/headerにしましょう。
作業が完了したら、コミット・プッシュを忘れずに行いましょう!
細かいコマンドは、前回すべて紹介していますので、もし怪しい場合は確認してみてください。
2. 画面デザイン
前回と同様に実装に入る前に画面デザインを最初に確認します。
今回もFigmaで作成したものを共有いたします。
ご覧のように左から、
- ロゴ
- ページタイトル
- ハンバーガーアイコン
の三つを用意する感じでいきたいと思います。
3. コンポーネントへの切り分け
前回、Layouts/AppLayout.jsxにて、headerを用意していました。
export default function AppLayout({ children }) {
return (
<div className="min-h-dvh flex flex-col bg-[#EEF5F9]">
{/* Header */}
<header className="h-14 border-b border-gray-200 bg-[#EEF5F9] flex items-center px-6">
<h1 className="text-lg font-semibold">ヘッダー領域</h1>
</header>
{/* Main Content Area */}
<main className="flex-1 bg-transparent flex">
<div
className="
mx-auto
w-full
max-w-container
px-[clamp(16px,4vw,32px)]
py-6
bg-[#EEF5F9]
flex-1
"
>
{children || <p className="text-gray-400 text-center">メインコンテンツ領域</p>}
</div>
</main>
</div>
);
}
このままこの中身を修正していってもよいのですが、このheader自体を別の場所に移動させて、それをLayouts/AppLayout.jsxで呼び出すという形を取りたいと思います。
こうすることで、仮に今後headerの中身が肥大化していっもLayouts/AppLayout.jsxのファイルの行数がそれほど多くならず、保守性が上がります。
このような考え方をコンポーネントの切り分けと呼びます!
つまり、部品ごとに役割を分けて、上位の部品は下位の部品を組み合わせることに集中するようにするということです。
新しく、Components/Header.jsxというファイルを作成しましょう。
const Header = () => {
return (
<header>
{/* ここにAppLayout.jsx内のheader要素の中身を貼り付けます */}
</header>
);
};
export default Header;
前回の復習になりますが、Reactでは関数コンポーネントというものを用意して、それをエクスポート・インポートして使いまわすことができるのでした。
もともとのAppLayout.jsx内にあるheaderの内容を返す関数を変数Headerに代入してエクスポートします。
const Header = () => {
return (
<header className="h-14 border-b border-gray-200 bg-[#EEF5F9] flex items-center px-6">
<h1 className="text-lg font-semibold">ヘッダー領域</h1>
</header>
);
};
export default Header;
これを、AppLayoutの方からインポートして使えばOKです!
もともとあったheaderは削除しないとヘッダーが二つ表示されていしまうので注意です。
import Header from "../Components/Header"; // 追加
export default function AppLayout({ children }) {
return (
<div className="min-h-dvh flex flex-col bg-[#EEF5F9]">
{/* Header */}
<Header /> {/* 追加。もともとあったheaderは削除 */}
{/* Main Content Area */}
<main className="flex-1 bg-transparent flex">
<div
className="
mx-auto
w-full
max-w-container
px-[clamp(16px,4vw,32px)]
py-6
bg-[#EEF5F9]
flex-1
"
>
{children || <p className="text-gray-400 text-center">メインコンテンツ領域</p>}
</div>
</main>
</div>
);
}
例によって、AppLayoutが適用されているマイページで確認しましょう。
http://localhost/mypage

問題なさそうですね!(≧◇≦)
※ちなみに前回書きそびれてしまったのですが、Reactでの注意点としてクラス属性を指定するときは、classではなく、classNameと書かないといけません。
4. ロゴ作成
今日、この後はHeader.jsxの中にいろいろと追加していきます!
まずは、ロゴです。
ロゴはアプリ全体の印象を決める重要な部分です。
覚えてもらえるようなものにしたいですよね。
また、クリックすることでトップページに移動できるようにしておくと、ユーザーからも使いやすいと思われるはずです!
事前にロゴの画像を用意しましたので、ダウンロードして使ってください。
ChatGPTとFigmaを組み合わせて作りました!
header.svg
以下のコマンドを実行して、新しいディレクトリを用意してください。
実行コマンド
$ mkdir -p src/resources/js/Assets/logo
このディレクトリ内にダウンロードした画像ファイルを入れてください(VS Codeにドラッグ & ドロップが一番簡単かもです)。
出来たら、Header.jsxで呼び出します。
import logo from '../Assets/logo/header.svg';
const Header = () => {
return (
<header className="h-14 border-b border-gray-200 bg-[#EEF5F9] flex items-center px-6">
<div className="flex items-center">
<img src={logo} alt="App Logo" className="h-9 w-auto" />
</div>
</header>
);
};
export default Header;
本来であれば、画像ファイルはこんな感じにインポートできるものではないのですが、本シリーズで採用しているViteという実行環境だと、こんな感じに書くことで、logoという変数に画像ファイルのパスが代入されます。
それによって、src={logo}のように展開するだけで画像をimgタグの中で設定できます!
さらに、もう一歩アプリを使いやすくしたい方は、トップページへ遷移するためのリンクも貼っておくとよいでしょう!
import logo from '../Assets/logo/header.svg';
import { Link } from '@inertiajs/react'; // 追加
const Header = () => {
return (
<header className="h-14 border-b border-gray-200 bg-[#EEF5F9] flex items-center px-6">
<div className="flex items-center">
<Link
href={route('labs.home')}
aria-label='トップページへ'
>
<img src={logo} alt="App Logo" className="h-9 w-auto" />
</Link>
</div>
</header>
);
};
route('labs.home')と書くことで、バックエンド編で作ったweb.phpに書かれているルーティングを呼び出せます!
Route::get('/', [LabController::class, 'home'])->name('labs.home'); // <-これ
aria-label='トップページへ'の部分は、視覚障がい者の方が使うスクリーンリーダーでの自動読み上げのためのものです。
レイアウトには関係ないので、そこまで気にしなくて大丈夫です!
ロゴをクリックするとトップページに移動できることを確認しましょう!

5. ページタイトル作成
次に、ページタイトルを作成したいと思います。
画面の最上位にあるヘッダーの真ん中にページのタイトルがあれば、今どのページを開いているのかがユーザーから一目でわかってよいと思います!
共通レイアウトとしてのヘッダーなのにどうやってページごとにタイトルを変えるのかと言いますと、例のpropsというものを用います。
少し難しそうに聞こえるかもしれませんが、異なるコンポーネント間でやり取りする変数みたいなものだと思ってもらえればよいかなって思います!
本シリーズでは、Inertitaを用いているのでInertia propsを活用して、Laravel側から設定しているタイトルを最終的にはHeaderに渡すという流れにしたいと思います。
データの流れとしては、以下のイメージです。
MyPageController -> MyPage/Index-> AppLayout -> Header
ということで、MyPageController.phpでInertiaを使って、タイトル情報をpropsでMyPage/Index.jsxに渡すように修正しましょう。
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
use Inertia\Inertia;
class MyPageController extends Controller
{
public function showUser()
{
$user = Auth::user();
// 管理者・一般ユーザー問わず通知を取得
$notifications = $user->notifications()->latest()->get();
return Inertia::render('MyPage/Index', [
'title' => "{$user->name}さんのマイページ", // 追加
'user' => $user,
'notifications' => $notifications,
]);
}
// ...
フロントエンド編なのにバックエンドのコード変えるのかよ!
と思ったかもしれません。
まあ、そういうときもありますよ。(笑)
特にInertiaだとバックエンドとフロンエンドが密に関係性を持っているので行ったり来たりになってしまうのは仕方ないと思います。('_')
実際、バックエンド実装編の時点でReactを作っていましたしね。
続いて、このtitleをMyPage/Index.jsxで受け取って、共通レイアウトへ送りましょう。
import AppLayout from '@/Layouts/AppLayout'; // 追加: AppLayoutをインポート
import { Head, Link, router } from '@inertiajs/react';
export default function Index({ user, notifications = [], title }) { // 追加: titleをpropsとして受け取る
const handleDeleteAccount = () => {
if (confirm('本当に退会しますか?この操作は取り消せません。')) {
router.delete(route('mypage.delete'));
}
};
const unreadCount = notifications.filter(n => !n.read_at).length;
return (
<AppLayout title={title}>
{/* 今まで「マイページ」を設定していたHeadは削除 */}
<div className="space-y-4">
<div className="text-gray-800">
<div className={user.is_admin ? 'text-red-500' : ''}>
未読の通知: {unreadCount}件
</div>
<Link href={route('notifications.index')}>
<button className="mt-2 px-4 py-1 bg-gray-200 rounded-md hover:bg-gray-300">
通知一覧を見る
</button>
</Link>
</div>
<h3 className="text-lg font-semibold">ユーザー情報</h3>
<p>名前: {user.name}</p>
<p>メールアドレス: {user.email}</p>
<p>登録日: {new Date(user.created_at).toLocaleDateString('ja-JP')}</p>
<div className="space-x-2">
<Link href={route('mypage.edit')}>
<button className="px-3 py-1 border rounded-md hover:bg-gray-100">編集する</button>
</Link>
<Link href={route('mypage.bookmarks')}>
<button className="px-3 py-1 border rounded-md hover:bg-gray-100">ブックマーク済み研究室</button>
</Link>
<button
onClick={handleDeleteAccount}
className="px-3 py-1 border rounded-md text-red-600 hover:bg-red-50"
>
退会する
</button>
</div>
</div>
</AppLayout>
);
}
<AppLayout title={title}>とすることで、MyPageControllerから受け取ったtitleをAppLayoutのtitleへ渡します(どちらもtitleなのでわかりにくいですね。その場合は、片方を別の変数名にしても動くことを確認してみてください)。
次に、AppLayoutでそのtitleを受け取って、それをさらにHeaderに渡すようにします。
import { Head } from '@inertiajs/react';
import Header from "../Components/Header"; // 追加
export default function AppLayout({ children, title }) { // 追加: titleをpropsとして受け取る
return (
<div className="min-h-dvh flex flex-col bg-[#EEF5F9]">
<Head title={title} />
{/* Header */}
<Header title={title}/>
{/* Main Content Area */}
<main className="flex-1 bg-transparent flex">
<div
className="
mx-auto
w-full
max-w-container
px-[clamp(16px,4vw,32px)]
py-6
bg-[#EEF5F9]
flex-1
"
>
{children || <p className="text-gray-400 text-center">メインコンテンツ領域</p>}
</div>
</main>
</div>
);
}
一つ前と同様に、<Header title={title}/>とすることで、MyPage/Indexから受け取ったtitleをHeaderにtitleというpropsとして渡すことができます。
ちなみに、<Head title={title} />とすることで、ブラウザ上でのページタイトルを指定することができます。
ブラウザのタブの部分に表示されるものです。
↓これです。

最後に、Headerでtitleを受け取って、表示しましょう。
h1タグを使うのが良いかなと思います。
import logo from '../Assets/logo/header.svg';
import { Link } from '@inertiajs/react';
const Header = ({ title }) => {
return (
<header className="h-14 border-b border-gray-200 bg-[#EEF5F9] flex items-center px-6">
<div className="flex items-center">
<Link
href={route('labs.home')}
aria-label='トップページへ'
>
<img src={logo} alt="App Logo" className="h-9 w-auto" />
</Link>
</div>
<h1 className="absolute left-1/2 -translate-x-1/2 text-xl font-semibold text-black">
{title}
</h1>
</header>
);
};
export default Header;
6. ハンバーガーアイコン作成
さてさて、お次はハンバーガーアイコンを用意しましょう!
少しお腹がすいてきますね。(´・ω・)
こんな感じのやつです。
例: YouTube
https://www.youtube.com/

この一番上の三本戦のやつをハンバーガーメニューと呼んだりしまして、ここをクリックすると次回作る予定のサイドバーが開くという感じにしたいと思います。
画像は以下をお使いください!
hamburger.svg
React Iconsというサイトから探すとよいものが見つかります。
https://react-icons.github.io/react-icons/
ここからダウンロードして、Figmaで手を加えたものが今回使用する画像です。
import logo from '../Assets/logo/header.svg';
import hamburgerIcon from '../Assets/icons/hamburger.svg'; // 追加
import { Link } from '@inertiajs/react';
const Header = ({ title }) => {
return (
<header className="h-14 border-b border-gray-200 bg-[#EEF5F9] flex items-center justify-between px-6">
<div className="flex items-center">
<Link href={route('labs.home')} aria-label="トップページへ">
<img src={logo} alt="App Logo" className="h-9 w-auto" />
</Link>
</div>
<h1 className="absolute left-1/2 -translate-x-1/2 text-xl font-semibold text-black">
{title}
</h1>
<button
className="flex items-end"
aria-label="メニューを開く"
>
<img src={hamburgerIcon} alt="メニュー" className="h-7 w-7 hover:opacity-80 transition" />
</button>
</header>
);
};
export default Header;
ホバー時の変化も付けて起きました。
また、ロゴ、タイトルとの配置が良くなるように、headerのclassName属性にjustify-betweenを追加しておきましたので、ご注意ください!
7. 境界線のフェード作成
ここから先は、おまけなのでお腹いっぱいの方は、コミット・プッシュして終了でも大丈夫です。(笑)
見本とまだ違う点として、下方向の境界線の色がついていません。
これを付けて今日は完成としましょう~!
本来グラデーションは、linear-gradientというCSSプロパティを使うのですが、Tailwind CSSだとできないので、headerの下線というよりは、線を書き加えることで下線であるように見せます。
最終的なHeader.jsxは以下のようになります。
import logo from '../Assets/logo/header.svg';
import hamburgerIcon from '../Assets/icons/hamburger.svg';
import { Link } from '@inertiajs/react';
const Header = ({ title }) => {
return (
<header
className="
relative
h-14
flex
items-center
justify-between
px-6
bg-[#EEF5F9]
after:content-['']
after:absolute
after:bottom-0
after:left-0
after:h-[3px]
after:w-full
after:bg-[linear-gradient(to_right,rgba(226,145,140,0.8),rgba(215,145,232,0.8),rgba(118,192,235,0.8))]
"
>
{/* 左:ロゴ */}
<div className="flex items-center">
<Link href={route('labs.home')} aria-label="トップページへ">
<img src={logo} alt="App Logo" className="h-9 w-auto" />
</Link>
</div>
{/* 中央:タイトル */}
<h1 className="absolute left-1/2 -translate-x-1/2 text-xl font-semibold text-black">
{title}
</h1>
{/* 右:ハンバーガー */}
<button className="ml-auto flex items-center" aria-label="メニューを開く">
<img
src={hamburgerIcon}
alt="メニュー"
className="h-7 w-7 hover:opacity-80 transition"
/>
</button>
</header>
);
};
export default Header;
afterは疑似要素と呼ばれるもので、要素っぽいけどHTMLの要素にはならない装飾のためだけのものとして使うことができます。
気になる方は、「CSS 疑似要素」とかで調べてみましょう!
【気になる方だけ】疑似要素について調べてみましょう。
...どこかで見たことがあるって?
そうですよ!!
ポケポケのデザインが気に入ったので、今回デザインを作るうえでかなり意識させていただきました!(というかパ〇りました)
8. ☆コラム ~Prettierを使おう~
さらに、おまけですが、みなさんはコードフォーマッターというものを使っていますか?
特にHTMLなどのネスト(入れ子構造のこと)が深くなりがちなものを書いているときに、自動でインデントをそろえてくれるツールを使うと便利です。
Prettierは、VS Codeの拡張機能で、インストールをすると、「Ctrl」+「Alt」+「F」でコードをきれいに整えてくれます。
環境構築編の時に、伝えそびれてしまいましたが、便利なのでぜひインストールして使ってみてください。
ところが、自動成型の結果が見づらい時ってあると思います。
自分の期待通りのフォーマットにしたいですよね。
そんな時は、package.jsonなどと同じ階層に.prettierrcというJSON形式の設定ファイルを作成すると便利です。
僕が採用している設定は以下の通りです。
良ければ使ってみてください。
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100,
"arrowParens": "avoid",
"bracketSpacing": true,
"jsxSingleQuote": false,
"endOfLine": "auto"
}
コードフォーマッターPrettierを使ってみよう!
9. まとめ・次回予告
お疲れ様でした!
作業が終わったら、コミット・プッシュをお忘れなくです。('_')
今日は、共通レイアウトととして、ヘッダーを作成しました。
まず、ロゴを作ることで、アプリ全体のイメージを印象付け、トップページに戻りやすくすることでより良いUI/UXを実現しました。
次に、ページタイトルを真ん中に設けることで、自分が今どのページを見ているのかが一目でわかるようになりました。
さらに、ハンバーガーアイコンを付けることで、サイドバーを開く準備をしました。
サイドバーは次回作りますので、お楽しみに!(≧◇≦)
最後に、変なパクリ疑惑も出ましたが、見た目を整えました。
次回もよろしくお願いします!
ありがとうございました。
軽く宣伝
YouTubeを始めました(というか始めてました)。
内容としては、Webエンジニアの生活や稼げるようになるまでの成長記録などを発信していく予定です。
現在、まったく再生されておらず、落ち込みそうなので、見てくださる方はぜひ高評価を教えもらえると励みになると思います。"(-""-)"
これまでの記事一覧
☆要件定義・設計編
☆環境構築編
- その2: 環境構築編① ~WSL, Ubuntuインストール~
- その3: 環境構築編② ~Docker Desktopインストール~
- その4: 環境構築編③ ~Dockerコンテナ立ち上げ~
- その5: 環境構築編④ ~Laravelインストール~
- その6: 環境構築編⑤ ~Gitリポジトリ接続~
☆バックエンド実装編
- その7: バックエンド実装編① ~認証機能作成~
- その8: バックエンド実装編②前編 ~レビュー投稿機能作成~
- その8.5: バックエンド実装編②後編 ~レビュー投稿機能作成~
- その9: バックエンド実装編③ ~レビューCRUD機能作成~
- その10: バックエンド実装編④ ~レビューCRUD機能作成その2~
- その11: バックエンド実装編⑤ ~新規大学・学部・研究室作成機能作成~
- その12: バックエンド実装編⑥ ~大学検索機能作成~
- その13: バックエンド実装編⑦ ~大学・学部・研究室編集機能作成~
- その14: バックエンド実装編⑧ ~コメント投稿機能~
- その15: バックエンド実装編⑨ ~コメント編集・削除機能~
- その16: バックエンド実装編⑩ ~ブックマーク機能~
- その17: バックエンド実装編⑪ ~排他制御・トランザクション処理~
- その18: バックエンド実装編⑫ ~マイページ機能作成~
- その19: バックエンド実装編⑬ ~管理者アカウント機能作成~
- その20: バックエンド実装編⑭ ~通知機能作成~
- その21: バックエンド実装編⑮ ~ソーシャルログイン機能作成~




