docs: добавить стайлгайд nextjs-style-guide в репозиторий
- Добавлена документация SLM-архитектуры, базовых правил и прикладных разделов - Добавлены разделы: стили, SVG-спрайты, шаблоны генерации, PostCSS, REST, Realtime - Удалены устаревшие файлы (спрайты, скрипты, стили из app/)
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
---
|
||||
title: Business-композиция
|
||||
description: Когда REST-данные нужно объединить или интерпретировать в бизнес-модуле.
|
||||
keywords: [rest, business, композиция, hooks, domain, isAuth]
|
||||
---
|
||||
|
||||
# Business-композиция
|
||||
|
||||
Business-композиция используется, когда простого GET-метода или прозрачного GET-хука недостаточно: нужно объединить несколько источников, преобразовать DTO или вычислить доменное состояние.
|
||||
|
||||
## Когда использовать
|
||||
|
||||
- Нужно объединить несколько GET-запросов.
|
||||
- Нужно вычислить `isAuth`, `canEdit`, `hasAccess`, `hasPets`.
|
||||
- Нужно преобразовать DTO в доменную модель.
|
||||
- Нужно спрятать бизнес-сценарий за доменным API.
|
||||
|
||||
Такая логика не пишется в `infrastructure/`. REST-клиент остаётся прозрачным адаптером к API.
|
||||
|
||||
## Пример поверх одного GET-хука
|
||||
|
||||
```ts
|
||||
// src/business/pets/hooks/use-available-pets.hook.ts
|
||||
import { useGetPetList } from 'infrastructure/pet-store-api'
|
||||
|
||||
/**
|
||||
* Доменный список доступных питомцев.
|
||||
*/
|
||||
export const useAvailablePets = () => {
|
||||
const query = useGetPetList('available')
|
||||
|
||||
return {
|
||||
...query,
|
||||
hasPets: Boolean(query.data?.length),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`useGetPetList` — infrastructure-хук. `hasPets` — бизнес-интерпретация, поэтому она появляется в `business/pets`.
|
||||
|
||||
## Пример композиции нескольких GET-хуков
|
||||
|
||||
```ts
|
||||
// src/business/pets/hooks/use-pets-dashboard.hook.ts
|
||||
import { useGetPetList } from 'infrastructure/pet-store-api'
|
||||
|
||||
/**
|
||||
* Данные dashboard по питомцам.
|
||||
*/
|
||||
export const usePetsDashboard = () => {
|
||||
const availablePets = useGetPetList('available')
|
||||
const pendingPets = useGetPetList('pending')
|
||||
const soldPets = useGetPetList('sold')
|
||||
|
||||
return {
|
||||
availablePets,
|
||||
pendingPets,
|
||||
soldPets,
|
||||
total:
|
||||
(availablePets.data?.length ?? 0) +
|
||||
(pendingPets.data?.length ?? 0) +
|
||||
(soldPets.data?.length ?? 0),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Композиция нескольких запросов не добавляется в `infrastructure/pet-store-api/hooks/`, потому что это уже сценарий потребления данных.
|
||||
|
||||
## Пример auth-состояния
|
||||
|
||||
```ts
|
||||
// src/business/auth/hooks/use-auth-state.hook.ts
|
||||
import { useGetCurrentUser } from 'infrastructure/backend-api'
|
||||
|
||||
/**
|
||||
* Состояние авторизации текущего пользователя.
|
||||
*/
|
||||
export const useAuthState = () => {
|
||||
const currentUser = useGetCurrentUser()
|
||||
const user = currentUser.data
|
||||
|
||||
return {
|
||||
...currentUser,
|
||||
user,
|
||||
isAuth: Boolean(user),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`isAuth` не является частью REST-клиента. Это доменный смысл результата запроса.
|
||||
|
||||
## Где размещать
|
||||
|
||||
```text
|
||||
src/business/
|
||||
└── pets/
|
||||
├── hooks/
|
||||
│ └── use-available-pets.hook.ts
|
||||
├── mappers/
|
||||
│ └── map-pet-dto-to-pet.ts
|
||||
├── types/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Модуль `business/` экспортирует наружу готовый доменный API через `index.ts`.
|
||||
|
||||
## Что запрещено
|
||||
|
||||
```ts
|
||||
// Плохо — business-смысл внутри infrastructure-хука
|
||||
export const useGetPetList = (status: PetStatus) => {
|
||||
const query = useSWR(...)
|
||||
|
||||
return {
|
||||
...query,
|
||||
hasPets: Boolean(query.data?.length),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
REST-модуль отвечает за доступ к API. Business-модуль отвечает за смысл этих данных в продукте.
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
title: Клиентский GET-хук
|
||||
description: Получение REST-данных в Client Components через готовые GET-хуки REST-клиента.
|
||||
keywords: [rest, client components, swr, get-хук, client state]
|
||||
---
|
||||
|
||||
# Клиентский GET-хук
|
||||
|
||||
Клиентский GET-хук используется, когда данные зависят от состояния браузера: вкладки, фильтра, поиска, пагинации, модалки или действия пользователя.
|
||||
|
||||
## Когда использовать
|
||||
|
||||
- Запрос зависит от client state.
|
||||
- Данные не обязательны для первого HTML.
|
||||
- Пользователь меняет параметры запроса на клиенте.
|
||||
- Нужны SWR-кеширование, дедупликация и ревалидация.
|
||||
|
||||
## Пример с вкладками
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useGetPetList } from 'infrastructure/pet-store-api'
|
||||
import type { PetStatus } from 'infrastructure/pet-store-api'
|
||||
|
||||
const statuses: PetStatus[] = ['available', 'pending', 'sold']
|
||||
|
||||
export function PetTabs() {
|
||||
const [status, setStatus] = useState<PetStatus>('available')
|
||||
const { data: pets, isLoading, error } = useGetPetList(status)
|
||||
|
||||
return (
|
||||
<section>
|
||||
<div>
|
||||
{statuses.map((item) => (
|
||||
<button key={item} type="button" onClick={() => setStatus(item)}>
|
||||
{item}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{isLoading && <div>Загрузка...</div>}
|
||||
{error && <div>Ошибка загрузки</div>}
|
||||
|
||||
<ul>
|
||||
{pets?.map((pet) => (
|
||||
<li key={pet.id}>{pet.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Компонент выбирает параметр `status`, но не знает про SWR-ключ и fetcher. Запрос выполняет готовый GET-хук REST-клиента.
|
||||
|
||||
## Если хука нет
|
||||
|
||||
Хук добавляется в REST-модуль сервиса:
|
||||
|
||||
```text
|
||||
src/infrastructure/pet-store-api/hooks/use-get-pet-list.hook.ts
|
||||
```
|
||||
|
||||
Не создавайте локальный `useSWR` в компоненте.
|
||||
|
||||
## Плохо
|
||||
|
||||
```tsx
|
||||
// Плохо — прямой вызов клиента в useEffect
|
||||
useEffect(() => {
|
||||
petStoreApi.pet.findPetsByStatus({ status }).then(setPets)
|
||||
}, [status])
|
||||
|
||||
// Плохо — useSWR в компоненте
|
||||
const { data } = useSWR(
|
||||
['pet-store-api', 'pet', 'list', status],
|
||||
() => petStoreApi.pet.findPetsByStatus({ status }),
|
||||
)
|
||||
```
|
||||
|
||||
Такой код теряет единое место для ключей, дублирует fetcher и разносит инфраструктурные детали по UI.
|
||||
|
||||
## Когда выбрать другую стратегию
|
||||
|
||||
- Данные нужны до первого HTML — [Серверный await](./server-await.md).
|
||||
- Клиентский хук должен получить начальные данные сразу — [Начальные данные для клиентских хуков](./client-hooks-initial-data.md).
|
||||
- Нужно вычислить бизнес-состояние — [Business-композиция](./business-composition.md).
|
||||
@@ -0,0 +1,109 @@
|
||||
---
|
||||
title: Начальные данные для клиентских хуков
|
||||
description: Как дать клиентским GET-хукам начальные REST-данные.
|
||||
keywords: [rest, swr, fallback, initial data, client hooks, unstable_serialize, isr, ssr]
|
||||
---
|
||||
|
||||
# Начальные данные для клиентских хуков
|
||||
|
||||
Как дать клиентским GET-хукам начальные REST-данные.
|
||||
|
||||
Эта стратегия используется, когда данные должны быть запущены на сервере, но потребляться на клиенте через GET-хуки REST-клиента.
|
||||
|
||||
Технически это делается через `SWRConfig fallback`: сервер передаёт промис в fallback, а клиентский хук использует тот же SWR-ключ.
|
||||
|
||||
## Когда использовать
|
||||
|
||||
- Внутри страницы есть Client Components с GET-хуками.
|
||||
- Нужно начать загрузку данных на сервере раньше.
|
||||
- Клиентский компонент должен остаться обычным потребителем `useGetPetList(...)`.
|
||||
- Не нужно писать отдельный prop-drilling для начальных данных.
|
||||
|
||||
## Рендер страницы
|
||||
|
||||
Перед этой стратегией сначала определите рендер маршрута. Серверный preload для `fallback` подчиняется тем же правилам, что и любой серверный запрос в `page.tsx` или `layout.tsx`.
|
||||
|
||||
Если данные общие и могут обновляться по интервалу, сохраняйте static/ISR. Если preload зависит от cookie, headers, `searchParams`, `no-store` или персональных данных пользователя, маршрут становится dynamic/SSR.
|
||||
|
||||
`SWRConfig fallback` не должен быть причиной отключать ISR на всякий случай. Он только передаёт клиентскому GET-хуку данные, которые уже были запущены на сервере.
|
||||
|
||||
## Ключ хука
|
||||
|
||||
```ts
|
||||
// src/infrastructure/pet-store-api/hooks/use-get-pet-list.hook.ts
|
||||
export const getPetListKey = (status: PetStatus) =>
|
||||
['pet-store-api', 'pet', 'list', status] as const
|
||||
```
|
||||
|
||||
Ключ экспортируется из REST-модуля, потому что он нужен и GET-хуку, и серверному `SWRConfig fallback`.
|
||||
|
||||
## Пример layout
|
||||
|
||||
```tsx
|
||||
// src/app/(routes)/pets/layout.tsx
|
||||
import type { ReactNode } from 'react'
|
||||
import { SWRConfig, unstable_serialize } from 'swr'
|
||||
import {
|
||||
getPetListKey,
|
||||
petStoreApi,
|
||||
} from 'infrastructure/pet-store-api'
|
||||
|
||||
type PetsLayoutProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export default async function PetsLayout({ children }: PetsLayoutProps) {
|
||||
const availablePetsPromise = petStoreApi.pet.findPetsByStatus({
|
||||
status: 'available',
|
||||
})
|
||||
|
||||
return (
|
||||
<SWRConfig
|
||||
value={{
|
||||
fallback: {
|
||||
[unstable_serialize(getPetListKey('available'))]: availablePetsPromise,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SWRConfig>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Если GET-хук использует array-key, ключ для `fallback` сериализуется через `unstable_serialize`.
|
||||
|
||||
## Клиентский компонент
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import { useGetPetList } from 'infrastructure/pet-store-api'
|
||||
|
||||
export function PetList() {
|
||||
const { data: pets, isLoading } = useGetPetList('available')
|
||||
|
||||
if (isLoading) return <div>Загрузка...</div>
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{pets?.map((pet) => (
|
||||
<li key={pet.id}>{pet.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Компонент не знает, что данные были запущены на сервере. Он использует обычный GET-хук REST-клиента.
|
||||
|
||||
## Что важно
|
||||
|
||||
- Ключ `fallback` должен совпадать с ключом GET-хука.
|
||||
- Серверный код вызывает метод клиента, а не GET-хук.
|
||||
- Клиентский компонент вызывает GET-хук, а не `useSWR` напрямую.
|
||||
- Эта стратегия не означает ручную работу с кешем в компонентах.
|
||||
|
||||
## Когда не использовать
|
||||
|
||||
Если данные нужны только серверному компоненту, используйте [Серверный await](./server-await.md). Если данные зависят от состояния браузера, используйте [Клиентский GET-хук](./client-get-hook.md).
|
||||
100
ai/nextjs-style-guide/data/rest/strategies/index.md
Normal file
100
ai/nextjs-style-guide/data/rest/strategies/index.md
Normal file
@@ -0,0 +1,100 @@
|
||||
---
|
||||
title: Стратегии получения данных
|
||||
description: Как выбрать получение REST-данных с учётом рендера страницы.
|
||||
keywords: [rest, стратегии, render, isr, ssr, server components, swr, promise, business]
|
||||
---
|
||||
|
||||
# Стратегии получения данных
|
||||
|
||||
Как выбрать получение REST-данных с учётом рендера страницы.
|
||||
|
||||
Перед выбором стратегии должен быть создан REST-клиент сервиса. Если клиента ещё нет, начните с раздела [Создание клиента](../clients/index.md).
|
||||
|
||||
## Сначала определите рендер страницы
|
||||
|
||||
В Next.js выбор начинается не с `await`, `Suspense` или SWR. Сначала нужно понять, какой рендер получится у маршрута: static/ISR или dynamic/SSR.
|
||||
|
||||
Next.js может перевести страницу в dynamic rendering автоматически, если в маршруте используются API текущего запроса. Поэтому первый вопрос такой:
|
||||
|
||||
```text
|
||||
Можно ли сохранить ISR, или странице нужны данные на каждый request?
|
||||
```
|
||||
|
||||
ISR — приоритет. Если данные общие для пользователей и их можно обновлять с интервалом, не переводите страницу в SSR без необходимости.
|
||||
|
||||
SSR/dynamic rendering выбирается только когда данные действительно зависят от текущего request или должны пересчитываться на каждый запрос.
|
||||
|
||||
## Что переводит страницу в dynamic rendering
|
||||
|
||||
Проверьте, нужны ли странице API и настройки, которые делают маршрут динамическим:
|
||||
|
||||
- `cookies()` — данные зависят от cookie текущего пользователя.
|
||||
- `headers()` — данные зависят от request headers.
|
||||
- `draftMode()` — нужен preview/draft-режим.
|
||||
- `searchParams` в `page.tsx` — данные зависят от query string.
|
||||
- `cache: 'no-store'` или `revalidate: 0` в методе клиента — запрос нельзя кешировать.
|
||||
- `connection()` — рендер явно ждёт request.
|
||||
- `export const dynamic = 'force-dynamic'` — SSR включён вручную.
|
||||
|
||||
Если ничего из этого не нужно, сначала проектируйте страницу как static/ISR. Серверный `await` сам по себе не означает SSR: режим зависит от кеширования запроса и dynamic API маршрута.
|
||||
|
||||
## Рендер перед стратегией
|
||||
|
||||
| Рендер | Когда подходит | Что выбирать дальше |
|
||||
|--------|----------------|---------------------|
|
||||
| Static/ISR | Данные общие и могут обновляться по интервалу | Серверные стратегии: `await`, `Promise.all`, передача промиса ниже, SWR `fallback` |
|
||||
| SSR/dynamic | Данные зависят от request, пользователя или должны быть свежими на каждый запрос | Серверные стратегии с учётом блокировки первого HTML |
|
||||
| После гидрации | Данные зависят от вкладки, фильтра, поиска, пагинации или действия пользователя | Клиентский GET-хук |
|
||||
|
||||
## Как выбрать стратегию
|
||||
|
||||
Когда режим рендера понятен, выбирайте конкретный способ получения данных:
|
||||
|
||||
| Ситуация после выбора рендера | Стратегия | Где читать |
|
||||
|-------------------------------|-----------|------------|
|
||||
| Данные обязательны для первого HTML, SEO, `notFound()` или `redirect()` | Серверный `await` | [Серверный await](./server-await.md) |
|
||||
| Несколько независимых данных нужны до рендера | Запуск промисов + `Promise.all` | [Параллельные серверные запросы](./parallel-server-requests.md) |
|
||||
| Часть UI можно загрузить отдельно | Передача промиса ниже + `Suspense` | [Передача промиса ниже](./pass-promise-down.md) |
|
||||
| Client Component должен получить данные сразу из SWR | Начальные данные для клиентских хуков | [Начальные данные для клиентских хуков](./client-hooks-initial-data.md) |
|
||||
| Данные зависят от client state | Клиентский GET-хук | [Клиентский GET-хук](./client-get-hook.md) |
|
||||
| Нужно объединить несколько запросов или вычислить `isAuth`, `canEdit`, `hasPets` | Business-композиция | [Business-композиция](./business-composition.md) |
|
||||
|
||||
## Правило выбора
|
||||
|
||||
Не выбирайте стратегию по любимому инструменту. Выбирайте её по двум вопросам:
|
||||
|
||||
```text
|
||||
Можно ли сохранить ISR?
|
||||
Где нужны данные и что должно произойти до первого HTML?
|
||||
```
|
||||
|
||||
Если данные можно кешировать между пользователями — сохраняйте static/ISR. Если данные request-specific — используйте SSR/dynamic rendering. Если данные зависят от состояния браузера — используйте GET-хук REST-клиента. Если простой GET превращается в доменный сценарий — переходите в `business/`.
|
||||
|
||||
## Общие запреты
|
||||
|
||||
```tsx
|
||||
// Плохо — SSR включён на всякий случай
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// Плохо — ISR отключён без требования к свежести на каждый request
|
||||
export const revalidate = 0
|
||||
|
||||
// Плохо — прямой fetch в компоненте
|
||||
useEffect(() => {
|
||||
fetch('/api/pets').then(...)
|
||||
}, [])
|
||||
|
||||
// Плохо — useSWR в компоненте
|
||||
const { data } = useSWR(
|
||||
['pet-store-api', 'pet', 'list', status],
|
||||
() => petStoreApi.pet.findPetsByStatus({ status }),
|
||||
)
|
||||
|
||||
// Плохо — бизнес-флаг внутри GET-хука REST-клиента
|
||||
return {
|
||||
...query,
|
||||
hasPets: Boolean(query.data?.length),
|
||||
}
|
||||
```
|
||||
|
||||
Не отключайте ISR без причины. В компонентах используются готовые методы клиента или готовые хуки. SWR-ключи, fetcher и транспорт остаются внутри REST-модуля.
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
title: Параллельные серверные запросы
|
||||
description: Как запускать независимые REST-запросы на сервере без waterfall.
|
||||
keywords: [rest, promise.all, параллельные запросы, server components]
|
||||
---
|
||||
|
||||
# Параллельные серверные запросы
|
||||
|
||||
Если серверному компоненту нужно несколько независимых данных, запускайте запросы до ожидания результата. Последовательный `await` создаёт waterfall и замедляет рендер.
|
||||
|
||||
## Когда использовать
|
||||
|
||||
- Запросы независимы друг от друга.
|
||||
- Все данные нужны текущему серверному компоненту перед возвратом UI.
|
||||
- Нельзя или не нужно стримить часть UI отдельно.
|
||||
|
||||
## Хорошо
|
||||
|
||||
```tsx
|
||||
import { petStoreApi } from 'infrastructure/pet-store-api'
|
||||
import { PetsDashboardScreen } from 'screens/pets-dashboard'
|
||||
|
||||
export default async function PetsDashboardPage() {
|
||||
const availablePetsPromise = petStoreApi.pet.findPetsByStatus({ status: 'available' })
|
||||
const pendingPetsPromise = petStoreApi.pet.findPetsByStatus({ status: 'pending' })
|
||||
const soldPetsPromise = petStoreApi.pet.findPetsByStatus({ status: 'sold' })
|
||||
|
||||
const [availablePets, pendingPets, soldPets] = await Promise.all([
|
||||
availablePetsPromise,
|
||||
pendingPetsPromise,
|
||||
soldPetsPromise,
|
||||
])
|
||||
|
||||
return (
|
||||
<PetsDashboardScreen
|
||||
availablePets={availablePets}
|
||||
pendingPets={pendingPets}
|
||||
soldPets={soldPets}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Плохо
|
||||
|
||||
```tsx
|
||||
export default async function PetsDashboardPage() {
|
||||
const availablePets = await petStoreApi.pet.findPetsByStatus({ status: 'available' })
|
||||
const pendingPets = await petStoreApi.pet.findPetsByStatus({ status: 'pending' })
|
||||
const soldPets = await petStoreApi.pet.findPetsByStatus({ status: 'sold' })
|
||||
|
||||
return (
|
||||
<PetsDashboardScreen
|
||||
availablePets={availablePets}
|
||||
pendingPets={pendingPets}
|
||||
soldPets={soldPets}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Во втором примере каждый следующий запрос ждёт предыдущий, хотя они независимы.
|
||||
|
||||
## Зависимые запросы
|
||||
|
||||
Если второй запрос зависит от результата первого, последовательный `await` допустим:
|
||||
|
||||
```tsx
|
||||
export default async function OrderPage({ params }: OrderPageProps) {
|
||||
const { id } = await params
|
||||
const order = await petStoreApi.store.getOrderById(Number(id))
|
||||
const pet = await petStoreApi.pet.getPetById(order.petId)
|
||||
|
||||
return <OrderScreen order={order} pet={pet} />
|
||||
}
|
||||
```
|
||||
|
||||
Не превращайте зависимый сценарий в `Promise.all` искусственно.
|
||||
|
||||
## Когда выбрать другую стратегию
|
||||
|
||||
Если часть данных не обязательна для первого блока UI, можно запустить промис выше и передать его ниже: [Передача промиса ниже](./pass-promise-down.md).
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
title: Передача промиса ниже
|
||||
description: Как запускать серверный REST-запрос выше и ожидать его во вложенном server-компоненте.
|
||||
keywords: [rest, promise, suspense, streaming, server components]
|
||||
---
|
||||
|
||||
# Передача промиса ниже
|
||||
|
||||
Серверный компонент может запустить запрос и передать промис вложенному server-компоненту. Это полезно, когда часть UI можно загрузить отдельно через `Suspense`.
|
||||
|
||||
## Когда использовать
|
||||
|
||||
- Верхняя часть страницы может отрендериться без этих данных.
|
||||
- Данные нужны только вложенному server-компоненту.
|
||||
- Нужна `Suspense`-граница и серверный стриминг.
|
||||
|
||||
## Пример
|
||||
|
||||
```tsx
|
||||
// src/app/(routes)/pets/page.tsx
|
||||
import { Suspense } from 'react'
|
||||
import { petStoreApi } from 'infrastructure/pet-store-api'
|
||||
import { PetListSection } from 'widgets/pet-list-section'
|
||||
import { PetListSkeleton } from 'widgets/pet-list-section'
|
||||
import type { Pet } from 'infrastructure/pet-store-api'
|
||||
|
||||
export default function PetsPage() {
|
||||
const petsPromise = petStoreApi.pet.findPetsByStatus({ status: 'available' })
|
||||
|
||||
return (
|
||||
<main>
|
||||
<h1>Питомцы</h1>
|
||||
<Suspense fallback={<PetListSkeleton />}>
|
||||
<AvailablePets petsPromise={petsPromise} />
|
||||
</Suspense>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
async function AvailablePets({ petsPromise }: { petsPromise: Promise<Pet[]> }) {
|
||||
const pets = await petsPromise
|
||||
|
||||
return <PetListSection pets={pets} />
|
||||
}
|
||||
```
|
||||
|
||||
Запрос стартует в `PetsPage`, но ожидание происходит внутри `AvailablePets`. `Suspense` управляет fallback для этой части UI.
|
||||
|
||||
## Граница стратегии
|
||||
|
||||
Эта стратегия остаётся серверной. Не используйте её как замену GET-хукам в Client Components.
|
||||
|
||||
Если данные должны попасть в клиентский SWR-хук, используйте [Начальные данные для клиентских хуков](./client-hooks-initial-data.md).
|
||||
|
||||
## Что не делать
|
||||
|
||||
```tsx
|
||||
// Плохо — передавать промис в произвольный клиентский компонент без ясной стратегии
|
||||
return <PetListClient petsPromise={petsPromise} />
|
||||
```
|
||||
|
||||
Для клиентского потребления есть отдельная стратегия через `SWRConfig fallback` и готовые GET-хуки REST-клиента.
|
||||
88
ai/nextjs-style-guide/data/rest/strategies/server-await.md
Normal file
88
ai/nextjs-style-guide/data/rest/strategies/server-await.md
Normal file
@@ -0,0 +1,88 @@
|
||||
---
|
||||
title: Серверный await
|
||||
description: Получение REST-данных на сервере до первого HTML.
|
||||
keywords: [rest, server components, await, nextjs, isr, ssr, notFound, redirect]
|
||||
---
|
||||
|
||||
# Серверный await
|
||||
|
||||
Получение REST-данных на сервере до первого HTML.
|
||||
|
||||
Серверный `await` — базовая стратегия для данных, которые нужны до рендера страницы или серверного блока.
|
||||
|
||||
## Когда использовать
|
||||
|
||||
- Данные нужны для первого HTML.
|
||||
- Данные влияют на `metadata`.
|
||||
- По результату запроса нужно вызвать `notFound()` или `redirect()`.
|
||||
- Компонент серверный и данные не зависят от состояния браузера.
|
||||
|
||||
## Влияние на рендер
|
||||
|
||||
Серверный `await` сам по себе не означает SSR. В App Router страница может остаться static/ISR, если маршрут не использует dynamic API и запросы можно кешировать.
|
||||
|
||||
ISR — приоритет для общих данных. Если список или детальная страница могут обновляться по интервалу, сохраняйте кеширование и не добавляйте `no-store`, `revalidate: 0` или `force-dynamic` без требования.
|
||||
|
||||
SSR/dynamic rendering нужен, когда данные зависят от текущего request: cookie, headers, `searchParams`, preview-режим или персональные данные пользователя.
|
||||
|
||||
## Пример страницы списка
|
||||
|
||||
```tsx
|
||||
// src/app/(routes)/pets/page.tsx
|
||||
import { petStoreApi } from 'infrastructure/pet-store-api'
|
||||
import { PetsScreen } from 'screens/pets'
|
||||
|
||||
export default async function PetsPage() {
|
||||
const pets = await petStoreApi.pet.findPetsByStatus({
|
||||
status: 'available',
|
||||
})
|
||||
|
||||
return <PetsScreen pets={pets} />
|
||||
}
|
||||
```
|
||||
|
||||
`page.tsx` получает данные первого рендера и передаёт их ниже. UI страницы остаётся в `screens/`, а не пишется прямо в `app/`.
|
||||
|
||||
## Пример детальной страницы
|
||||
|
||||
```tsx
|
||||
// src/app/(routes)/pets/[id]/page.tsx
|
||||
import { notFound } from 'next/navigation'
|
||||
import { petStoreApi } from 'infrastructure/pet-store-api'
|
||||
import { PetDetailScreen } from 'screens/pet-detail'
|
||||
|
||||
type PetPageProps = {
|
||||
params: Promise<{ id: string }>
|
||||
}
|
||||
|
||||
export default async function PetPage({ params }: PetPageProps) {
|
||||
const { id } = await params
|
||||
const pet = await petStoreApi.pet.getPetById(Number(id)).catch(() => null)
|
||||
|
||||
if (!pet) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
return <PetDetailScreen pet={pet} />
|
||||
}
|
||||
```
|
||||
|
||||
Обработка 404 зависит от API-клиента и класса ошибок. В примере показана идея: решение о `notFound()` принимается на уровне маршрута, а не внутри REST-клиента.
|
||||
|
||||
## Что не делать
|
||||
|
||||
```tsx
|
||||
// Плохо — хуки нельзя вызывать в Server Component
|
||||
const { data } = useGetPetList('available')
|
||||
|
||||
// Плохо — прямой fetch в обход клиента
|
||||
const response = await fetch('https://petstore3.swagger.io/api/v3/pet/findByStatus')
|
||||
```
|
||||
|
||||
Если данные нужны на сервере, вызывайте метод REST-клиента напрямую.
|
||||
|
||||
## Когда выбрать другую стратегию
|
||||
|
||||
- Несколько независимых запросов — [Параллельные серверные запросы](./parallel-server-requests.md).
|
||||
- Часть UI можно грузить отдельно — [Передача промиса ниже](./pass-promise-down.md).
|
||||
- Данные нужны клиентскому хуку сразу после гидрации — [Начальные данные для клиентских хуков](./client-hooks-initial-data.md).
|
||||
Reference in New Issue
Block a user