0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.jsのApp Routerチュートリアルで説明されていること

Posted at

記事概要

以下のチュートリアルの各章の概要と出てくる機能や説明していることのまとめ。App Routerの機能を改めて見つめたい人。

Vercelにデプロイする前提だが、DBをdockerで用意してローカルで行ったのでVercelの登録が面倒な人にも参考になるかも。

1. Getting Started

サンプルアプリをインストールする。特筆すべき点なし。サンプルアプリに機能追加をしていく、というチュートリアル

2. CSS Styling

  • CSSプロパティの設定にtailwindcssを使っている。
    決められた値を組み合わせて簡単にスタイリングできるというライブラリ。
    例えば<div className="flex p-4 md:h-52">とした場合
    • display:flex
    • padding: 1rem
    • ウィンドウサイズがmedium以上の場合はheight: 13rem
      という意味になる。
      ※0.25remが基本単位らしい。一応0.25remが基本単位というのはここが根拠かも?https://tailwindcss.com/docs/theme

--spacing: 0.25rem;
...
--spacing-* Spacing and sizing utilities like px-4, max-h-16, and many more

  • CSSスタイリングの条件分岐はclsx
    statusがXXならYYというスタイルを適用する、というわかりやすいライブラリ。
export default function InvoiceStatus({ status }: { status: string }) {
  return (
    <span
      className={clsx(
        'inline-flex items-center rounded-full px-2 py-1 text-sm',
        {
          'bg-gray-100 text-gray-500': status === 'pending',
          'bg-green-500 text-white': status === 'paid',
        },
      )}
    >

3. Optimizing Fonts and Images

フォントを変えたり画像をレスポンシブにしている。
画像についてはtailwindcssの書き方で、以下はモバイルサイズのウィンドウだとhidden, mediumサイズ以上だとblockとして表示されるように設定している。

      <Image
        src="/hero-desktop.png"
        width={1000}
        height={760}
        className="hidden md:block"
        alt="Screenshots of the dashboard project showing desktop version"
      />

4. Creating Layouts and Pages

App Routerでダミーページを作っておき、layout.tsxでサイドバーを表示するのがメイン。
layout.tsxを配置すると、その配下のpage.tsx全部に影響する。
layout.tsxはchildrenを必須引数としてとり、これは配下のページを指す。

5. Navigating Between Pages

サイドバーにある、各ページへのリンクを表示するNavLinksコンポーネントを作る。
image.png

  • <Link><a>タグと同じだがbackgroundでprefecthしてくれていたり色々やってくれている
  • usePathnameで現在のURLのパスを取得できる
  • サイドバーの現在表示しているページだけハイライトするのもclsxでやっている
import { usePathname } from 'next/navigation';
import clsx from 'clsx';
 
// ...
//※linksはハードコードでした
 
export default function NavLinks() {
  const pathname = usePathname();
 
  return (
    <>
      {links.map((link) => {
        const LinkIcon = link.icon;
        return (
          <Link
            key={link.name}
            href={link.href}
            className={clsx(
              'flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3',
              {
                'bg-sky-100 text-blue-600': pathname === link.href,
              },
            )}
          >
            <LinkIcon className="w-6" />
            <p className="hidden md:block">{link.name}</p>
          </Link>
        );
      })}
    </>
  );
}

6. Setting Up Your Database

DBを使うためにVercelへデプロイしようという内容。
ローカルでやりたかったのでdockerでDBを起動した。

おまけ〜ローカル環境での工夫

  • bcryptは環境の問題で動かなかったのでbycryptjsをインストールしてそれを使った。import文を変えるのみでコードは変えないでも動いた。
  • dockerでpostgres
    .envファイルの書き方がvercelのものしか書いておらず困ったが、以下でいけた。ユーザー名とDB名が'postgres'の場合
POSTGRES_URL="postgres://postgres:password@localhost:5432/postgres"
POSTGRES_PRISMA_URL="postgres://postgres:password@localhost:5432/postgres"
POSTGRES_URL_NON_POOLING="postgres://postgres:password@localhost:5432/postgres"
POSTGRES_USER="postgres"
POSTGRES_HOST="localhost"
POSTGRES_PASSWORD="password"
POSTGRES_DATABASE="postgres"

以降でDB接続する箇所はTLSを使わないので、全て以下のようにsslオプションを削除して使うことになる。

- const sql = postgres(process.env.POSTGRES_URL!, { ssl: 'require' });
+ const sql = postgres(process.env.POSTGRES_URL!);

7. Fetching Data

  • DBからデータをfetchするにはいくつかの方法がある

    1. Route Handlerを使ってAPIを作る。
      • クライアントコードからクエリする場合にはセキュリティ上必要になる。
      • app/api/route.tsexport async function GET(request: Request) {}とかのメソッドを用意するだけ。
      • 実はChapter6でDBにデータをseedするときにさらっと使った。
    2. サーバーコンポーネントを使う。チュートリアルではこの方法。
  • request waterfallsにならないように並列でリクエストしよう
    以下みたいな順次処理をpromise.all()を使って書こう、というような内容。
    しかしこれだと一つのリクエストだけ遅かったら全部処理止まるけどどうする?という疑問提起をして次のChapterに続く。

const revenue = await fetchRevenue();
const latestInvoices = await fetchLatestInvoices(); // wait for fetchRevenue() to finish
const {
  numberOfInvoices,
  numberOfCustomers,
  totalPaidInvoices,
  totalPendingInvoices,
} = await fetchCardData(); // wait for fetchLatestInvoices() to finish

8. Static and Dynamic Rendering

  • Static RenderingとDynamic Rendering
    最近のNext.jsではSSR, SSGという言葉ではなくSSG, ISRあたりをDynamic, SSRをStaticと表現しているらしい。
  • Dynamic Renderingでは遅いリクエストがあると表示が遅い。
    前Chapterからの続き。これをどう解消するか?

9. Streaming

読み込みが完了するまで別のページやコンポーネントを表示することができる機能。

  • page単位ではloading.tsxファイルを用意する
    ページのレンダリングが完了するまではloading.tsxファイルの内容が表示され、レンダリングが終わると自動で本来のページに切り替わる。

loading.tsxは配下のページ全てに影響してしまうため、Route Groupsを使って影響範囲を/dashboard/page.tsxだけに変更する。
以下のように(overview)を作成するとこの部分はアクセス時のパスに影響しないため、/dashboardにアクセスすれば変わらず/dashboard/page.tsxが表示されるまま影響範囲を分離することができる。

 app/
- └── dashboard/
-     ├── page.tsx
-     ├── loading.tsx
      ├── Invoices/
      │   └── page.tsx
      └── Customers/
          └── page.tsx

  app/
+ └── dashboard/
+     ├── (overview)/
+     │   ├── page.tsx
+     │   └── loading.tsx
      ├── Invoices/
      │   └── page.tsx
      └── Customers/
         └── page.tsx

  • コンポーネント単位ではReactの<Suspense>機能を利用する
    時間のかかる処理をコンポーネント(以下ではRevenueChart)内の処理に含め、処理待ち中に表示したいコンポーネントをfallbackに指定する。コンポーネントの処理が終わったら非同期に本来のコンポーネントに切り替わってくれる。
import { Suspense } from 'react';
import { RevenueChartSkeleton } from '@/app/ui/skeletons';

//...
 return (
// ...
        <Suspense fallback={<RevenueChartSkeleton />}>
          <RevenueChart />
        </Suspense>

10. Partial Prerendering

Partial PrerenderingはNext15の時点でもまだexperimentalな機能だが、これからデフォルトのレンダリングモデルになっていくと思っているとのこと。
設定ファイルで有効化するだけで、必要な場合に自動で実施されてくれるようになる。

<Suspense>単体とPartial Prerenderingの違い

<Suspense>をすることでそのコンポーネントだけ非同期的に後から表示させられるが、これは"リクエスト時に"ページの静的な部分のレンダリングが行われている。
一方Partial Prerenderingを有効にすると、<Suspense>で囲ったコンポーネントだけが欠けた(holeとした)静的ページを"ビルド時に"作成するようだ。
イメージ的にはSSR+fallbackか、SSG+fallbackかという感じか。
公式の説明も動画かどこかにあるのかな?という気がするが、誰かが答えてくれているdiscussionがあったのでそちらを貼る。

11. Adding Search and Pagination

検索用のコンポーネントとページネーション用のコンポーネントを作成する。URLのクエリパラメータを使ってページやコンポーネント間でのパラメータをやりとりしているため、そのための手段が多数使われている。

next/navigation

<Link>以外で以下の3つの機能が登場する

  • useSearchParams- 現在のURLからパラメータを取得できる。 /dashboard/invoices?page=1&query=pendingなら{page: '1', query: 'pending'}
  • usePathname - 現在のURLからパスが取得できる。 /dashboard/invoicesなら /dashboard/invoices
  • useRouter - URL操作を行うメソッドたちを返す。今回は遷移するreplaceしか使わないが、pushback, refreshなども使える。

Debounce

以下だと入力するたびにハンドラが走って鬱陶しい。これを一定期間止まった場合にのみイベントが走るようにすること。

  onChange={(e) => {
    handleSearch(e.target.value);
  }}

ライブラリを使うと簡単。
具体的なコードは次のSearchコンポーネントを参照。

コード例〜Searchコンポーネント

next/navigationのメソッドたちとDebounceはSearchコンポーネントで使われている。

// 入力された値termをクエリパラメータに足したURLに遷移する
'use client';
 
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import { useSearchParams, usePathname, useRouter } from 'next/navigation';
import { useDebouncedCallback } from 'use-debounce';
 
export default function Search() {
  const searchParams = useSearchParams();
  const pathname = usePathname();
  const { replace } = useRouter();

 // 0.3秒入力がない場合にのみイベントが発生するようにdebounceするライブラリでラップ
  const handleSearch = useDebouncedCallback((term) => {
    const params = new URLSearchParams(searchParams);
    if (term) {
      params.set('query', term);
    } else {
      params.delete('query');
    }
    replace(`${pathname}?${params.toString()}`);
  }, 300);

  .....

{/* URLとsyncするようにdefaultValueを設定 */}
  <input
  className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
  placeholder={placeholder}
  onChange={(e) => {
    handleSearch(e.target.value);
  }}
  defaultValue={searchParams.get('query')?.toString()}
/>

...
}

searchParams

Page.tsxの予約されたパラメータとしてsearchParamsがある

/dashboard/invoices/page.tsxで以下のように使われ、URLパラメータを取得してTableコンポーネントに渡している。

export default async function Page(props: {
  searchParams?: Promise<{
    query?: string;
    page?: string;
  }>;
}) {
  const searchParams = await props.searchParams;
  const query = searchParams?.query || '';
  const currentPage = Number(searchParams?.page) || 1;
...

      <Suspense key={query + currentPage} fallback={<InvoicesTableSkeleton />}>
        <Table query={query} currentPage={currentPage} />
      </Suspense>
...

searchParamsとuseSearchParamsの違い

先述のSearchコンポーネントではnext/navigationのuseSearchParamsを使ってURLパラメータを取得した。
クライアントコンポーネントだとフックを使ってuseSearchParamsを使い、サーバーコンポーネントの場合はsearchParamsを使うようだ。

<Search> is a Client Component, so you used the useSearchParams() hook to access the params from the client.
<Table> is a Server Component that fetches its own data, so you can pass the searchParams prop from the page to the component.
As a general rule, if you want to read the params from the client, use the useSearchParams() hook as this avoids having to go back to the server.

Pagenation

完成系ではないが、useSearchParamsから現在のページを取得したらあとは以下のように地道に計算したり分岐して表示している。
https://github.com/vercel/next-learn/blob/main/dashboard/starter-example/app/ui/invoices/pagination.tsx

12. Mutating Data

Server Actions

formから直接指定できてサーバーサイドで実行されるメソッド。

// Server Component
export default function Page() {
  // Action
  async function create(formData: FormData) {
    'use server';
 
    // Logic to mutate data...
  }
 
  // Invoke the action using the "action" attribute
  return <form action={create}>...</form>;
}

メリットの一つとしてprogressive enhancementが挙げられている。
JavaScriptをクライアント側で読み込む必要がないため、formを含んだhtmlだけ最低限の機能としてまず提供できる、ということのよう。

An advantage of invoking a Server Action within a Server Component is progressive enhancement - forms work even if JavaScript has not yet loaded on the client. For example, without slower internet connections.

なお、POSTエンドポイントが自動で作られてくれているため、自分でAPIエンドポイントを作る必要もない。

Behind the scenes, Server Actions create a POST API endpoint

型チェックライブラリ zod

コンパイル時のみではなく、実行時にも型チェックが行える。外部APIを呼び出す時とかは一応やっておくとよさそう。

'use server';
 
import { z } from 'zod';
 
const FormSchema = z.object({
  id: z.string(),
  customerId: z.string(),
  amount: z.coerce.number(),
  status: z.enum(['pending', 'paid']),
  date: z.string(),
});
 
const CreateInvoice = FormSchema.omit({ id: true, date: true });
 
export async function createInvoice(formData: FormData) {
  const { customerId, amount, status } = CreateInvoice.parse({
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  });
  // ...
}

あとは動的ルート(/dashboard/invoices/[id]/edit/page.tsxみたいなパス)と、データ更新したらキャッシュをrevalidateしましょう、ということを言っていた。

bind

Server Actionにform以外の変数を渡したいときはbindする。
/app/ui/invoices/edit-form.tsxで、formの入力値にないidをupdateInvoiceアクションに渡す場合には以下のように書く。

  import { updateInvoice } from '@/app/lib/actions';
  
  const updateInvoiceWithId = updateInvoice.bind(null, invoice.id);
  return <form action={updateInvoiceWithId}>{/* ... */}</form>;

/app/lib/actions.tsのServer Actionでは以下のように受け取れる。

export async function updateInvoice(id: string, formData: FormData) {
...

13. Handling Errors

error.tsx: Server Actionでuncaught exceptionが発生した場合に備えて、エラーページを用意できる。
not-found.tsx: 404エラーを出したいタイミングでnotFound();すると表示されるページ

14. Improving Accessibility

アクセシビリティの話。
適切なHTMLタグを使ったり、altタグによる説明をしたりするとSEO上がったり、スクリーンリーダーで(画像が見えないユーザーに対しても)読み上げてもらえる、と言った点を説明している。
そこから入力値のvalidationやErrorなど、ユーザー体験的な話になっていく。

  • eslint-plugin-jsx-a11yというプラグインを使うとアクセシビリティの観点からlintしてくれる(適切なaltプロパティがない、とか)。
  • Server Actionでzodの型チェックのエラー時にカスタムメッセージを追加
  • useActionStateを使ったエラーメッセージの表示。
    useActionStateを使うと、state, isLoading(いずれも変数名は任意)を管理できる。クライアントコンポーネント側で以下のようなことが手軽に行える新しい機能のよう。
    • Server Actionの結果(成功/失敗)をstateにすることでエラーハンドリング(stateで条件分岐してエラーメッセージの表示)
    • (チュートリアルでは使っていないが)処理中であることを示すisLoadingによって表示コンテンツを変える
      useActionState公式の例がわかりやすいかも。

15. Adding Authentication

  • next-authライブラリを使ったログインフォーム作成

  • middleware.tsで定義したパスにアクセスしたときにauth.config.tsのcallbacksで定義した認可チェックが行われ、未ログインだった場合はauth.config.tsのpagesで定義した/loginページにリダイレクトする。

  • next-authではsignOut, SignInなどはuse serverで行う必要がある。クライアント側で使用したい場合はnext-auth/reactを使う必要がある
    ただし、ログインなどの処理はサーバー側でやる方がセキュアで推奨と公式に書いてある。
    https://nextjs.org/docs/app/guides/authentication#auth-js-server-actions

Since Server Actions always execute on the server, they provide a secure environment for handling authentication logic.

use clientとuse server

use clientとuse serverは対になるものではない。

use client: クライアントで動作する
use server: サーバーサイドの関数をクライアントコードから呼び出せるようにマークする。server actionsを作るときによく使う。
無印: サーバーで動作するコンポーネントになる。use serverとは違う

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?