深夜のオフィス、締め切りに追われる開発者の姿。画面には Next.js のコードが映り、状態管理のライブラリの選択に頭を抱えています。「もっと効率的な方法があるはずだ...」そんな思いを抱えながら、あなたも同じような悩みを抱えていませんか?
ウェブ開発の世界は日々進化し、新しい技術やアプローチが次々と登場します。その中でも、Next.js の App Router と効果的な状態管理の組み合わせは、多くの開発者が直面する課題となっています。パフォーマンスを犠牲にせず、かつ開発効率を向上させる - この一見相反する要求をどのように満たせばよいのでしょうか?
本記事では、Next.js の App Router を活用した最新の状態管理アプローチについて、詳細かつ実践的な情報をお届けします。React の新機能を最大限に活用し、サーバーサイドレンダリングとクライアントサイドの状態を巧みに操る技術、そしてパフォーマンスと開発体験を両立させるベストプラクティスをご紹介します。
この記事を読むことで、あなたは Next.js アプリケーションの構造を最適化し、効率的な状態管理を実現するための具体的な方法を学ぶことができます。結果として、より高速で保守性の高いアプリケーションを構築し、ユーザー体験と開発者体験の両方を向上させることができるでしょう。
さあ、Next.js App Router と状態管理の新たな地平線へ、一緒に踏み出しましょう!
はじめに:Next.js App Router の革新
Next.js 13で導入された App Router は、React アプリケーションの構造と状態管理に革命をもたらしました。従来の pages ディレクトリベースのルーティングから、より直感的で柔軟な app ディレクトリベースのアプローチへの移行は、単なる構造の変更以上の意味を持ちます。
App Router の主な特徴は以下の通りです:
- ネストされたルーティング:URL 構造とファイルシステムの一致
- レイアウトの共有:共通のUIを効率的に管理
- サーバーコンポーネントのデフォルトサポート:パフォーマンスの向上
- ストリーミング:ユーザー体験の改善
- ローディングとエラー状態の改善:UI/UXの向上
これらの特徴は、アプリケーションの構造だけでなく、状態管理のアプローチにも大きな影響を与えています。特に、サーバーコンポーネントとクライアントコンポーネントの使い分けは、状態管理の戦略に新たな視点をもたらしました。
サーバーコンポーネントとクライアントコンポーネント
App Router の重要な概念の一つが、サーバーコンポーネントとクライアントコンポーネントの区別です。この区別は、状態管理の方法に直接影響を与えます。
サーバーコンポーネント
サーバーコンポーネントは、サーバー上でレンダリングされ、クライアントに送信されるHTMLの一部となります。
特徴:
- データベースやファイルシステムへの直接アクセスが可能
- クライアントサイドのJavaScriptバンドルサイズを削減
- 静的な内容や、サーバーサイドでのみ必要な処理に最適
// app/ServerComponent.js
import { readFile } from 'fs/promises';
export default async function ServerComponent() {
const content = await readFile('./some-file.txt', 'utf8');
return <div>{content}</div>;
}
クライアントコンポーネント
クライアントコンポーネントは、ブラウザ上で動作し、インタラクティブな機能を提供します。
特徴:
- ブラウザAPIや、useStateなどのReactフックを使用可能
- イベントリスナーの追加やユーザー入力の処理が可能
- 動的な UI 要素の実装に使用
'use client';
// app/ClientComponent.js
import { useState } from 'react';
export default function ClientComponent() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
この2つのコンポーネントタイプの適切な使い分けが、効果的な状態管理の鍵となります。
Next.js App Router における状態管理の基本戦略
App Router を使用する Next.js アプリケーションでの状態管理は、従来の SPA (Single Page Application) とは異なるアプローチが必要です。以下に、基本的な戦略をいくつか紹介します。
1. サーバーサイドの状態管理
サーバーコンポーネントを活用することで、多くの状態をサーバーサイドで管理することができます。これにより、クライアントサイドの状態管理の複雑さを軽減できます。
例えば、データベースから取得したユーザー情報を表示する場合:
// app/users/page.js
import { getUsersFromDB } from '../lib/db';
export default async function UsersPage() {
const users = await getUsersFromDB();
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
この例では、ユーザーデータの取得と表示がすべてサーバーサイドで行われ、クライアントに送信されるのは最終的なHTMLのみです。
2. クライアントサイドの状態管理
インタラクティブな要素や、ユーザー入力に基づいて変化する状態については、クライアントコンポーネントを使用します。
'use client';
// app/components/Counter.js
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
この Counter コンポーネントは、クライアントサイドで状態を管理し、ユーザーのクリックに応じて更新します。
3. サーバーとクライアントの状態の連携
時には、サーバーサイドで取得したデータをクライアントサイドで操作する必要がある場合があります。この場合、サーバーコンポーネントからクライアントコンポーネントにプロップスとしてデータを渡す方法が効果的です。
// app/users/page.js
import { getUsersFromDB } from '../lib/db';
import UserList from './UserList';
export default async function UsersPage() {
const users = await getUsersFromDB();
return <UserList initialUsers={users} />;
}
// app/users/UserList.js
'use client';
import { useState } from 'react';
export default function UserList({ initialUsers }) {
const [users, setUsers] = useState(initialUsers);
<em>// ユーザーリストを操作するロジックをここに実装</em>
return (
<em>// ユーザーリストの表示と操作UIをここに実装</em>
);
}
この例では、サーバーサイドで取得したユーザーデータを、クライアントコンポーネントの初期状態として使用しています。
高度な状態管理テクニック
基本的な戦略を押さえたところで、より高度な状態管理テクニックを見ていきましょう。これらのテクニックを適切に使用することで、アプリケーションのパフォーマンスと開発効率を大幅に向上させることができます。
1. useReducer フックの活用
複雑な状態ロジックを持つコンポーネントでは、useState
の代わりに useReducer
を使用することで、状態更新ロジックをより整理された形で管理できます。
'use client';
// app/components/TodoList.js
import { useReducer } from 'react';
const initialState = { todos: [] };
function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return { todos: [...state.todos, action.payload] };
case 'REMOVE_TODO':
return { todos: state.todos.filter(todo => todo.id !== action.payload) };
default:
return state;
}
}
export default function TodoList() {
const [state, dispatch] = useReducer(todoReducer, initialState);
const addTodo = (text) => {
dispatch({ type: 'ADD_TODO', payload: { id: Date.now(), text } });
};
const removeTodo = (id) => {
dispatch({ type: 'REMOVE_TODO', payload: id });
};
return (
<div>
{/* TodoリストのUI実装 */}
</div>
);
}
useReducer
を使用することで、状態更新ロジックを一箇所にまとめ、コンポーネントのメインロジックから分離することができます。これにより、コードの可読性と保守性が向上します。
2. コンテキストAPIの効果的な使用
アプリケーション全体で共有する必要のある状態がある場合、React の Context API を使用することで、プロップスのバケツリレーを避けることができます。
'use client';
// app/contexts/ThemeContext.js
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
return useContext(ThemeContext);
}
// app/layout.js
import { ThemeProvider } from './contexts/ThemeContext';
export default function RootLayout({ children }) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
);
}
// app/components/ThemeToggle.js
'use client';
import { useTheme } from '../contexts/ThemeContext';
export default function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
);
}
この例では、テーマの状態をアプリケーション全体で共有し、任意のコンポーネントから簡単にアクセスおよび更新できるようにしています。
3. サーバーアクションの活用
Next.js 13.4以降では、サーバーアクションを使用して、クライアントコンポーネントからサーバーサイドの関数を直接呼び出すことができます。これにより、状態更新とサーバーサイドの処理を密接に連携させることが可能になります。
// app/actions.js
'use server';
import { revalidatePath } from 'next/cache';
export async function addTodo(formData) {
const todo = formData.get('todo');
<em>// データベースに新しいTodoを追加する処理</em>
await db.todos.create({ data: { text: todo } });
revalidatePath('/todos');
}
// app/components/TodoForm.js
'use client';
import { addTodo } from '../actions';
export default function TodoForm() {
return (
<form action={addTodo}>
<input type="text" name="todo" required />
<button type="submit">Add Todo</button>
</form>
);
}
この例では、addTodo
サーバーアクションを使用して、フォーム送信時にサーバーサイドでデータを処理し、関連するページを再検証しています。これにより、クライアントサイドの状態管理とサーバーサイドの処理をシームレスに統合できます。
4. SWRを使用したデータフェッチングと状態管理
SWR (stale-while-revalidate) は、Vercel社が開発した React 用のデータフェッチングライブラリです。Next.js と組み合わせることで、効率的なデータ取得と状態管理を実現できます。
SWRの主な特徴:
- キャッシュからのデータ即時提供
- バックグラウンドでの再検証
- インターバル再フェッチ
- フォーカス時の再検証
- ページ遷移時の自動再フェッチ
Next.js の App Router と SWR を組み合わせる例を見てみましょう:
'use client';
// app/components/UserProfile.js
import useSWR from 'swr';
const fetcher = (...args) => fetch(...args).then(res => res.json());
export default function UserProfile({ userId }) {
const { data, error, isLoading } = useSWR(`/api/user/${userId}`, fetcher);
if (error) return <div>Failed to load user data</div>;
if (isLoading) return <div>Loading...</div>;
return (
<div>
<h1>{data.name}</h1>
<p>Email: {data.email}</p>
</div>
);
}
// app/[userId]/page.js
import UserProfile from '../components/UserProfile';
export default function UserPage({ params }) {
return <UserProfile userId={params.userId} />;
}
この例では、UserProfile
コンポーネントが SWR を使用してユーザーデータをフェッチしています。SWR は自動的にデータのキャッシュと再検証を行い、コンポーネントの状態管理を簡素化します。
SWR を使用することの利点:
- コード量の削減:データフェッチングと状態管理のボイラープレートコードが大幅に減少します。
- パフォーマンスの向上:キャッシュを活用することで、アプリケーションの応答性が向上します。
- リアルタイム性:自動再検証により、データの鮮度を保ちつつ、ユーザー体験を損なわないリアルタイム更新が可能です。
- エラー処理の簡素化:ネットワークエラーやローディング状態の扱いが容易になります。
5. Zustand を用いた軽量な状態管理
複雑な状態管理が必要な場合、Redux のような大規模なライブラリの代わりに、Zustand のような軽量な状態管理ライブラリを使用することができます。Zustand は、シンプルな API と優れたパフォーマンスを提供し、Next.js の App Router と相性が良いです。
Zustand の基本的な使用例:
// app/store.js
import { create } from 'zustand';
const useStore = create((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
}));
export default useStore;
// app/components/BearCounter.js
'use client';
import useStore from '../store';
export default function BearCounter() {
const bears = useStore((state) => state.bears);
const increasePopulation = useStore((state) => state.increasePopulation);
return (
<div>
<h1>{bears} around here...</h1>
<button onClick={increasePopulation}>Add a bear</button>
</div>
);
}
Zustand を使用する利点:
- シンプルな API:学習コストが低く、短時間で習得できます。
- ボイラープレートの削減:Redux と比較して、必要なコード量が大幅に減少します。
- パフォーマンス:内部で最適化されており、不要な再レンダリングを防ぎます。
- TypeScript サポート:型安全な状態管理が可能です。
- ミドルウェアサポート:必要に応じて機能を拡張できます。
6. Jotai による原子的状態管理
Jotai は、React 用の原子的な状態管理ライブラリです。小さな単位(原子)で状態を管理することで、柔軟でスケーラブルな状態管理を実現します。Next.js の App Router と組み合わせることで、効率的な状態管理が可能になります。
Jotai の基本的な使用例:
// app/atoms.js
import { atom } from 'jotai';
export const textAtom = atom('hello');
export const uppercaseAtom = atom(
(get) => get(textAtom).toUpperCase()
);
// app/components/TextInput.js
'use client';
import { useAtom } from 'jotai';
import { textAtom, uppercaseAtom } from '../atoms';
export default function TextInput() {
const [text, setText] = useAtom(textAtom);
const [uppercase] = useAtom(uppercaseAtom);
return (
<div>
<input
value={text}
onChange={(e) => setText(e.target.value)}
/>
<p>Uppercase: {uppercase}</p>
</div>
);
}
Jotai を使用する利点:
- 細粒度の状態管理:必要な部分だけを更新できるため、パフォーマンスが向上します。
- コンポーネント間の状態共有が容易:Providerの設定が不要で、直感的に使用できます。
- 導入の容易さ:最小限のセットアップで使い始めることができます。
- 非同期サポート:非同期操作を簡単に扱えます。
- デバッグのしやすさ:Redux DevTools との互換性があります。
パフォーマンス最適化と状態管理
Next.js の App Router を使用する際、効果的な状態管理はパフォーマンス最適化の重要な要素となります。以下に、パフォーマンスを考慮した状態管理のベストプラクティスをいくつか紹介します。
サーバーコンポーネントの活用: 可能な限りサーバーコンポーネントを使用し、クライアントサイドの状態管理を最小限に抑えます。これにより、初期ページ読み込み時間が短縮され、SEOも向上します。
状態の分割: 大規模なグローバル状態を避け、必要な部分だけを管理する「原子的」なアプローチを採用します。これにより、不要な再レンダリングを防ぎ、アプリケーションの応答性が向上します。
メモ化の活用: useMemo
や useCallback
を適切に使用して、計算コストの高い処理や子コンポーネントへの不要な再レンダリングを防ぎます。
import { useMemo, useCallback } from 'react';
function ExpensiveComponent({ data, onItemClick }) {
const processedData = useMemo(() => expensiveCalculation(data), [data]);
const handleClick = useCallback((item) => onItemClick(item), [onItemClick]);
return (
// コンポーネントの実装
);
}
遅延読み込みと動的インポート: 大規模なコンポーネントや状態管理ロジックを必要に応じて動的にインポートすることで、初期バンドルサイズを削減し、ページの読み込み速度を向上させます。
import dynamic from 'next/dynamic';
const DynamicComponent = dynamic(() => import('../components/HeavyComponent'), {
loading: () => <p>Loading...</p>
});
状態の永続化: 必要に応じて、ローカルストレージやセッションストレージを活用して状態を永続化します。これにより、ページ再読み込み時の状態復元が高速化され、ユーザー体験が向上します。
import { useEffect } from 'react';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
const useStore = create(
persist(
(set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
}),
{ name: 'bear-storage' }
)
);
function BearCounter() {
const bears = useStore((state) => state.bears);
const increasePopulation = useStore((state) => state.increasePopulation);
useEffect(() => {
// ストアが準備されたら何かする
}, []);
return (
// コンポーネントの実装
);
}
サーバーサイドレンダリング(SSR)とスタティックサイトジェネレーション(SSG)の活用: 可能な限り、SSRやSSGを活用してページの初期状態を生成します。これにより、クライアントサイドでの状態管理の負荷を軽減し、初期読み込み時間を短縮できます。
// app/page.js
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
export default async function Post({ params }) {
const post = await getPostBySlug(params.slug);
return <PostContent post={post} />;
}
これらの最適化技術を適切に組み合わせることで、Next.js App Router を使用したアプリケーションのパフォーマンスを大幅に向上させることができます。
状態管理のベストプラクティスとパターン
Next.js の App Router を使用する際の状態管理に関するベストプラクティスとパターンをいくつか紹介します。これらのアプローチを適切に組み合わせることで、メンテナンス性が高く、パフォーマンスの良いアプリケーションを構築できます。
状態の分離と責務の明確化: グローバル状態、ページレベルの状態、コンポーネントレベルの状態を明確に分離します。これにより、コードの可読性が向上し、バグの発生を減らすことができます。
// グローバル状態 (例: Zustandを使用)
const useGlobalStore = create((set) => ({
user: null,
setUser: (user) => set({ user }),
}));
// ページレベルの状態
function ProductsPage() {
const [selectedCategory, setSelectedCategory] = useState(null);
// ...
}
// コンポーネントレベルの状態
function ProductItem({ product }) {
const [isExpanded, setIsExpanded] = useState(false);
// ...
}
単一の信頼できる情報源(Single Source of Truth): 特定の状態に対して、単一の信頼できる情報源を維持します。これにより、状態の一貫性が保たれ、デバッグが容易になります。
// 正しいアプローチ
const [count, setCount] = useState(0);
// 避けるべきアプローチ
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
// count1とcount2が同期しない可能性がある
状態更新の一元化: 状態の更新ロジックを一箇所にまとめることで、予測可能性が向上し、バグの発生を防ぐことができます。
function useCounter() {
const [count, setCount] = useState(0);
const increment = useCallback(() => setCount(c => c + 1), []);
const decrement = useCallback(() => setCount(c => c - 1), []);
const reset = useCallback(() => setCount(0), []);
return { count, increment, decrement, reset };
}
不変性の維持: 状態を更新する際は、常に新しいオブジェクトや配列を作成し、直接的な変更を避けます。これにより、予期せぬ副作用を防ぎ、パフォーマンスの最適化が容易になります。
// 正しいアプローチ
setTodos(prevTodos => [...prevTodos, newTodo]);
// 避けるべきアプローチ
setTodos(prevTodos => {
prevTodos.push(newTodo);
return prevTodos;
});
コンポーネントの純粋性の維持: 可能な限り、コンポーネントを純粋な関数として実装します。これにより、テストが容易になり、予測可能性が向上します。
// 純粋なコンポーネント
function PureComponent({ data, onAction }) {
return (
<div>
<h1>{data.title}</h1>
<button onClick={onAction}>Action</button>
</div>
);
}
エラー状態の適切な管理: データフェッチングやユーザー操作に関連するエラー状態を適切に管理することで、ユーザー体験を向上させ、デバッグを容易にします。
function DataFetchingComponent() {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) throw new Error('Network response was not ok');
const result = await response.json();
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
};
fetchData();
}, []);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!data) return <div>No data available</div>;
return <div>{/* データを表示 */}</div>;
}
コンポーザブルな状態管理: 複雑な状態ロジックを小さな、再利用可能な単位に分割することで、コードの再利用性と保守性を向上させます。
function usePagination(totalItems, itemsPerPage) {
const [currentPage, setCurrentPage] = useState(1);
const totalPages = Math.ceil(totalItems / itemsPerPage);
const nextPage = useCallback(() => {
setCurrentPage(page => Math.min(page + 1, totalPages));
}, [totalPages]);
const prevPage = useCallback(() => {
setCurrentPage(page => Math.max(page - 1, 1));
}, []);
return { currentPage, nextPage, prevPage, totalPages };
}
function useItemSelection(items) {
const [selectedItems, setSelectedItems] = useState([]);
const toggleSelection = useCallback((item) => {
setSelectedItems(prev =>
prev.includes(item)
? prev.filter(i => i !== item)
: [...prev, item]
);
}, []);
return { selectedItems, toggleSelection };
}
function ItemList({ items }) {
const { currentPage, nextPage, prevPage, totalPages } = usePagination(items.length, 10);
const { selectedItems, toggleSelection } = useItemSelection(items);
// これらのカスタムフックを組み合わせて使用
}
状態の正規化: 特に複雑なデータ構造を扱う場合、状態を正規化することで、更新の効率化とデータの一貫性を保つことができます。
// 正規化された状態の例
const initialState = {
users: {
byId: {
'1': { id: '1', name: 'Alice' },
'2': { id: '2', name: 'Bob' },
},
allIds: ['1', '2'],
},
posts: {
byId: {
'101': { id: '101', title: 'Hello World', authorId: '1' },
'102': { id: '102', title: 'React is awesome', authorId: '2' },
},
allIds: ['101', '102'],
},
};
非同期状態管理: 非同期操作(データフェッチング、APIコール等)の状態を適切に管理することで、ユーザー体験を向上させ、エラーハンドリングを容易にします。
function useAsyncOperation(asyncFunc) {
const [state, setState] = useState({
status: 'idle',
data: null,
error: null,
});
const execute = useCallback(async (...args) => {
setState({ status: 'pending', data: null, error: null });
try {
const data = await asyncFunc(...args);
setState({ status: 'resolved', data, error: null });
} catch (error) {
setState({ status: 'rejected', data: null, error });
}
}, [asyncFunc]);
return { ...state, execute };
}
// 使用例
function UserProfile({ userId }) {
const { status, data: user, error, execute } = useAsyncOperation(fetchUser);
useEffect(() => {
execute(userId);
}, [userId, execute]);
if (status === 'pending') return <div>Loading...</div>;
if (status === 'rejected') return <div>Error: {error.message}</div>;
if (!user) return <div>No user data</div>;
return <div>{user.name}</div>;
}
状態の永続化と再水和: アプリケーションの状態を適切に永続化し、ページのリロード時に再水和することで、ユーザー体験を向上させることができます。
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
const useStore = create(
persist(
(set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
}),
{
name: 'bear-storage', // ユニークな名前
getStorage: () => localStorage, // 使用するストレージ
}
)
);
function App() {
const bears = useStore((state) => state.bears);
const increasePopulation = useStore((state) => state.increasePopulation);
return (
<div>
<h1>{bears} around here...</h1>
<button onClick={increasePopulation}>Add a bear</button>
</div>
);
}
これらのベストプラクティスとパターンを適切に組み合わせることで、Next.js App Router を使用したアプリケーションの状態管理を効果的に行うことができます。ただし、各アプリケーションの要件や規模に応じて、最適なアプローチは異なる場合があるため、常にトレードオフを考慮しながら設計を行うことが重要です。
結論:Next.js App Router と状態管理の未来
Next.js の App Router は、React アプリケーションの構築方法に革命をもたらしました。そして、それに伴い状態管理のアプローチも進化しています。以下に、Next.js App Router を使用する際の状態管理に関する重要なポイントをまとめます:
- サーバーコンポーネントとクライアントコンポーネントの適切な使い分け
- サーバーサイドの状態管理の活用
- 軽量な状態管理ライブラリ(Zustand, Jotai など)の採用
- SWR や React Query などのデータフェッチングライブラリの活用
- パフォーマンスを考慮した状態設計と最適化
- コンポーザブルな状態管理アプローチの採用
これらのアプローチを適切に組み合わせることで、高性能で保守性の高いアプリケーションを構築することができます。
今後、Next.js と React のエコシステムがさらに進化するにつれ、状態管理のベストプラクティスも変化していく可能性があります。開発者は常に最新のトレンドやツールに注目し、自身のプロジェクトに最適なアプローチを選択することが重要です。
最後に、状態管理は単なる技術的な課題ではなく、アプリケーションの設計哲学にも深く関わる問題です。シンプルさと柔軟性のバランス、パフォーマンスと開発効率のトレードオフを常に意識しながら、プロジェクトの要件に最適な解決策を見出していくことが、成功するwebアプリケーション開発の鍵となるでしょう。
参考リンク
これらのリソースを参考にしながら、自身のプロジェクトに最適な状態管理戦略を構築していってください。Next.js App Router と最新の状態管理アプローチを組み合わせることで、より効率的で保守性の高いwebアプリケーションの開発が可能になるはずです。
Happy coding
コメント