🎉

Next.js: 細かいUI/UX 向上テクニックのハッカソンメモ

に公開

はじめに

こないだ参加したハッカソンで得た知見の備忘録です。

グローバルな UI フィードバック (nextjs-toploader)

ページ遷移やrouter.refresh()実行時に、画面上部にプログレスバーを表示してロード中であることをユーザーにフィードバックする。

Provider などを用いて children を無闇にクライアントコンポーネント化することなく導入できるnextjs-toploaderが有効。

// layout.tsx
import NextTopLoader from "nextjs-toploader";

export default function MainLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <>
      <NextTopLoader color="#EBC2AD" showSpinner={false} />
      <Header />
      <main className="pt-16">{children}</main>
    </>
  );
}

グローバルな状態管理とデータ同期 (Zustand)

コンポーネント間で状態を共有し、UI の一貫性を保つために Zustand を導入。
離れたコンポーネント間で情報を同期させるケースで有効。

ストアの作成 (stores/userStore.ts)

import { create } from "zustand";

export const useUserStore = create<UserState>((set) => ({
  user: null,
  isLoading: true,
  fetchUser: async () => {
    try {
      const res = await fetch("/api/me");
      if (res.ok) {
        const data = await res.json();
        set({ user: data, isLoading: false });
      } else {
        set({ user: null, isLoading: false });
      }
    } catch (e) {
      set({ user: null, isLoading: false });
    }
  },
}));

コンポーネントからの呼び出し

// Client Component内
import { useUserStore } from "@/stores/userStore";

const { user, fetchUser } = useUserStore();

同期的な UI 更新とトランジション (useTransition)

Promise を返さない非同期処理と、それに続く状態更新を、ユーザー体験上は同時に発生しているように見せるためにuseTransitionを使用する。

startTransitionでラップされた処理は一つのまとまり(トランジション)として管理される。React は、その中で最も時間のかかる処理が完了するのを待ってから、すべての状態更新を一度に画面に反映する。

// Client Component内
const [isPending, startTransition] = useTransition();

const handleUpdate = () => {
  // ... APIリクエストが成功した後 ...

  startTransition(() => {
    router.refresh(); // 時間のかかるUI更新
    setOpen(false); // 即時実行される状態更新
  });

  // ユーザーには、refreshが完了した後にモーダルが閉じたように見える
};
KA projects

Discussion