Closed16

Next.js Tutorial

HiHi

Chapter 2 CSS Styling

  • Global styles
  • Tailwind
  • CSS Modules
  • clsx
    • conditional style

の紹介

HiHi

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/imageImageコンポーネントがやってくれる最適化
    • 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"
/>
HiHi

Chapter 6 Setting Up Your Database

  • @vercel/postgresの宣伝。
HiHi

Chapter 7 Fetching Data

  • Asyncコンポーネントにできる。

Chapter 8 Static and Dynamic Rendering

  • 特になし
HiHi

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を使いましょう。
HiHi

Chapter 11 Adding Search and Pagination

https://nextjs.org/learn/dashboard-app/adding-search-and-pagination

  • ページコンポーネントは{ params: {}, searchParams: {} }のようなpropsを受け取る
  • サーバーコンポーネントは基本 static renderingだが、中で dynamic function を使うと自動的に dynamic rendering に変わる
  • クライアントコンポーネント('use client')の場合はuseSearchParams()で動的にsearchParamsを取得する
HiHi

Chapter 12 Mutating Data

  • <form action={serverAction}>
    • フォームの内容をサーバーで処理するために'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/cacherevalidatePath('/dashboard/invoices')でサーバーのキャッシュを破棄する
  • next/navigationredirect('/dashboard/invoices')でリダイレクト
HiHi

Chapter 13 Handling Errors

  • error.tsxはそのルートのすべてのエラーをキャッチする
  • not-found.tsxnext/navigationnotFound()が呼ばれたときに表示される
HiHi

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などのフォーム以外の要素が必要な場合は、prevStateformDataの前に受け取るようにして引数をbindするといい
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)

  // ...
}
HiHi

Chapter 15 Adding Authentication

  • NextAuth.jsを使ったAuthentication

  • secret key の生成

    • openssl rand -base64 32
    • .envAUTH_{PROVIDER}_{ID|SECRET}={SECRET_KEY}を置くと勝手に読まれる
  • auth.config.ts

    • pagesにはsignIn, signOut, error, verifyRequest, newUserのための自前のページのパスを指定する
      • 指定しないとNextAuth.jsのデフォルトのページが開く
    • callbacksauthorizedでは認証後のリダイレクト先を決定する
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;
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
      },
    }),
  ],
})
HiHi

Chapter 16 Adding Metadata

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',
}
このスクラップは16日前にクローズされました