В цьому всебічному посібнику ми створимо блог-додаток, використовуючи Next.js 13+ App Router, TanStack Query (раніше React Query) та REST API. Ми охопимо все — від налаштування до передових шаблонів, створюючи повністю функціональний блог з постами та коментарями.
Зміст
- Налаштування проєкту
- Налаштування REST API
- Конфігурація TanStack Query
- Створення основних функцій
- Реалізація передових шаблонів
- Кращі практики та оптимізація
1.
Налаштування проєкту
1.1 Створення нового проєкту на Next.js
npx create-next-app@latest my-blog-app
cd my-blog-app
Виберіть наступні опції:
- ✔ Чи хочете ви використовувати TypeScript? Так
- ✔ Чи хочете ви використовувати ESLint? Так
- ✔ Чи хочете ви використовувати Tailwind CSS? Так
- ✔ Чи хочете ви використовувати директорію
src/
? Ні - ✔ Чи хочете ви використовувати App Router? Так
- ✔ Чи хочете ви налаштувати стандартний псевдонім імпорту? Ні
Вибір за вами, залежно від ваших вподобань.
1.2 Встановлення залежностей
npm install @tanstack/react-query @tanstack/react-query-devtools
2.
Налаштування REST API
2.1 Створення структури бази даних
Спочатку створіть файл data/db.json
, який слугуватиме нашою тимчасовою базою даних:
{
"posts": [
{
"id": 1,
"title": "Перший пост",
"body": "Це зміст першого поста",
"userId": 1
}
],
"comments": [
{
"id": 1,
"postId": 1,
"name": "Джон Доу",
"email": "[email protected]",
"body": "Чудовий пост!"
}
]
}
Ви можете використовувати будь-яку базу даних на ваш вибір: SQL або NoSQL, з ORM, таким як Prisma, Drizzle і так далі.
2.2 Реалізація утиліт для роботи з базою даних
Створіть файл lib/db.ts
для роботи з файлами:
import { promises as fs } from 'fs';
import path from 'path';
export async function readDB() {
const filePath = path.join(process.cwd(), 'data', 'db.json');
const data = await fs.readFile(filePath, 'utf8');
return JSON.parse(data);
}export async function writeDB(data: any) {
const filePath = path.join(process.cwd(), 'data', 'db.json');
await fs.writeFile(filePath, JSON.stringify(data, null, 2));
}
Ви можете використовувати операції з базою даних, такі як .create(), .select() або будь-які інші залежно від бази даних, яку ви використовуєте.
3.
Налаштування TanStack Query
3.1 Створення провайдера запитів
Створіть файл app/providers.tsx
:
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { useState } from 'react';
export default function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
refetchOnWindowFocus: false,
},
},
})
);
return (
{children}
);
}
3.2 Додавання провайдера до Layout
Оновіть файл app/layout.tsx
:
import Providers from './providers';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
{children}
);
}
4.
Створення основних функцій
4.1 Створення API клієнта
Створіть файл lib/api.ts
:
const BASE_URL = '/api';
export async function fetchPosts(page = 1, limit = 10) {
const response = await fetch(
`${BASE_URL}/posts?_page=${page}&_limit=${limit}`
);
if (!response.ok) throw new Error('Не вдалося отримати пости');
return {
posts: await response.json(),
hasMore: response.headers.get('x-total-count') > page * limit,
};
}
export async function createPost(post: Omit) {
const response = await fetch(`${BASE_URL}/posts`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(post),
});
if (!response.ok) throw new Error('Не вдалося створити пост');
return response.json();
}
4.2 Реалізація власних хуків
Створіть файл hooks/usePost.ts
:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { fetchPost, updatePost, fetchPostComments } from '@/lib/api';
export function usePost(postId: number) {
const postQuery = useQuery({
queryKey: ['post', postId],
queryFn: () => fetchPost(postId),
});
const commentsQuery = useQuery({
queryKey: ['post', postId, 'comments'],
queryFn: () => fetchPostComments(postId),
});
return {
post: postQuery.data,
comments: commentsQuery.data,
isLoading: postQuery.isLoading || commentsQuery.isLoading,
error: postQuery.error || commentsQuery.error,
};
}
5.
Реалізація складних патернів
5.1 Нескінченне прокручування
Створіть файл hooks/useInfinitePosts.ts
:
import { useInfiniteQuery } from '@tanstack/react-query';
import { fetchPosts } from '@/lib/api';
export function useInfinitePosts() {
return useInfiniteQuery({
queryKey: ['posts', 'infinite'],
queryFn: ({ pageParam = 1 }) => fetchPosts(pageParam),
getNextPageParam: (lastPage, allPages) =>
lastPage.hasMore ? allPages.length + 1 : undefined,
initialPageParam: 1,
});
}
5.2 Оптимістичні оновлення
Створіть файл hooks/useUpdatePost.ts
:
export function useUpdatePost() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, updates }) => updatePost(id, updates),
onMutate: async ({ id, updates }) => {
await queryClient.cancelQueries({ queryKey: ['post', id] });
const previousPost = queryClient.getQueryData(['post', id]);
queryClient.setQueryData(['post', id], old => ({
...old,
...updates,
}));
return { previousPost };
},
onError: (err, variables, context) => {
if (context?.previousPost) {
queryClient.setQueryData(
['post', variables.id],
context.previousPost
);
}
},
});
}
6.
Найкращі практики та оптимізація
6.1 Керування ключами запитів
Створіть файл з константами для ключів запитів:
// constants/queryKeys.ts
export const queryKeys = {
posts: {
all: ['posts'],
lists: () => [...queryKeys.posts.all, 'list'],
list: (filters: string) => [...queryKeys.posts.lists(), filters],
details: () => [...queryKeys.posts.all, 'detail'],
detail: (id: number) => [...queryKeys.posts.details(), id],
},
};
6.2 Реалізація кордону помилок
Створіть файл components/ErrorBoundary.tsx
:
'use client';
import { Component, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback: ReactNode;
}
interface State {
hasError: boolean;
}
export class ErrorBoundary extends Component {
public state: State = {
hasError: false,
};
public static getDerivedStateFromError(): State {
return { hasError: true };
}
public render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
Більше теорії про TanStack Query з прикладами,
1.
Ключі запитів
Ключі запитів — це унікальні ідентифікатори, які TanStack Query використовує для кешування та керування даними запитів.
1.1 Базова структура ключів запитів
// Простий ключ запиту
const queryKey = ['todos']
// Ключ запиту з параметрами
const queryKey = ['todo', 5]
// Ключ запиту з фільтрами
const queryKey = ['todos', { status: 'done', page: 1 }]
// Структуровані ключі запитів (рекомендований підхід)
const queryKeys = {
todos: {
all: ['todos'] as const,
lists: () => [...queryKeys.todos.all, 'list'] as const,
list: (filters: string) => [...queryKeys.todos.lists(), { filters }] as const,
details: () => [...queryKeys.todos.all, 'detail'] as const,
detail: (id: number) => [...queryKeys.todos.details(), id] as const,
}
}
1.2 Приклади використання ключів запитів
// Отримання всіх todos
useQuery({
queryKey: queryKeys.todos.all,
queryFn: fetchTodos
})
// Отримання конкретного todo
useQuery({
queryKey: queryKeys.todos.detail(5),
queryFn: () => fetchTodoById(5)
})
// Отримання відфільтрованих todos
useQuery({
queryKey: queryKeys.todos.list('active'),
queryFn: () => fetchTodosByStatus('active')
})
2.
useQueryClient
useQueryClient — це хук, який надає доступ до інстанції QueryClient для ручної взаємодії з кешем.
2.1 Базове використання
function TodoComponent() {
const queryClient = useQueryClient()
// Доступ до кешованих даних
const todos = queryClient.getQueryData(['todos'])
// Інвалідизація запитів
const invalidateTodos = () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
} // Попереднє завантаження даних
const prefetchTodo = (id: number) => {
queryClient.prefetchQuery({
queryKey: ['todo', id],
queryFn: () => fetchTodoById(id)
})
}
}
2.2 Розширені операції з кешем
function AdvancedCacheOperations() {
const queryClient = useQueryClient()
// Пряме встановлення даних у кеш
const updateCache = () => {
queryClient.setQueryData(['todo', 1], (old) => ({
...old,
title: 'Оновлений заголовок'
}))
} // Видалення запитів з кешу
const clearTodoCache = () => {
queryClient.removeQueries({ queryKey: ['todos'] })
} // Скидання всього кешу
const resetCache = () => {
queryClient.resetQueries()
} // Скасування запитів, що виконуються
const cancelQueries = () => {
queryClient.cancelQueries({ queryKey: ['todos'] })
}
}
3.
3.1 useQuery
Для отримання даних:
function TodosList() {
const {
data,
isLoading,
isError,
error,
isFetching,
refetch
} = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 5000, // Дані стають застарілими через 5 секунд
cacheTime: 300000, // Кеш зберігається протягом 5 хвилин
retry: 3, // Повторити неуспішні запити 3 рази
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
refetchOnWindowFocus: true, // Перезапитувати дані при фокусуванні на вікні
enabled: true, // Керувати запуском запиту
})
}
3.2 useMutation
Для зміни даних:
function CreateTodo() {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: (newTodo) => createTodo(newTodo),
onMutate: async (newTodo) => {
// Скасувати всі запити на повторну вибірку
await queryClient.cancelQueries({ queryKey: ['todos'] })
// Зберегти попереднє значення
const previousTodos = queryClient.getQueryData(['todos']) // Оптимістичне оновлення кешу
queryClient.setQueryData(['todos'], old => [...old, newTodo]) return { previousTodos }
},
onError: (err, newTodo, context) => {
// Откотити зміни у випадку помилки
queryClient.setQueryData(['todos'], context.previousTodos)
},
onSettled: () => {
// Перезапитувати після помилки або успіху
queryClient.invalidateQueries({ queryKey: ['todos'] })
}
})
}
3.3 useInfiniteQuery
Для пагінації/нескінченного прокручування:
function InfiniteTodosList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage
} = useInfiniteQuery({
queryKey: ['todos', 'infinite'],
queryFn: ({ pageParam = 0 }) => fetchTodoPage(pageParam),
getNextPageParam: (lastPage, allPages) => {
return lastPage.nextCursor || undefined
},
initialPageParam: 0
})
}
4.
4.1 Query Suspense
function SuspenseTodos() {
return (
Loading...}>
)
}
function TodoList() {
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
suspense: true
})
}
4.2 Паралельні запити
function ParallelQueries() {
const todos = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
const users = useQuery({ queryKey: ['users'], queryFn: fetchUsers })
// Або за допомогою useQueries
const results = useQueries({
queries: [
{ queryKey: ['todo', 1], queryFn: () => fetchTodoById(1) },
{ queryKey: ['todo', 2], queryFn: () => fetchTodoById(2) }
]
})
}
4.3 Залежні запити
function DependentQueries() {
const { data: user } = useQuery({
queryKey: ['user', email],
queryFn: () => fetchUserByEmail(email)
})
const { data: todos } = useQuery({
queryKey: ['todos', user?.id],
queryFn: () => fetchUserTodos(user.id),
enabled: !!user // Запит виконується лише, коли існують дані користувача
})
}
5.
5.1 Глобальні налаштування за замовчуванням
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 хвилин
cacheTime: 10 * 60 * 1000, // 10 хвилин
retry: 3,
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
refetchOnWindowFocus: true,
refetchOnMount: true,
refetchOnReconnect: true,
},
mutations: {
retry: 3,
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
},
},
})
5.2 Спостерігач запитів
const observer = new QueryObserver(queryClient, {
queryKey: ['todos'],
queryFn: fetchTodos,
})
const unsubscribe = observer.subscribe(result => {
console.log(result)
// Оновити UI або виконати побічні ефекти
})// Пізніше
unsubscribe()
Висновок
Дотримуючись цього посібника, ви створили повністю функціональний блог-додаток з:
- Потужною реалізацією REST API
- Ефективним отриманням даних за допомогою TanStack Query
- Розширеними можливостями, такими як нескінченне прокручування та оптимістичне оновлення
- Типізованими реалізаціями
- Кращими практиками для продуктивного використання
Основні висновки:
1.
5.3 Основні переваги
- TanStack Query значно спрощує управління даними
- Next.js App Router забезпечує чисту, сучасну архітектуру
- TypeScript гарантує типову безпеку по всьому додатку
- Користувацькі хуки (Custom Hooks) дозволяють зберігати код модульним та багаторазовим
Наступні кроки:
- Реалізувати автентифікацію
- Додати інтеграцію з реальною базою даних
- Додати більш розширені функції, такі як пошук і фільтрація
- Реалізувати стратегії кешування
- Додати тести
Не забудьте перевірити документацію TanStack Query для ознайомлення з більш розширеними функціями та оптимізаціями.
Перекладено з: Building a Modern Blog App with Next.js App Router, TanStack Query, and REST API