はじめに
5日間でReactの基礎を習得する試みの4日目です。4日目は、GitHubの既存のRepositoryを編集することで、実践的に学びます。
私はC#を仕事で使っているため、オブジェクト指向の言語的な考え方を使って理解を進めます。同じような境遇の方の理解の助けになれば幸いです。
Repositoryの選定
Manu Aroraさんが作られたこちらのReactのポートフォリオを編集します。
選定理由は以下です。
- starが300を超えていること
- next.jsで作られていること
- TypeScriptが使われていないこと(今回の勉強の対象外であるため)
- 見た目がきれいであること
- 自由に使って良いという記載があったこと
編集内容
今回は、ナビゲーションバーを編集することで、理解を深めようと思います。
各文字はリンクになっていて、クリックするとページが遷移します。
準備
GitHubからリポジトリをクローンします。
git clone https://github.com/manuarora700/simple-developer-portfolio-website.git
ディレクトリに移動し、必要なパッケージをインストールします。
cd simple-developer-portfolio-website
npm install --force
通常はnpm install
でインストールするべきですが、依存関係でエラーが発生したため、今回は無理やりnpm install --force
で突破しました。恐らく良くないですが、時間の関係で解決は諦めました。
起動しましょう。
npm run dev
コードリーディング
ナビゲーションバーのコンポーネントを見つける
ナビゲーションバーのリンクには、"About","Projects","Experience","Contact"の4つがあります。まず、"Experience"という文字が使われているところを、全文検索で探しました。
いくつかファイルがヒットしましたが、リンクなので、href=
となっているはずです。
つまり、Navbar.jsにナビゲーションバーのコンポーネントがありました。
Navbarコンポーネントを呼び出しているコンポーネント
Navbarを使っているコンポーネントを探しましょう。全文検索です。
ContainerBlock.jsですね。
ContainerBlockコンポーネントを呼び出しているコンポーネント
同様に検索します。
たくさんヒットしました。あらゆるところで使われているコンポーネントみたいです。
この中には、index.jsがあります。Next.jsでは、page/index.jsのコンポーネントが最初に表示されるページ(エントリーポイント)として描画されるため、ここを見てみます。
Homeコンポーネントが書かれていました。
export default function Home({ repositories }) {
return (
<ContainerBlock
title="Manu Arora - Developer, Writer, Creator"
description="This is a template built specifically for my blog - Creating a developer portfolio that gets you a job."
>
<Hero />
<FavouriteProjects />
<LatestCode repositories={repositories} />
</ContainerBlock>
);
}
つまり、エントリーポイントにアクセスする
=> Homeコンポーネントが描画される
=> その中のContainerBlockが描画される
=> その中のNavbarコンポーネントが描画される
という訳です。
ContainerBlockが色々なページから呼び出されているのは、全ページのヘッダーとフッター部分を描画するコンポーネントだからでしょう。ナビゲーションバーはヘッダーの一部ですね。
Navbarコンポーネント
では、Navbarコンポーネントを見てみましょう。
import React, { useEffect, useState } from "react";
import Link from "next/link";
import { useTheme } from "next-themes";
import { useRouter } from "next/router";
import userData from "@constants/data";
export default function Navbar() {
const router = useRouter();
console.log(router.asPath);
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
return (
<div className="max-w-6xl mx-auto px-4 py-10 md:py-20">
<div className="flex md:flex-row justify-between items-center">
<div className="flex flex-col">
<Link href="/">
<h1 className="font-semibold text-xl dark:text-gray-100">
{userData.name}
</h1>
<p className="text-base font-light text-gray-500 dark:text-gray-300">
{userData.designation}
</p>
</Link>
</div>
<div className="space-x-8 hidden md:block">
<Link
href="/experience"
className={`text-base ${
router.asPath === "/experience"
? "text-gray-800 font-bold dark:text-gray-400"
: "text-gray-600 dark:text-gray-300 font-normal "
}`}
>
Experience{" "}
{router.asPath === "/experience" && (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
className="bi bi-arrow-down inline-block h-3 w-3"
viewBox="0 0 16 16"
>
<path
fillRule="evenodd"
d="M8 1a.5.5 0 0 1 .5.5v11.793l3.146-3.147a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 .708-.708L7.5 13.293V1.5A.5.5 0 0 1 8 1z"
/>
</svg>
)}
</Link>
// その他のリンクもおおよそ同じ実装なので省略。
</div>
<div className="space-x-4 flex flex-row items-center">
// 省略。SNSへのリンクを表示するコード。
</div>
</div>
<div className="space-x-8 block md:hidden mt-4">
// 省略。モバイルサイズのときに、サイズを調整してリンクを表示するコード。
</div>
</div>
);
}
ナビゲーションバーを編集する上で、理解すべきなのはLinkコンポーネントとuseRouterです。
<Link
href="/experience"
className={`text-base ${
router.asPath === "/experience"
? "text-gray-800 font-bold dark:text-gray-400"
: "text-gray-600 dark:text-gray-300 font-normal "
}`}
>
Experience{" "}
// 一旦省略するが、ここにページによって表示の有無を切り替えたい要素の記載あり
</Link>
Linkコンポーネント
Linkコンポーネントは、Next.jsが提供しているコンポーネントです。これを使えば、該当要素をクリックすることで、ページ内の遷移が可能です。(HomeからExperienceのページに移動するなど)
以下の形で宣言されています。
<Link href="移動先のリンク" className="指定したいクラス名">
リンクに表示したい文字列
</Link>
移動先のリンクにpagesディレクトリのファイルを指定すると、そのファイル内のコンポーネントが表示されるようになります。今回はhref="/experience"なので、遷移後にはpages/experience.js内のコンポーネントが表示されます。
useRouter
useRouterはNext.jsが提供しているフックの1種です。routerオブジェクトを返します。
const router = useRouter();
このrouterオブジェクトは、現在表示中のページのパスに対する情報を持っています。
例えば、router.asPath
を実行すれば、パスが取得できます。(http://localhost:3000/experienceにアクセスすると、router.asPathで"/experience"が返ってくる)
Linkの中でclassNameを指定する際に、このrouterが使われています。
className={`text-base ${
router.asPath === "/experience"
? "text-gray-800 font-bold dark:text-gray-400" // /experience表示時に付与されるクラス名
: "text-gray-600 dark:text-gray-300 font-normal " // それ以外の表示時に付与されるクラス名
}`}
NavBarはあらゆるところから呼ばれており、experience.jsからも呼ばれています。そのため、上記のコードではexperience.jsにアクセスすると、条件分岐によってclassNameが切り替わるようになっているようです。
同じ原理を使って、このLinkコンポーネント内では、/experienceのページでのみ、矢印が表示されるようになっています。
<Link
href="/experience"
className={`text-base ${
router.asPath === "/experience"
? "text-gray-800 font-bold dark:text-gray-400"
: "text-gray-600 dark:text-gray-300 font-normal "
}`}
>
Experience{" "}
// これ以降がページによって表示の有無を切り替えたい要素
// /experienceのページでは、以下の要素(svgで書かれる矢印)が表示される。
{router.asPath === "/experience" && (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
className="bi bi-arrow-down inline-block h-3 w-3"
viewBox="0 0 16 16"
>
<path
fillRule="evenodd"
d="M8 1a.5.5 0 0 1 .5.5v11.793l3.146-3.147a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 .708-.708L7.5 13.293V1.5A.5.5 0 0 1 8 1z"
/>
</svg>
)}
</Link>
/experienceのページにアクセスすると、"Experience"の文字の右に矢印が表示されます。
補足
classNameに色々と指定されていますが、これはTailwindCSSを使っているようです。例えば、className="flex"
が指定されることで、すべての要素が横並びに配置されています。
ただし、今回の範囲外なので立ち入りません。
ナビゲーションバーを編集する
それでは、ナビゲーションバーを編集します。
その前にリファクタリング
ナビゲーションバーのLinkには、"About","Projects","Experience","Contact"の4つがあります。もともとの実装では、それぞれ同じようなコードが重複して書かれています。これだと、毎回4つともコピペで編集しないといけないので、リファクタリングします。
Link間で異なるのは、hrefの値とLink間に記載するtextでした。そのため、これらを引数にし、共通部分をすべてまとめたNavLinkコンポーネントを作り、Navbar.jsに追記しました。
// chatGPTに任せました
export const NavLink = ({ href, children }) => {
const router = useRouter();
const isActive = router.asPath === href;
const className = isActive
? "text-gray-800 font-bold dark:text-gray-400"
: "text-gray-600 dark:text-gray-300 font-normal";
return (
<Link href={href} className={`text-base ${className}`}>
{children}
{isActive && (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
className="bi bi-arrow-down inline-block h-3 w-3"
viewBox="0 0 16 16"
>
<path fillRule="evenodd" d="M8 1a.5.5 0 0 1 .5.5v11.793l3.146-3.147a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 .708-.708L7.5 13.293V1.5A.5.5 0 0 1 8 1z" />
</svg>
)}
</Link>
);
};
これを使うと、次のようなきれいなコードになります。
<div className="space-x-8 hidden md:block">
<NavLink href="/about">About</NavLink>
<NavLink href="/projects">Projects</NavLink>
<NavLink href="/experience">Experience</NavLink>
<NavLink href="/contact">Contact</NavLink>
</div>
Reactにおいて、コンポーネントの間に挟んだコンポーネントと文字列はchildプロパティとなり、自動的に引数のchildに格納されます。今回の場合は<NavLink href="**">text</NavLink>
のtextがchildに入ります。
編集してみる
ChatGPTにNavbar.jsをコピペし、編集する課題を出してもらいました。ただし、条件として"Material UI"を使った課題であること"を要求しました。世の中でよく使われており、使ってみたかったからです。(参考: Material UIの紹介)
課題内容
MUIのButtonコンポーネントとTypographyコンポーネントを使って、NavLinkコンポーネントをスタイリッシュに再設計する。
MUIのインストール・導入
// 依存関係を強行突破してインストール
npm install @mui/material @emotion/react @emotion/styled @mui/icons-material --force
import Button from '@mui/material/Button';
import { Typography } from "@mui/material";
編集
Material UIの公式サイトで、ButtonとTypographyのサンプルコードを見て、使ってみました。
Typographyコンポーネントは、childに含まれる文字列が読みやすくなるように登録された、文字サイズやフォントなどのプリセットを指定できるコンポーネントです。
export const NavLink = ({ href, children}) => {
const router = useRouter();
const isActive = router.asPath === href;
const className = isActive
? "text-gray-800 font-bold dark:text-gray-400"
: "text-gray-600 dark:text-gray-300 font-normal";
return (
<Link href={href} className={`text-base ${className}`}>
// ここからMUIを追加した部分
<Button color="secondary"> // colorに"secondary"というプリセットを指定
<Typography variant="body1"> // body1というプリセットを指定
{children}
</Typography>
</Button>
// ここまで
{isActive && (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
className="bi bi-arrow-down inline-block h-3 w-3"
viewBox="0 0 16 16"
>
<path fillRule="evenodd" d="M8 1a.5.5 0 0 1 .5.5v11.793l3.146-3.147a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 .708-.708L7.5 13.293V1.5A.5.5 0 0 1 8 1z" />
</svg>
)}
</Link>
);
};
NavLinkを使うコードはそのまま変更なしです。
<div className="space-x-8 hidden md:block">
<NavLink href="/about">About</NavLink>
<NavLink href="/projects">Projects</NavLink>
<NavLink href="/experience">Experience</NavLink>
<NavLink href="/contact">Contact</NavLink>
</div>
さらに編集
折角なので、ボタンの左側にiconを入れることにしました。
NavLinkの引数に、iconを設定出来るようにします。
export const NavLink = ({ href, children, icon }) => {
// 省略
return (
<Link href={href} className={`text-base ${className}`}>
<Button color="secondary">
{icon}
<Typography variant="body1">
{children}
</Typography>
</Button>
// 省略
</Link>
);
};
公式サイトのIconのカタログから使いたいIconを選び、importします。
import {Info, History, Code, Email} from "@mui/icons-material";
NavLinkのpropsにiconを入れます。
<div className="space-x-8 hidden md:block">
<NavLink href="/about" icon={<Info/>}>About</NavLink>
<NavLink href="/projects" icon={<Code/>}>Projects</NavLink>
<NavLink href="/experience" icon={<History/>}>Experience</NavLink>
<NavLink href="/contact" icon={<Email/>}>Contact</NavLink>
</div>
最後に
今まで3日間勉強してきた内容を踏まえれば、色々調べながらではあるものの、コードリーディング/実装を進められることが分かりました。
また、Material UIでは、公式サイトのドキュメントが充実しているため、引数に関する情報をよく読めば、どんどん使える予感がしました。
次回は最終回です。ここで修正したコードのテストを作ります。
何か間違いがあれば、是非コメントを頂けると助かります!
参考文献