はじめに
Install
・node.jsのインストール
npm install react-icons <-- 超定番アイコン(NextJSにはデフォルトでアイコンが入っていない)npx create-next-app@latest
npx create-next-app@latest nextproject <--- 大文字が使えない
√ Would you like to use TypeScript?
... No(TypeScriptも勉強したければYes)
√ Would you like to use ESLint?
... Yes(ESLintは作成したコードをレビューしてくれるツールです。あって困ることはないのでYes)
√ Would you like to use Tailwind CSS?
... No(最近流行りのCSSだけど学習コストが高いのでNO)
√ Would you like your code inside a `src/` directory?
... Yes(プロジェクト直下にsrcディレクトリを配置するかどうか。基本的にYes)
√ Would you like to use App Router? (recommended)
... No(App Routerの情報が少ないので、とりあえずPages Routerで学習してみる)
√ Would you like to use Turbopack for `next dev`?
... No(Webpackの代わりに現れた高速ビルドツール。不安定なので使わない)
√ Would you like to customize the import alias (`@/*` by default)?
... No(インポートエイリアスとはimportでモジュールを読み込むときの相対パスを短くしてくれます
import {Home} from "../../../Home" が import {Home} from "@/Home" になります
初心者にはどうでもいいところなのでNo)ローカルサーバーの立ち上げ
cd nextproject
npm run dev <--- ローカルサーバーの立ち上げフォルダ構成
nextproject
|---node_modules
|---package.json
|---package-lock.json
|---public
| |---favicon.ico
| |---next.svg
| |---sample.jpg
| |---audio
| |---sample.mp3
|---src
| |---components
| | |---Header.js
| | |---Footer.js
| |---pages
| | |---index.js
| |---styles
| |---globals.css
| |---Home.module.csssrc/pages/_app.js
import "@/styles/globals.css"; <-- すべてのページに適用したいCSS
import { Noto_Sans_JP } from 'next/font/google';
const NotoSansJP = Noto_Sans_JP({
weight: ["400", "700"],
subsets: ["latin"],
display: "swap",
});
export default function App({ Component, pageProps }) {
return (
<div className={NotoSansJP.className}>
<Component {...pageProps} />
</div>
);
}
src/pages/index.js
import Header from "@/component/Header"; <-- エイリアスを使う
import { FaBeer } from "react-icons/fa"; <-- react-icons(※1)
import Image from "next/image"; <-- ビルド時に画像圧縮して最適化してくれる
import style from "@/styles/Home.module.css";
export default function Home() {
return(
<>
<Header />
<FaBeer />
<Image
className={style.img1}
src="/sample.webp" <-- public/sample.jpgを参照(ビルド時もコレでOK)
alt=""
width={1000} <-- width,heightプロパティは必須
height={373} <-- pxで指定する必要あり(100%とかはダメ)
</>
);
}※1 … アイコンの種類【https://react-icons.github.io/react-icons/】
src/pages/beginner/question/[id].js
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useState, useEffect } from 'react';
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import styles from '@/styles/Question.module.css';
import { beginnerquestions } from '@/data/beginnerquestions';
export default function Question() {
const [isOpen, setIsOpen] = useState(false);
const [saved, setSaved] = useState(false);
// データフォルダからデータを取り出す
const router = useRouter();
const { id } = router.query; <-- query()にすると router.query is not a function というエラーが出る
if (!id) return null;
const questionId = parseInt(id, 10);
const question = beginnerquestions.find(q => q.id === questionId);
if (!question) return <p>NOT FOUND QUESTION</p>;
const prevId = questionId > 1 ? questionId - 1 : null;
const nextId = questionId < beginnerquestions.length ? questionId + 1 : null;
// ローカルストレージの更新
useEffect( () => {
const savedItems = JSON.parse(localStorage.getItem("favorite") || "[]");
setSaved( savedItems.includes(questionId) );
}, [questionId]);
const switchFavorite = () => {
const savedItems = JSON.parse(localStorage.getItem("favorite") || "[]");
let newItems;
if (saved) {
newItems = saveItems.filter(id => id != questionId);
} else {
newItems = [...savedItems, questionId];
}
localStorage.setItem("favorite", JSON.stringify(newItems));
setSaved(!saved);
}
return (
<>
<Header />
<main>
<div>
<span>英文</span>
<Link href="/beginner">英文一覧に戻る >></Link>
</div>
<p>{question.pron}</p>
<p>{question.eng}</p>
<p>{question.jpn}</p>
<div>
<svg width="30px" height="30px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path /></svg>
<audio controls src="" ></audio>
</div>
<button onClick={() => setIsOpen(!isOpen)}>
<p>使用例</p>
<svg width="16px" height="16px" viewBox="0 0 16 16" />
</button>
{isOpen && (
<div>{question.usage}</div>
)}
{saved ? (
<button onClick={switchFavorite}>後で見ない</button>
) : (
<button onClick={switchFavorite}>後で見る</button>
)}
<div>
{prevId ? (
<Link href={`/beginner/question/${prevId}`}>PREV</Link>
) : (
<button disbled>PREV</button>
)}
{nextId ? (
<Link href={`/beginner/question/${nextId}`}>NEXT</Link>
) : (
<button disbled>NEXT</button>
)}
</div>
</main>
<Footer />
</>
);
}src/components/Header.js
import style from "@/styles/Header.module.css"; <-- インポートしたファイルにだけ読み込まれるCSS
import { Roboto_Condensed } from 'next/font/google';
import Link from "next/link"; <-- ※2
const RobotoCondensed = Roboto_Condensed({
weight: ["400", "700", "900"],
subsets: ["latin"],
display: "swap",
});
export default function Header() {
return (
<>
<header> <-- ※1
<p>SITE TITLE</p>
<nav>
<ul>
<li><Link href="/">HOME</Link></li> <-- href属性には相対パスではなく絶対パスを使用
</ul>
</nav>
</header>
<div className={style.wrap}>
<p className={RobotoCondensed.className}>xxx</p>
<br /> <-- スラッシュで閉じないとエラーになります
</div>
</>
);
}※1…ここを<Header>とすると「RangeError: Maximum call stack size exceeded」というエラーが発生します。Header関数が自分自身を呼び出しているので永遠にループするためです
※2 … あらかじめLinkタグのコンテンツを先読みしてキャッシュに保存することで、ページ遷移の速度を向上させています。prefetch機能と呼びます。
styles/globals.css
div {
background: url("/sample.jpg"); <-- public/sample.jpgを参照
}