Створення сучасного блогу з використанням Next.js App Router, TanStack Query та REST API

В цьому всебічному посібнику ми створимо блог-додаток, використовуючи Next.js 13+ App Router, TanStack Query (раніше React Query) та REST API. Ми охопимо все — від налаштування до передових шаблонів, створюючи повністю функціональний блог з постами та коментарями.

pic

Зміст

  1. Налаштування проєкту
  2. Налаштування REST API
  3. Конфігурація TanStack Query
  4. Створення основних функцій
  5. Реалізація передових шаблонів
  6. Кращі практики та оптимізація

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 Основні переваги

  1. TanStack Query значно спрощує управління даними
  2. Next.js App Router забезпечує чисту, сучасну архітектуру
  3. TypeScript гарантує типову безпеку по всьому додатку
  4. Користувацькі хуки (Custom Hooks) дозволяють зберігати код модульним та багаторазовим

Наступні кроки:

  • Реалізувати автентифікацію
  • Додати інтеграцію з реальною базою даних
  • Додати більш розширені функції, такі як пошук і фільтрація
  • Реалізувати стратегії кешування
  • Додати тести

Не забудьте перевірити документацію TanStack Query для ознайомлення з більш розширеними функціями та оптимізаціями.

Перекладено з: Building a Modern Blog App with Next.js App Router, TanStack Query, and REST API

Leave a Reply

Your email address will not be published. Required fields are marked *