Closed16
Next.js Tutorial
Next.js Tutorial
Chapter 2 CSS Styling
- Global styles
- Tailwind
- CSS Modules
- clsx
- conditional style
の紹介
Chapter 3 Optimizing Fonts and Images
next/font
- ビルド時にフォントを埋め込んでフォント読み込み時間のレイアウトシフトが起きなくなる
import { Inter, Lusitana } from 'next/font/google';
export const inter = Inter({ subsets: ['latin'] });
<html lang="en">
<body className={`${inter.className} antialiased`}>{children}</body>
</html>
next/image
-
next/image
のImage
コンポーネントがやってくれる最適化- Preventing layout shift automatically when images are loading.
- Resizing images to avoid shipping large images to devices with a smaller viewport.
- Lazy loading images by default (images load as they enter the viewport).
- Serving images in modern formats, like WebP and AVIF, when the browser supports it.
-
width
,height
を指定する
<Image
src="/hero-desktop.png"
width={1000}
height={760}
className="hidden md:block"
alt="Screenshots of the dashboard project showing desktop version"
/>
Chapter 4 Creating Layouts and Pages
-
app
ディレクトリのpage.tsx
かroute.ts
のみルーティングされるので、あとは自由に。 -
layout.tsx
で共通レイアウト。
Chapter 5 Navigating Between Pages
-
<a>
じゃなくて<Link>
を使いましょう。 -
usePathname()
でアクティブリンク。
Chapter 6 Setting Up Your Database
-
@vercel/postgres
の宣伝。
Chapter 7 Fetching Data
- Asyncコンポーネントにできる。
Chapter 8 Static and Dynamic Rendering
- 特になし
Chapter 9 Streaming
-
loading.tsx
にAsyncコンポーネントの表示待ちの間Suspenseで表示するStaticコンポーネントを書ける。 -
/dashboard
直下に配置するとそれ以下のすべてのルートに対して適用されてしまう(たとえば/customers/page.tsx
)。- 最上位のコンポーネントにのみ適用したい場合は、たとえば
/(overview)
というディレクトリにいれる。- これは route groups という機能で、別のディレクトリに分けながらもルーティングとしてはひとつ上の階層に展開されるような感じになる。
- https://nextjs.org/docs/app/building-your-application/routing/route-groups
- 最上位のコンポーネントにのみ適用したい場合は、たとえば
- /dashboard
- /(overview)
- loading.tsx
- page.tsx <- /dashboard
- /customers
- /page.tsx <- /dashboard/customers
- layout.tsx
- 互いに無関係なデータを並列で取得する場合、取得できたデータから表示したい。
- 自分でSuspenseを使いましょう。
Chapter 10 Partial Prerendering (Optional)
- まだ experimental な機能だが、部分的なプリレンダリングを使用することができる。
Chapter 11 Adding Search and Pagination
- ページコンポーネントは
{ params: {}, searchParams: {} }
のようなprops
を受け取る - サーバーコンポーネントは基本 static renderingだが、中で dynamic function を使うと自動的に dynamic rendering に変わる
- https://nextjs.org/docs/app/building-your-application/rendering/server-components#dynamic-functions
- cookies() and headers(): Using these in a Server Component will opt the whole route into dynamic rendering at request time.
- searchParams: Using the searchParams prop on a Page will opt the page into dynamic rendering at request time.
- クライアントコンポーネント(
'use client'
)の場合はuseSearchParams()
で動的にsearchParams
を取得する
Chapter 12 Mutating Data
-
<form action={serverAction}>
- フォームの内容をサーバーで処理するために
'use server'
ディレクティブを使用したサーバーアクションを使う- 自動的に生えるPOST API エンドポイントへリクエストする関数になるだけなので、クライアントコンポーネントの他の場所でも使える
- https://ja.react.dev/reference/react/use-server
- フォームの内容をサーバーで処理するために
'use server'
export async function createInvoice(formData: FormData) {
const rawFormData = {
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
}
console.log(rawFormData)
}
- フォームを処理したあと、
next/cache
のrevalidatePath('/dashboard/invoices')
でサーバーのキャッシュを破棄する- https://nextjs.org/docs/app/api-reference/functions/revalidatePath
- 一覧画面の delete ボタンのようにフォームアクションのあとにリダイレクトがない場合は、これだけで自動的にリロードされる
-
next/navigation
のredirect('/dashboard/invoices')
でリダイレクト
Chapter 13 Handling Errors
-
error.tsx
はそのルートのすべてのエラーをキャッチする -
not-found.tsx
はnext/navigation
のnotFound()
が呼ばれたときに表示される
Chapter 14 Improving Accessibility
- next.jsはデフォルトでeslint-plugin-jsx-a11yを含んでいる
-
useFormState
によるサーバーサイドバリデーション
create-form.tsx
'use client'
import { useFormState } from 'react-dom'
export default function Form({ customers }: { customers: CustomerField[] }) {
const initialState = { message: null, errors: {} };
const [state, dispatch] = useFormState(createInvoice, initialState);
return <form action={dispatch}>...</form>;
}
action.ts
export type State = {
errors?: {
customerId?: string[]
amount?: string[]
status?: string[]
}
message?: string | null
}
const FormSchema = z.object({
id: z.string(),
customerId: z.string({
invalid_type_error: 'Please select a customer.',
}),
amount: z.coerce
.number()
.gt(0, { message: 'Please enter an amount greater than $0.' }),
status: z.enum(['pending', 'paid'], {
invalid_type_error: 'Please select an invoice status.',
}),
date: z.string(),
});
export async function createInvoice(
prevState: State,
formData: FormData,
): Promise<State> {
const validatedFields = CreateInvoice.safeParse({
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
})
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
message: 'Missing Fields. Failed to Create Invoice.',
}
}
const { amount, customerId, status } = validatedFields.data
// ...
}
- ユーザーのフォーカスがあたっていなくても変更される可能性のある領域をライブリージョンと呼ぶ
- https://developer.mozilla.org/ja/docs/Web/Accessibility/ARIA/ARIA_Live_Regions
-
aria-live="polite"
によって、エラーメッセージの変更をユーザーの邪魔をしない形で通知できる - さらにライブリージョンに
aria-atomic="true"
を加えると、ライブリージョンの一部(子孫要素)で変更があった場合、その要素だけでなくaria-atomic="true"
のある要素全体(=ライブリージョン全体)の内容の変更を通知する- 変更があった要素から
aria-atomic="true"
のある要素まで祖先要素を遡って通知する領域を決定するという仕組み
- 変更があった要素から
<select
id="customer"
name="customerId"
defaultValue=""
aria-describedby="customer-error"
>
{/* ... */}
<div id="customer-error" aria-live="polite" aria-atomic="true">
{state.errors?.customerId &&
state.errors.customerId.map((error: string) => (
<p className="mt-2 text-sm text-red-500" key={error}>
{error}
</p>
))}
</div>
-
bind
していこう- (prevState: State, formData: FormData): Promise<State> という型は
useFormState
の要求 - アクションに
id
などのフォーム以外の要素が必要な場合は、prevState
、formData
の前に受け取るようにして引数をbind
するといい
- (prevState: State, formData: FormData): Promise<State> という型は
actions.ts
export async function updateInvoice(
id: string,
prevState: State,
formData: FormData,
) {}
edit-form.tsx
export default function EditInvoiceForm({
invoice,
customers,
}: {
invoice: InvoiceForm
customers: CustomerField[]
}) {
const initialState = { message: null, errors: {} }
const updateInvoiceWithId = updateInvoice.bind(undefined, invoice.id)
const [state, dispatch] = useFormState(updateInvoiceWithId, initialState)
// ...
}
Chapter 15 Adding Authentication
-
NextAuth.jsを使ったAuthentication
-
secret key の生成
openssl rand -base64 32
-
.env
にAUTH_{PROVIDER}_{ID|SECRET}={SECRET_KEY}
を置くと勝手に読まれる
-
auth.config.ts
-
pages
にはsignIn
,signOut
,error
,verifyRequest
,newUser
のための自前のページのパスを指定する- 指定しないとNextAuth.jsのデフォルトのページが開く
-
callbacks
のauthorized
では認証後のリダイレクト先を決定する
-
auth.config.ts
import type { NextAuthConfig } from 'next-auth';
export const authConfig = {
pages: {
signIn: '/login',
},
callbacks: {
authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user;
const isOnDashboard = nextUrl.pathname.startsWith('/dashboard');
if (isOnDashboard) {
if (isLoggedIn) return true;
return false; // Redirect unauthenticated users to login page
} else if (isLoggedIn) {
return Response.redirect(new URL('/dashboard', nextUrl));
}
return true;
},
},
providers: [], // Add providers with an empty array for now
} satisfies NextAuthConfig;
- project root に
middleware.ts
を置くとミドルウェアとして実行される
middleware.ts
import NextAuth from 'next-auth'
import { authConfig } from './auth.config'
export default NextAuth(authConfig).auth
export const config = {
// https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher
matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}
-
auth.ts
にユーザーを取得する処理を書く-
signIn
,signOut
が返ってくるのでアプリケーション内で使う
-
auth.ts
import type { User } from '@/app/lib/definitions'
import { sql } from '@vercel/postgres'
import bcrypt from 'bcrypt'
import NextAuth from 'next-auth'
import Credentials from 'next-auth/providers/credentials'
import { z } from 'zod'
import { authConfig } from './auth.config'
async function getUser(email: string): Promise<User | undefined> {
try {
const user = await sql<User>`SELECT * FROM users WHERE email=${email}`
return user.rows[0]
} catch (error) {
console.error('Failed to fetch user:', error)
throw new Error('Failed to fetch user.')
}
}
export const { auth, signIn, signOut } = NextAuth({
...authConfig,
providers: [
Credentials({
async authorize(credentials) {
const parsedCredentials = z
.object({ email: z.string().email(), password: z.string().min(6) })
.safeParse(credentials)
if (parsedCredentials.success) {
const { email, password } = parsedCredentials.data
const user = await getUser(email)
if (!user) return null
const passwordsMatch = await bcrypt.compare(password, user.password)
if (passwordsMatch) return user
}
console.log('Invalid credentials')
return null
},
}),
],
})
Chapter 16 Adding Metadata
-
metadata
オブジェクトまたはgenerateMetadata
関数をlayout.js
かpage.js
で export する - ファイルベースのメタデータは置くだけでいい
- favicon.ico, apple-icon.jpg, and icon.jpg
- opengraph-image.jpg and twitter-image.jpg
- OGやTwitter向けに動的に生成するには
ImageResponse
を使う - https://nextjs.org/docs/app/api-reference/functions/image-response
- OGやTwitter向けに動的に生成するには
- robots.txt
- sitemap.xml
-
title.template
を使うと、HogePage | Company
のようなタイトルのHogePage
の部分だけページごとに切り替えられる
app/layout.tsx
import { Metadata } from 'next';
export const metadata: Metadata = {
title: {
template: '%s | Acme Dashboard',
default: 'Acme Dashboard',
},
description: 'The official Next.js Learn Dashboard built with App Router.',
metadataBase: new URL('https://next-learn-dashboard.vercel.sh'),
};
app/dashboard/invoices/page.tsx
export const metadata: Metadata = {
title: 'Invoices',
}
Chapter 17 Next Steps
このスクラップは2024/04/14にクローズされました