概要
Next.js は、React を使ってWebページを作成できるフレームワーク
「ネクスト・ジェイエス」と読む
ただし今新規に作成するなら、Next.js ではなく Nuxt.js でいいのかもしれない
Next.js by Vercel - The React Framework
https://nextjs.org/
なぜNext.jsを採用するのか? - mottox2 blog
https://mottox2.com/posts/429
Next.js - Qiita
https://qiita.com/nkzawa/items/1e0e93efd13fb982c8c0
TypeScript と Redux に入門しながら Next.js でサンプルアプリケーションを作ってみた - Qiita
https://qiita.com/monzou/items/8ff07258a42042075d7b
Next.js 4年目の知見:SSRはもう古い、VercelにAPIサーバを置くな - Qiita
https://qiita.com/jagaapple/items/faf125e28f8c2860269c
無償版PaaSだけでGithubでログインするNext.jsアプリを作る - Qiita
https://qiita.com/greenteabiscuit/items/b32a5715d48cbc794a70
Next.js は本当にSEOに強いのか調べてみた
https://zenn.dev/fukurose/articles/e15df7129cc421
結局ReactとNext.jsのどちらで開発を進めればいいの? - Qiita
https://qiita.com/hiroki-yama-1118/items/b3388c5dcb155e2e367d
■App Router
Next.jsでは「Page Router」と呼ばれる手法が使われていたが、後から「App Router」と呼ばれる手法が導入された
新しく作るアプリケーションでは「App Router」を使うことが推奨されるようだが、これにより考えることが多くなった
Next.js って App Router が出てきて平和じゃなくなったよね
https://zenn.dev/noko_noko/articles/3ccc64c389259c
App Router時代のZero-Runtimeの理解を深める
https://zenn.dev/blueish/articles/e8bc1a5caf139f
App Router で1年間開発した知見と後悔
https://zenn.dev/ficilcom/articles/091abe948f44fb
Next.jsの考え方
https://zenn.dev/akfm/books/nextjs-basic-principle
それなら、シンプルな構成のRemixに移行しよう…という動きもあるらしい
Remixについては「Remix.txt」を参照
起動
>cd C:\path\to\next_project
>npx create-next-app next_app
Need to install the following packages:
create-next-app@14.2.3
Ok to proceed? (y)
√ Would you like to use TypeScript? ... No / Yes
√ Would you like to use ESLint? ... No / Yes
√ Would you like to use Tailwind CSS? ... No / Yes
√ Would you like to use `src/` directory? ... No / Yes
√ Would you like to use App Router? (recommended) ... No / Yes
√ Would you like to customize the default import alias (@/*)? ... No / Yes
Creating a new Next.js app in C:\localhost\home\test\public_html\react\next_app.
next_app フォルダが作成され、その中にファイルが作成される
プロジェクトの操作はこのフォルダ内で行うため、フォルダ内に移動しておく
>cd next_app
以下でプロジェクトを実行する
>npm run dev
> next_app@0.1.0 dev
> next dev
▲ Next.js 14.2.3
- Local: http://localhost:3000
Starting...
Attention: Next.js now collects completely anonymous telemetry regarding usage.
This information is used to shape Next.js' roadmap and prioritize features.
You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL:
https://nextjs.org/telemetry
Ready in 6.6s
上記のように表示され、表示されている http://localhost:3000 にアクセスするとページが表示される
基本の構成
src/app 内のファイルを以下のように編集し、テキストが表示されるだけの状態にする
これをもとに、ファイルの編集を試していく
src/app/page.tsx
export default function Home() {
return (
<main>
<h1 className="text-2xl font-bold m-5">Next.js</h1>
<p className="m-5">これはサンプルアプリケーションです。</p>
</main>
)
}
src/app/layout.tsx
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'Next.js',
description: 'Generated by create next app',
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="ja">
<body className={inter.className}>{children}</body>
</html>
)
}
src/app/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 255, 255, 255;
--background-end-rgb: 255, 255, 255;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}
基本の機能
■Reactによる機能実装とTailwind CSSによる装飾
src/app/page.tsx
'use client'
import { useState } from 'react'
export default function Home() {
const [name, setName] = useState('')
const [message, setMessage] = useState('名前を入力してください。')
const doInput = (e: any)=> {
setName(e.target.value)
}
const doClick = (e: any)=> {
e.preventDefault()
setMessage('こんにちは、' + name + 'さん。')
}
return (
<main>
<h1 className="title">Next.js</h1>
<p className="message">{message}</p>
<form className="form">
<input type="text" className="input" onChange={doInput} />
<button className="button" onClick={doClick}>クリック</button>
</form>
</main>
)
}
src/app/globals.css (追加分)
.title {
@apply text-2xl font-bold m-5;
}
.message {
@apply m-5;
}
.form {
@apply m-5;
}
.input {
@apply p-1 border-solid border-2 border-gray-400;
}
.button {
@apply ms-1 px-4 py-2 bg-gray-800 text-white text-sm rounded-lg;
}
■ルーティングとページ遷移
src/app/page.tsx
'use client'
import { useState } from 'react'
import Link from 'next/Link'
export default function Home() {
const [name, setName] = useState('')
const [message, setMessage] = useState('名前を入力してください。')
const doInput = (e: any)=> {
setName(e.target.value)
}
const doClick = (e: any)=> {
e.preventDefault()
setMessage('こんにちは、' + name + 'さん。')
}
return (
<main>
<h1 className="title">Next.js</h1>
<p className="message">{message}</p>
<form className="form">
<input type="text" className="input" onChange={doInput} />
<button className="button" onClick={doClick}>クリック</button>
</form>
<div className="m-5">
<Link href="/other" className="text-blue-600 underline">Other pageへ</Link>
</div>
</main>
)
}
src/app/other/page.tsx
'use client'
import Link from 'next/Link'
export default function Other() {
return (
<main>
<h1 className="title">Other page</h1>
<p className="message">これは別のページです。</p>
<div className="m-5">
<Link href="/" className="text-blue-600 underline">戻る</Link>
</div>
</main>
)
}
■ローカルCSS
ローカルCSSを作成し、「import './style.css'」で読み込むことができる
これで src/app/globals.css の内容が上書きされる
src/app/other/style.css
.title {
@apply text-4xl text-red-600;
}
src/app/other/page.tsx
'use client'
import Link from 'next/Link'
import './style.css'
export default function Other() {
return (
<main>
<h1 className="title">Other page</h1>
<p className="message">これは別のページです。</p>
<div className="m-5">
<Link href="/" className="text-blue-600 underline">戻る</Link>
</div>
</main>
)
}
■Styled JSX
src/app/other/page.tsx
'use client'
import Link from 'next/Link'
import './style.css'
import JSXStyle from 'styled-jsx/style'
export default function Other() {
return (
<main>
<JSXStyle>
{`p.message {
text-align: center;
font-weight: bold;
}`}
</JSXStyle>
<h1 className="title">Other page</h1>
<p className="message">これは別のページです。</p>
<div className="m-5">
<Link href="/" className="text-blue-600 underline">戻る</Link>
</div>
</main>
)
}
■ダイナミックルーティング
src/app/list/[id]/page.tsx
'use client'
import Link from 'next/Link'
export default function List({params}:{params:{id: number}}) {
return (
<main>
<h1 className="title">Lis page</h1>
<p className="message">指定されたIDは「{params.id}」です。</p>
<div className="m-5">
<Link href="/" className="text-blue-600 underline">戻る</Link>
</div>
</main>
)
}
http://localhost:3000/list/1 にアクセスすると「指定されたIDは「1」です。」と表示され、
http://localhost:3000/list/2 にアクセスすると「指定されたIDは「2」です。」と表示される
src/app/list/page.tsx
'use client'
import Link from 'next/Link'
export default function List() {
return (
<main>
<h1 className="title">Lis page</h1>
<p className="message">IDを指定してアクセスしてください。</p>
<div className="m-5">
<Link href="/" className="text-blue-600 underline">戻る</Link>
</div>
</main>
)
}
http://localhost:3000/list/ にアクセスすると「IDを指定してアクセスしてください。」と表示される
■クエリパラメータ
src/app/query/page.tsx
'use client'
import { useSearchParams } from 'next/navigation'
export default function Query() {
const params = useSearchParams()
return (
<main>
<h1 className="title">Query sample</h1>
<p className="message">パラメータの取得。</p>
<ul className="m-5">
<li>id: {params.get('id')}</li>
<li>pass: {params.get('pass')}</li>
</ul>
</main>
)
}
http://localhost:3000/query?id=test&pass=abcd1234 のようにアクセスすると、指定してidとpassの値が表示される
データアクセス
■サーバコンポーネントからデータアクセス
ページにアクセスすると、JSONデータを取得して画面に表示する
src/app/public/sample.json
{
"message":"これはデータのサンプルです。",
"data":[
{"name":"太郎","mail":"taro@example.com","age":"39"},
{"name":"花子","mail":"hanako@example.com","age":"28"},
{"name":"幸子","mail":"sachico@example.com","age":"17"}
]
}
src/app/fetch/page.tsx
'use server'
async function getSampleData() {
const resp = await fetch(
'http://localhost:3000/sample.json',
{ cache: 'no-store' }
)
const result = await resp.json()
return result
}
export default async function Fetch() {
const data = await getSampleData()
return (
<main>
<h1 className="title">Fetch sample</h1>
<p className="message">{data.message}</p>
</main>
)
}
■クライアントコンポーネントからデータアクセス
ボタンをクリックすると、JSONデータを取得して画面に表示する
src/app/public/sample.json
{
"message":"これはデータのサンプルです。",
"data":[
{"name":"太郎","mail":"taro@example.com","age":"39"},
{"name":"花子","mail":"hanako@example.com","age":"28"},
{"name":"幸子","mail":"sachico@example.com","age":"17"}
]
}
src/app/fetch/page.tsx
'use client'
import { useState } from 'react'
async function getSampleData() {
const resp = await fetch(
'http://localhost:3000/sample.json',
{ cache: 'no-store' }
)
const result = await resp.json()
return result
}
export default function Fetch() {
const [message, setMessage] = useState('No data.')
const doInput = (e: any) => {
setMessage(e.target.value)
}
const doClick = (e: any)=> {
e.preventDefault()
getSampleData().then(resp => {
setMessage(resp.message)
})
}
return (
<main>
<h1 className="title">Fetch sample</h1>
<p className="message">{message}</p>
<form className="form">
<button className="button" onClick={doClick}>クリック</button>
</form>
</main>
)
}
サーバアクション
src/app/public/sample.json
{
"message":"これはデータのサンプルです。",
"data":[
{"name":"太郎","mail":"taro@example.com","age":"39"},
{"name":"花子","mail":"hanako@example.com","age":"28"},
{"name":"幸子","mail":"sachico@example.com","age":"17"}
]
}
src/app/action/page.tsx
'use client'
import { getSampleData } from './get-sample-data'
export default async function Action() {
return (
<main>
<h1 className="title">Action sample</h1>
<p className="message">ボタンをクリックしてください。</p>
<form className="m-5" action={getSampleData}>
<input type="number" className="input" name="number" />
<button className="button">クリック</button>
</form>
</main>
)
}
src/app/action/get-sample-data.tsx
'use server'
import { redirect } from 'next/navigation'
export async function getSampleData(form) {
const number = form.get('number')
const resp = await fetch(
'http://localhost:3000/sample.json',
{ cache: 'no-store' }
)
const result = await resp.json()
console.log(number)
let data = result.data[number]
if (data == null) {
data = {name:'-',mail:'-',age:0}
}
const query = 'name=' + data.name + '&mail=' + data.mail + '&age=' + data.age
const searchParams = new URLSearchParams(query)
redirect('/action-result?' + searchParams.toString())
}
src/app/action-result/page.tsx
'use client'
import Link from 'next/Link'
import { useSearchParams } from 'next/navigation'
export default async function ActionResult() {
const params = useSearchParams()
return (
<main>
<h1 className="title">Action sample</h1>
<p className="message">以下のデータを取得しました。</p>
<ul className="m-5">
<li>name: {params.get('name')}</li>
<li>mail: {params.get('mail')}</li>
<li>age: {params.get('age')}</li>
</ul>
<div className="m-5">
<Link href="/action" className="text-blue-600 underline">戻る</Link>
</div>
</main>
)
}
ファイルアクセス
■サーバコンポーネントからファイルアクセス
src/app/memo/page.tsx
'use server'
import { saveMemo } from './memo-action'
export default async function Memo() {
return (
<main>
<h1 className="title">Memo</h1>
<p className="message">メモを入力してください。</p>
<form className="m-5" action={saveMemo}>
<div><textarea className="input" name="memo" cols="30" rows="3"></textarea></div>
<button className="button">保存</button>
</form>
</main>
)
}
src/app/memo/memo-action.tsx
'use server'
import { redirect } from 'next/navigation'
import fs from 'fs'
const filename = './src/app/memo/data.txt'
export async function saveMemo(form) {
const memo = form.get('memo')
fs.writeFileSync(filename, memo)
redirect('/memo-complete')
}
export async function readMemo() {
return fs.readFileSync(filename, 'utf8')
}
src/app/memo-complete/page.tsx
'use server'
import Link from 'next/Link'
import { readMemo } from '../memo/memo-action'
export default async function MemoComplete() {
const memo = await readMemo()
return (
<main>
<h1 className="title">Memo</h1>
<p className="message">メッセージを保存しました。</p>
<pre className="message">{memo}</pre>
<div className="m-5">
<Link href="/memo" className="text-blue-600 underline">戻る</Link>
</div>
</main>
)
}
src/app/memo/data.txt
(カラのファイル)
■クライアントコンポーネントからファイルアクセス
src/app/memo-show/page.tsx
'use client'
import { readMemo } from '../memo/memo-action'
import { useState, useEffect } from 'react'
export default function MemoShow() {
const [memo, setMemo] = useState('No data.')
useEffect(() => {
readMemo().then(res => {
setMemo(res)
})
}, [])
return (
<main>
<h1 className="title">Memo</h1>
<p className="message">メッセージは以下のとおりです。</p>
<pre className="message">{memo}</pre>
</main>
)
}
APIアクセス
src/app/request-api/route.ts
'use server';
export async function GET(request: Request) {
const res = { content: 'Hello, World!' };
return new Response(JSON.stringify(res), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}
export async function POST(request: Request) {
const data = await request.json();
return new Response(JSON.stringify(data), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}
src/app/request/page.tsx
'use client'
import { useState, useEffect } from 'react'
export default function Request() {
const [data, setData] = useState('')
const [input, setInput] = useState('')
useEffect(() => {
fetchData()
}, [])
const fetchData = async () => {
try {
const response = await fetch('http://localhost:3000/request-api', {
method: 'GET'
})
if (!response.ok) {
throw new Error('Network response was not ok.')
}
const json = await response.json()
setData(json)
} catch (error) {
console.error('Failed to fetch data: ', error)
}
}
const doSubmit = async (event) => {
event.preventDefault()
try {
const response = await fetch('http://localhost:3000/request-api', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ content: input }),
})
if (!response.ok) {
throw new Error('Network response was not ok.')
}
const json = await response.json()
setData(json)
} catch (error) {
console.error('Failed to post data: ', error)
}
}
return (
<main>
<h1 className="title">Request</h1>
<form className="m-5" onSubmit={doSubmit}>
<input
type="text"
value={input}
className="input"
onChange={(e) => setInput(e.target.value)}
/>
<button className="button" type="submit">送信</button>
</form>
<div className="message">
{data ? (
<pre>{JSON.stringify(data, null, 2)}</pre>
) : (
<p>Now Loading...</p>
)}
</div>
</main>
)
}
ページ遷移アニメーション
JavaScriptでURLを書き換えてページ遷移したように見せることができる
恐らく、内部的には同じ方法が使われているのだと思われる
「a」ではなく「Link」でページ遷移することで、実際のページ遷移が発生しないようにしている
検証ページ
https://www.refirio.org/memos/javascript/state/
JavaScriptによるページ遷移なので、ページ遷移時のアニメーションを凝ったものにするなどができそう
Next.js + Framer Motion でページ遷移アニメーションを実装する #TypeScript - Qiita
https://qiita.com/arrow2nd/items/b16385cf22c567fbbf33
Next.js x Framer Motion : ページ遷移時にふわっと切り替わるアニメーション
https://zenn.dev/takna/articles/next-framer-motion-page-fade-in-animation
Next.js13.4 + Framer Motion でページ遷移時の ふわっ とした動きを実装してみた。
https://zenn.dev/bloomer/articles/3a814d9f054198
以下は遷移前と遷移後のページを同時に存在させて、トランジションを設定する解説みたい
また確認したい
Next.jsでPage Transitionを組む方法|東京のWEB制作会社・ホームページ制作会社|株式会社GIG
https://giginc.co.jp/blog/giglab/Nextjs-page-transition
■作業前
src/app/motion/page.tsx
'use client'
import Link from 'next/Link'
export default function Motion() {
return (
<main>
<h1 className="title">Motion</h1>
<p className="message">ページ遷移します。</p>
<div className="m-5">
<Link href="/motion-after" className="text-blue-600 underline">進む</Link>
</div>
</main>
)
}
src/app/motion-after/page.tsx
'use client'
import Link from 'next/Link'
export default function Motion() {
return (
<main>
<h1 className="title">Motion</h1>
<p className="message">ページ遷移しました。</p>
<div className="m-5">
<Link href="/motion" className="text-blue-600 underline">戻る</Link>
</div>
</main>
)
}
■アニメーション設定
まずは framer-motion をインストールする
>npm install framer-motion
アニメーション用のコンポーネントを作成する
'use client'
import { usePathname } from 'next/navigation'
import { AnimatePresence } from 'framer-motion'
import { motion } from 'framer-motion'
export default function MotionWrapper({
children,
}: {
children: React.ReactNode
}) {
// 一意のキーを設定するためにラップした画面のパスを取得
const pathName = usePathname()
return (
// アンマウント時の動きをつけるために必要な記述
<AnimatePresence mode="wait">
// 動きをつけるために必要な記述
// 具体的な動きを記述
// 今回はopacityを使用して「ふわっ」を実現
<motion.div
key={pathName}
initial={{ opacity: 0 }} // 初期状態
animate={{ opacity: 1 }} // マウント時
exit={{ opacity: 0 }} // アンマウント時
transition={{
type: 'linear',
duration: 2,
}}
>
{children}
</motion.div>
</AnimatePresence>
)
}
以下のようにMotionWrapperを指定すると、ページを表示させた際にアニメーションが表示される
'use client'
import Link from 'next/Link'
import MotionWrapper from '@/components/motionWrapper/motionWrapper'
export default function Motion() {
return (
<MotionWrapper>
<main>
<h1 className="title">Motion</h1>
<p className="message">ページ遷移しました。</p>
<div className="m-5">
<Link href="/motion" className="text-blue-600 underline">戻る</Link>
</div>
</main>
</MotionWrapper>
)
}
EC2での稼働
■参考になりそうな記事
AWS EC2でNext.jsの環境構築する際の備忘録 #AWS - Qiita
https://qiita.com/naniwadari/items/d74fc4a649e69477fa6f
Next.js(&TS)アプリをEC2にデプロイする
https://zenn.dev/eng_o109/articles/41611f14917ba2
React/Next.jsアプリケーションを作成し、AWS EC2を使って本番環境にデプロイするまで #JavaScript - Qiita
https://qiita.com/longtime1116/items/18553e43bfb44cbc9d81
【2020年】AWS EC2でNext.jsの環境構築 - VIVITABLOG
https://blog.vivita.io/entry/2020/08/17/112932
初心者がNext.jsをECSで動かすまで #Next.js - Qiita
https://qiita.com/mkt1234/items/52735503624d12243cf9
■想定の手順
※ChatGPTに確認した概要。正しい内容かどうかは未検証
※Next.jsとLaravelを同一EC2で稼働させる場合の例
※これで問題ないなら、Nginxはローカル開発用のDockerにも含めておくと良さそう
Next.jsを3000番ポートで稼働させる
トップページにリクエストされた場合などは、このポートで処理を行う想定とする
Laravelを8000番ポートで稼働させる
/api 配下にリクエストされた場合に限り、このポートで処理を行う想定とする
Nginxを80番ポートで稼働させ、以下のように設定する
server {
listen 80 default_server;
server_name _;
# 静的ファイルへのリクエストはNext.jsが処理
location / {
proxy_pass http://localhost:3000; # Next.jsのポート
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
# APIリクエストをLaravelに転送
location /api {
proxy_pass http://localhost:8000; # Laravelのポート
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
ロードバランサーでは80番ポートと443番ポートのみを開放する
80番ポートにアクセスがあれば、443番ポートに転送する
443番ポートにアクセスがあれば、Nginxの80番ポートに転送する
これでNext.jsアプリケーションが稼働するはず
メモ
■入門
とほほのNext.js入門 - とほほのWWW入門
https://www.tohoho-web.com/ex/nextjs.html
■CRUD
Next.js + LaravelでCRUD機能を作成する #Laravel - Qiita
https://qiita.com/masakiwakabayashi/items/5286e61f5cb664e1dab9
■サーバサイドAPIの作成
Next.jsのAPI Routesを利用して、データベースアクセスなども含めて対応することはできる
ただしNext.jsはフロントエンドに特化したフレームワークなので、バックエンドに関する機能は力不足(記述が煩雑)の場合がある
その場合、バックエンド部分にLaravelを使うという選択肢がある
…らしい
Next.js入門: データベースとの連携を学びながらToDoリストを作ろう
https://zenn.dev/mostlove/articles/c9e6f1aa45ea09
Next.jsでDBにデータを登録する(MySQL)
https://zenn.dev/kaikusakari/articles/f2cc55db8c90c4
Next.js + LaravelでCRUD機能を作成する #Laravel - Qiita
https://qiita.com/masakiwakabayashi/items/5286e61f5cb664e1dab9
■その他
Next.jsでDataLoaderを使ってコンポーネントの責務を明確にする
https://zenn.dev/praha/articles/716bc6b0f16cff