docs: добавить стайлгайд nextjs-style-guide в репозиторий
- Добавлена документация SLM-архитектуры, базовых правил и прикладных разделов - Добавлены разделы: стили, SVG-спрайты, шаблоны генерации, PostCSS, REST, Realtime - Удалены устаревшие файлы (спрайты, скрипты, стили из app/)
This commit is contained in:
60
ai/nextjs-style-guide/data/index.md
Normal file
60
ai/nextjs-style-guide/data/index.md
Normal file
@@ -0,0 +1,60 @@
|
||||
---
|
||||
title: Источники данных
|
||||
description: Какие источники данных используются в проекте и как с ними работать.
|
||||
keywords: [данные, api, rest, realtime, клиент, swr, infrastructure, введение, карта раздела]
|
||||
---
|
||||
|
||||
# Источники данных
|
||||
|
||||
Какие источники данных используются в проекте и как с ними работать.
|
||||
|
||||
## Принципы раздела
|
||||
|
||||
- **Клиент — в `infrastructure/`.** Каждый внешний сервис — отдельный модуль слоя `infrastructure/{service-name}/`.
|
||||
- **Прямой `fetch` запрещён.** Запросы идут только через клиент модуля. Исключения — точечные и обоснованные.
|
||||
- **Источник данных диктует канал.** REST, realtime и т.п. — независимые подразделы, у каждого своя модель клиента и своё потребление.
|
||||
- **Серверные и клиентские компоненты потребляют по-разному.** Server Components — прямой `await` метода клиента, клиентские — через готовые GET-хуки REST-клиента (`useGetUserList`, `useGetPostDetail` и т.п.). SWR инкапсулирован в хуке, компонент про него не знает.
|
||||
|
||||
## Карта раздела
|
||||
|
||||
### REST
|
||||
|
||||
Канал «запрос-ответ» по HTTP. Покрывает большинство API.
|
||||
|
||||
- [REST](./rest/index.md) — обзор раздела: создание клиента и использование.
|
||||
- **Создание клиента** — как оформляется REST API в проекте:
|
||||
- [Обзор](./rest/clients/index.md) — когда нужен клиент и как выбрать подход.
|
||||
- [Автогенерация из OpenAPI](./rest/clients/auto.md) — для API с OpenAPI-спецификацией, через `@gromlab/api-codegen`.
|
||||
- [Ручное создание](./rest/clients/manual.md) — для API без схемы, клиент пишется и поддерживается руками.
|
||||
- [GET-хуки REST-клиента](./rest/clients/hooks.md) — прозрачные SWR-обёртки над GET-методами клиента.
|
||||
- **Использование** — как получать данные через готовый клиент:
|
||||
- [Стратегии получения данных](./rest/strategies/index.md) — как выбрать способ получения данных под ситуацию.
|
||||
- [Серверный await](./rest/strategies/server-await.md) — прямой `await` метода клиента в Server Components.
|
||||
- [Параллельные серверные запросы](./rest/strategies/parallel-server-requests.md) — запуск независимых серверных запросов без waterfall.
|
||||
- [Передача промиса ниже](./rest/strategies/pass-promise-down.md) — серверный стриминг через промис и `Suspense`.
|
||||
- [Начальные данные для клиентских хуков](./rest/strategies/client-hooks-initial-data.md) — серверный промис в `SWRConfig fallback`.
|
||||
- [Клиентский GET-хук](./rest/strategies/client-get-hook.md) — получение данных в Client Components через готовый GET-хук.
|
||||
- [Business-композиция](./rest/strategies/business-composition.md) — доменная интерпретация и композиция REST-данных.
|
||||
|
||||
### Realtime
|
||||
|
||||
Канал push-данных: WebSocket, SSE, событийные шины. Транспорт не зашит в правила — важна абстракция «подписка».
|
||||
|
||||
- [Realtime](./realtime.md) — клиент realtime в `infrastructure/`, потребление через `useSWRSubscription` или прямые подписки.
|
||||
|
||||
## Что даёт раздел
|
||||
|
||||
После прочтения раздела понятно:
|
||||
|
||||
- Где живёт код работы с API и почему именно там.
|
||||
- Когда генерировать клиент автоматически, а когда писать вручную, и как структурирован каждый из вариантов.
|
||||
- Какие GET-хуки относятся к REST-клиенту и почему они живут в `infrastructure/{service-name}/hooks/`.
|
||||
- Как выбрать стратегию получения REST-данных под конкретную ситуацию.
|
||||
- Как подключать realtime-источники в общую модель работы с данными.
|
||||
- Какие правила обязательны и какие отклонения допустимы.
|
||||
|
||||
## Что не входит в раздел
|
||||
|
||||
- **Глобальное состояние UI** — Stores, формы, фичефлаги. Это [Stores](../applied/stores.md).
|
||||
- **Доменная логика** — как данные превращаются в сценарии бизнеса. Это слой `business/` в [Архитектуре](../basics/architecture/index.md).
|
||||
- **Хуки общего назначения** — переиспользуемые хуки UI, не привязанные к конкретному API. Отдельный прикладной раздел для них пока не ведётся.
|
||||
79
ai/nextjs-style-guide/data/realtime.md
Normal file
79
ai/nextjs-style-guide/data/realtime.md
Normal file
@@ -0,0 +1,79 @@
|
||||
---
|
||||
title: Realtime
|
||||
description: "Работа с push-данными от сервера: подписки и события."
|
||||
keywords: [realtime, websocket, sse, подписка, swr subscription, useSWRSubscription, push, события]
|
||||
---
|
||||
|
||||
# Realtime
|
||||
|
||||
Работа с push-данными от сервера: подписки и события.
|
||||
|
||||
## Принципы
|
||||
|
||||
- **Клиент realtime — в `infrastructure/`** отдельным модулем по имени канала. То же правило, что и для REST: никаких прямых соединений в коде приложения.
|
||||
- **Подписка — единица потребления.** Клиент даёт функцию `subscribe(topic, handler) → unsubscribe`. Внутри — конкретный транспорт.
|
||||
- **Использование на клиенте — два сценария:**
|
||||
- **`useSWRSubscription`** — для данных, которые показываются в UI и должны кешироваться/синхронизироваться с REST.
|
||||
- **Прямая подписка** — для побочных эффектов (тосты, нотификации, аналитика), не привязанных к рендеру.
|
||||
|
||||
## Размещение клиента
|
||||
|
||||
```text
|
||||
src/infrastructure/
|
||||
└── {channel-name}/
|
||||
├── connection.ts # установление соединения, реконнект
|
||||
├── subscribe.ts # subscribe(topic, handler) → unsubscribe
|
||||
├── types.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
## Использование через SWR
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import useSWRSubscription from 'swr/subscription'
|
||||
import { subscribe } from 'infrastructure/notifications'
|
||||
|
||||
export function NotificationCounter() {
|
||||
const { data: count } = useSWRSubscription(
|
||||
['notifications', 'count'],
|
||||
(key, { next }) =>
|
||||
subscribe('notifications.count', (value: number) => next(null, value)),
|
||||
)
|
||||
|
||||
return <span>{count ?? 0}</span>
|
||||
}
|
||||
```
|
||||
|
||||
Плюсы: кеш и дедупликация подписки между несколькими местами рендера; единая модель данных с REST.
|
||||
|
||||
## Прямая подписка
|
||||
|
||||
Для побочных эффектов, которые не влияют на состояние UI напрямую:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { subscribe } from 'infrastructure/notifications'
|
||||
import { showToast } from 'ui/toast'
|
||||
|
||||
export function NotificationsToaster() {
|
||||
useEffect(() => {
|
||||
return subscribe('notifications.new', (notification) => {
|
||||
showToast(notification.message)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
```
|
||||
|
||||
Возврат `unsubscribe` из `useEffect` обязателен — иначе утечка подписки.
|
||||
|
||||
## Запрет прямых соединений
|
||||
|
||||
Создавать `new WebSocket(...)`, `new EventSource(...)` или подписываться на событийные шины напрямую в коде приложения — запрещено. Все соединения проходят через клиент в `infrastructure/`.
|
||||
|
||||
Исключения — точечные и обоснованные (например, диагностический скрипт), помечаются комментарием.
|
||||
193
ai/nextjs-style-guide/data/rest/clients/auto.md
Normal file
193
ai/nextjs-style-guide/data/rest/clients/auto.md
Normal file
@@ -0,0 +1,193 @@
|
||||
---
|
||||
title: Автогенерация из OpenAPI
|
||||
description: Генерация REST-клиента из OpenAPI-спецификации через @gromlab/api-codegen.
|
||||
keywords: [rest, openapi, api-codegen, автогенерация, generated, npx]
|
||||
---
|
||||
|
||||
# Автогенерация из OpenAPI
|
||||
|
||||
Автогенерация используется, когда у API есть актуальная OpenAPI-спецификация. Генератор создаёт TypeScript-клиент, типы и методы API, а разработчик вручную добавляет настройку клиента и GET-хуки.
|
||||
|
||||
## Пример API
|
||||
|
||||
В примерах используется Swagger Petstore:
|
||||
|
||||
```text
|
||||
https://petstore3.swagger.io/api/v3/openapi.json
|
||||
```
|
||||
|
||||
Имена модуля:
|
||||
|
||||
```text
|
||||
src/infrastructure/pet-store-api/
|
||||
petStoreApi
|
||||
pet-store-api.generated.ts
|
||||
```
|
||||
|
||||
## Скрипт генерации
|
||||
|
||||
`@gromlab/api-codegen` не устанавливается в `devDependencies`. Используем `npx @gromlab/api-codegen@latest`, чтобы запускать свежую версию.
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"codegen:pet-store-api": "npx @gromlab/api-codegen@latest -i https://petstore3.swagger.io/api/v3/openapi.json -o src/infrastructure/pet-store-api/generated -n pet-store-api.generated"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Параметры:
|
||||
|
||||
- `-i` — путь к OpenAPI-спецификации: URL или локальный файл.
|
||||
- `-o` — директория для сгенерированного файла.
|
||||
- `-n` — имя сгенерированного файла без `.ts`.
|
||||
|
||||
Ключ `--swr` не используется. GET-хуки REST-клиента пишутся вручную, чтобы сохранить проектный контракт: один GET-хук = один GET-метод, без бизнес-логики и композиции.
|
||||
|
||||
## Генерация
|
||||
|
||||
```bash
|
||||
npm run codegen:pet-store-api
|
||||
```
|
||||
|
||||
Ожидаемый результат:
|
||||
|
||||
```text
|
||||
src/infrastructure/pet-store-api/generated/
|
||||
└── pet-store-api.generated.ts
|
||||
```
|
||||
|
||||
Сгенерированный файл не правится руками и коммитится в репозиторий.
|
||||
|
||||
## Проверка методов
|
||||
|
||||
После генерации откройте `generated/pet-store-api.generated.ts` и проверьте фактические имена методов.
|
||||
|
||||
Для Petstore нужны GET-операции вида:
|
||||
|
||||
```ts
|
||||
petStoreApi.pet.findPetsByStatus(...)
|
||||
petStoreApi.pet.getPetById(...)
|
||||
```
|
||||
|
||||
Точные сигнатуры зависят от OpenAPI-схемы и версии генератора. В рабочих задачах всегда сверяйтесь с generated-файлом.
|
||||
|
||||
## `client.ts`
|
||||
|
||||
Сгенерированный код не должен напрямую использоваться из приложения. Сначала создаётся настроенный инстанс клиента.
|
||||
|
||||
```ts
|
||||
// src/infrastructure/pet-store-api/client.ts
|
||||
import { Api, HttpClient } from './generated/pet-store-api.generated'
|
||||
|
||||
const httpClient = new HttpClient({
|
||||
baseUrl: 'https://petstore3.swagger.io/api/v3',
|
||||
baseApiParams: {
|
||||
secure: false,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const petStoreApi = new Api(httpClient)
|
||||
```
|
||||
|
||||
В реальном проекте вместо строки `baseUrl` обычно берётся из env-переменной, нормализуется в `client.ts` и передаётся в `HttpClient` через конфиг.
|
||||
|
||||
`client.ts` не содержит расширения типов, `declare module` и `Extended`-типы. Он только настраивает транспорт и экспортирует инстанс клиента.
|
||||
|
||||
## Расширение сгенерированных типов
|
||||
|
||||
Сгенерированный файл не правится руками. Если OpenAPI-спецификация неполная или генератор дал слишком общий тип (`object`, `unknown`, отсутствующее поле), расширения живут в `types/`.
|
||||
|
||||
```text
|
||||
src/infrastructure/biocad-less-api/
|
||||
├── generated/
|
||||
│ └── biocad-less-api.generated.ts
|
||||
├── types/
|
||||
│ ├── term.ts
|
||||
│ └── index.ts
|
||||
├── client.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Пример расширения generated-типа:
|
||||
|
||||
```ts
|
||||
// src/infrastructure/biocad-less-api/types/term.ts
|
||||
import type { TermRecordItem } from '../generated/biocad-less-api.generated'
|
||||
|
||||
declare module '../generated/biocad-less-api.generated' {
|
||||
interface TermRecordItem {
|
||||
media?: {
|
||||
file?: string
|
||||
title?: string
|
||||
url?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type TermRecordItemExtended = Omit<
|
||||
TermRecordItem,
|
||||
'categories' | 'tags' | 'fields'
|
||||
> & {
|
||||
categories?: Array<{
|
||||
_id?: string
|
||||
id?: string
|
||||
slug?: string
|
||||
name?: string
|
||||
}>
|
||||
tags?: Array<{
|
||||
_id?: string
|
||||
id?: string
|
||||
slug?: string
|
||||
name?: string
|
||||
}>
|
||||
fields?: Record<string, unknown>
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// src/infrastructure/biocad-less-api/types/index.ts
|
||||
export type { TermRecordItemExtended } from './term'
|
||||
```
|
||||
|
||||
`declare module` используется для добавления отсутствующих полей в generated-интерфейс. `Extended`-тип используется, когда нужно переопределить неточные поля, не трогая generated-файл.
|
||||
|
||||
## Публичный API
|
||||
|
||||
```ts
|
||||
// src/infrastructure/pet-store-api/index.ts
|
||||
export { petStoreApi } from './client'
|
||||
export type { Pet } from './generated/pet-store-api.generated'
|
||||
export * from './hooks'
|
||||
```
|
||||
|
||||
Наружу импортируют только из `infrastructure/pet-store-api`, не из `generated/`.
|
||||
|
||||
Если у модуля есть расширенные типы, они тоже реэкспортируются через `index.ts`:
|
||||
|
||||
```ts
|
||||
// src/infrastructure/biocad-less-api/index.ts
|
||||
export type { TermRecordItemExtended } from './types'
|
||||
```
|
||||
|
||||
## Регенерация
|
||||
|
||||
При изменении OpenAPI-схемы:
|
||||
|
||||
```bash
|
||||
npm run codegen:pet-store-api
|
||||
```
|
||||
|
||||
Что меняется:
|
||||
|
||||
- `generated/pet-store-api.generated.ts` — перезаписывается генератором.
|
||||
- `client.ts`, `hooks/`, `types/`, `index.ts` — не трогаются автоматически.
|
||||
|
||||
Если после регенерации поменялись сигнатуры методов или типы, это исправляется в ручном коде модуля.
|
||||
|
||||
## Следующий шаг
|
||||
|
||||
После генерации и настройки `client.ts` проверьте серверный вызов метода клиента или добавьте [GET-хук REST-клиента](./hooks.md) для Client Components.
|
||||
206
ai/nextjs-style-guide/data/rest/clients/hooks.md
Normal file
206
ai/nextjs-style-guide/data/rest/clients/hooks.md
Normal file
@@ -0,0 +1,206 @@
|
||||
---
|
||||
title: GET-хуки REST-клиента
|
||||
description: Прозрачные SWR-обёртки над GET-методами REST-клиента.
|
||||
keywords: [rest, swr, get-хуки, client components, infrastructure]
|
||||
---
|
||||
|
||||
# GET-хуки REST-клиента
|
||||
|
||||
GET-хуки REST-клиента — прозрачные SWR-обёртки над GET-методами API-клиента. Они нужны, чтобы Client Components получали данные с кешированием, дедупликацией и ревалидацией, не работая с `useSWR` напрямую.
|
||||
|
||||
## Где лежат
|
||||
|
||||
GET-хуки принадлежат REST-клиенту конкретного сервиса и живут рядом с ним:
|
||||
|
||||
```text
|
||||
src/infrastructure/
|
||||
└── pet-store-api/
|
||||
├── client.ts
|
||||
├── generated/
|
||||
├── hooks/
|
||||
│ ├── use-get-pet-list.hook.ts
|
||||
│ ├── use-get-pet-detail.hook.ts
|
||||
│ └── index.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
## Контракт
|
||||
|
||||
- Один GET-хук = один GET-метод клиента.
|
||||
- Имя GET-хука начинается с `useGet`: `useGetPetList`, `useGetPetDetail`.
|
||||
- Имя файла начинается с `use-get`: `use-get-pet-list.hook.ts`.
|
||||
- Хук принимает только параметры GET-метода и `config?: SWRConfiguration`.
|
||||
- Что передали хуку, то он передаёт в GET-метод.
|
||||
- Внутри только SWR-механика: key, fetcher, `useSWR`, `config`.
|
||||
- Хук возвращает тип ответа API: generated-тип или DTO из `types/`.
|
||||
- Хук не объединяет несколько запросов.
|
||||
- Хук не маппит DTO в доменную модель.
|
||||
- Хук не вычисляет бизнес-флаги: `isAuth`, `canEdit`, `hasAccess`, `hasPets`.
|
||||
- Хук не вызывает тосты, модалки, редиректы и не пишет UI-состояние.
|
||||
|
||||
## Пример списка
|
||||
|
||||
```ts
|
||||
// src/infrastructure/pet-store-api/hooks/use-get-pet-list.hook.ts
|
||||
import useSWR from 'swr'
|
||||
import type { SWRConfiguration } from 'swr'
|
||||
import { petStoreApi } from '../client'
|
||||
import type { Pet } from '../generated/pet-store-api.generated'
|
||||
|
||||
export type PetStatus = 'available' | 'pending' | 'sold'
|
||||
|
||||
export const getPetListKey = (status: PetStatus) =>
|
||||
['pet-store-api', 'pet', 'list', status] as const
|
||||
|
||||
/**
|
||||
* Получение списка питомцев по статусу.
|
||||
*/
|
||||
export const useGetPetList = (status: PetStatus | null, config?: SWRConfiguration) => {
|
||||
const isReady = status !== null
|
||||
const key = isReady ? getPetListKey(status) : null
|
||||
const fetcher = () => petStoreApi.pet.findPetsByStatus({ status })
|
||||
|
||||
return useSWR<Pet[]>(key, fetcher, config)
|
||||
}
|
||||
```
|
||||
|
||||
Функция `getPetListKey` нужна, чтобы один и тот же SWR-ключ использовался внутри GET-хука и при передаче начальных данных через `SWRConfig fallback`.
|
||||
|
||||
Пример начальных данных для клиентского хука:
|
||||
|
||||
```tsx
|
||||
import type { ReactNode } from 'react'
|
||||
import { SWRConfig, unstable_serialize } from 'swr'
|
||||
import {
|
||||
getPetListKey,
|
||||
petStoreApi,
|
||||
} from 'infrastructure/pet-store-api'
|
||||
|
||||
export default function PetsLayout({ children }: { children: ReactNode }) {
|
||||
const petsPromise = petStoreApi.pet.findPetsByStatus({ status: 'available' })
|
||||
|
||||
return (
|
||||
<SWRConfig
|
||||
value={{
|
||||
fallback: {
|
||||
[unstable_serialize(getPetListKey('available'))]: petsPromise,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SWRConfig>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Клиентский компонент при этом ничего не знает про preload/fallback и продолжает вызывать обычный хук:
|
||||
|
||||
```tsx
|
||||
const { data: pets } = useGetPetList('available')
|
||||
```
|
||||
|
||||
## Пример detail-запроса
|
||||
|
||||
```ts
|
||||
// src/infrastructure/pet-store-api/hooks/use-get-pet-detail.hook.ts
|
||||
import useSWR from 'swr'
|
||||
import type { SWRConfiguration } from 'swr'
|
||||
import { petStoreApi } from '../client'
|
||||
import type { Pet } from '../generated/pet-store-api.generated'
|
||||
|
||||
export const getPetDetailKey = (id: number) =>
|
||||
['pet-store-api', 'pet', 'detail', id] as const
|
||||
|
||||
/**
|
||||
* Получение питомца по идентификатору.
|
||||
*/
|
||||
export const useGetPetDetail = (id: number | null, config?: SWRConfiguration) => {
|
||||
const isReady = id !== null
|
||||
const key = isReady ? getPetDetailKey(id) : null
|
||||
const fetcher = () => petStoreApi.pet.getPetById(id)
|
||||
|
||||
return useSWR<Pet>(key, fetcher, config)
|
||||
}
|
||||
```
|
||||
|
||||
## Отложенный запрос через `null`
|
||||
|
||||
GET-хук может принимать `null` для обязательного параметра. `null` означает, что параметр ещё не готов и запрос выполнять нельзя.
|
||||
|
||||
Внутри хука это выражается через `isReady`: если параметр не готов, ключ SWR становится `null`, и SWR не вызывает fetcher.
|
||||
|
||||
```ts
|
||||
const isReady = id !== null
|
||||
const key = isReady ? getPetDetailKey(id) : null
|
||||
```
|
||||
|
||||
`null` не передаётся в метод клиента. Key-функция принимает только готовые параметры, поэтому её можно безопасно использовать для начальных данных через `SWRConfig fallback`.
|
||||
|
||||
Для числовых идентификаторов не используйте проверку `if (id)`: значение `0` тоже валидное число. Проверяйте явно: `id !== null`.
|
||||
|
||||
## Экспорт
|
||||
|
||||
```ts
|
||||
// src/infrastructure/pet-store-api/hooks/index.ts
|
||||
export { getPetListKey, useGetPetList } from './use-get-pet-list.hook'
|
||||
export type { PetStatus } from './use-get-pet-list.hook'
|
||||
export { getPetDetailKey, useGetPetDetail } from './use-get-pet-detail.hook'
|
||||
```
|
||||
|
||||
```ts
|
||||
// src/infrastructure/pet-store-api/index.ts
|
||||
export { petStoreApi } from './client'
|
||||
export type { Pet } from './generated/pet-store-api.generated'
|
||||
export * from './hooks'
|
||||
```
|
||||
|
||||
## Где заканчивается infrastructure
|
||||
|
||||
```ts
|
||||
// Хорошо: infrastructure, прозрачный GET-хук
|
||||
const { data: pets } = useGetPetList('available')
|
||||
```
|
||||
|
||||
```ts
|
||||
// Хорошо: business, доменная интерпретация
|
||||
export const useAvailablePets = () => {
|
||||
const query = useGetPetList('available')
|
||||
|
||||
return {
|
||||
...query,
|
||||
hasPets: Boolean(query.data?.length),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`hasPets` — не часть GET-запроса, поэтому он не добавляется в `useGetPetList`.
|
||||
|
||||
## Что запрещено
|
||||
|
||||
```ts
|
||||
// Плохо — useSWR в компоненте
|
||||
const { data } = useSWR(
|
||||
['pet-store-api', 'pet', 'list', status],
|
||||
() => petStoreApi.pet.findPetsByStatus({ status }),
|
||||
)
|
||||
|
||||
// Плохо — несколько GET внутри infrastructure-хука
|
||||
export const usePetDashboard = () => {
|
||||
const available = useGetPetList('available')
|
||||
const sold = useGetPetList('sold')
|
||||
|
||||
return { available, sold }
|
||||
}
|
||||
|
||||
// Плохо — бизнес-флаг внутри GET-хука REST-клиента
|
||||
export const useGetPetList = (status: PetStatus) => {
|
||||
const query = useSWR(...)
|
||||
|
||||
return {
|
||||
...query,
|
||||
hasPets: Boolean(query.data?.length),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Подробное потребление таких хуков описано в стратегии [Клиентский GET-хук](../strategies/client-get-hook.md).
|
||||
75
ai/nextjs-style-guide/data/rest/clients/index.md
Normal file
75
ai/nextjs-style-guide/data/rest/clients/index.md
Normal file
@@ -0,0 +1,75 @@
|
||||
---
|
||||
title: Создание клиента
|
||||
description: Из чего состоит REST-клиент и какие части нужно подготовить перед использованием API.
|
||||
keywords: [rest, клиент, infrastructure, методы, openapi, get-хуки, swr]
|
||||
---
|
||||
|
||||
# Создание клиента
|
||||
|
||||
REST-клиент — это infrastructure-модуль, через который проект работает с внешним REST API.
|
||||
|
||||
На этом этапе нужно подготовить клиент сервиса: создать оболочку клиента, получить методы API и добавить GET-хуки для клиентских компонентов.
|
||||
|
||||
## Из чего состоит клиент
|
||||
|
||||
REST-клиент состоит из трёх основных частей:
|
||||
|
||||
1. **Клиент** — самописная оболочка над транспортом.
|
||||
2. **Методы** — сгенерированные из OpenAPI или написанные вручную вызовы API.
|
||||
3. **GET-хуки** — SWR-обёртки для GET-запросов.
|
||||
|
||||
Эти части живут в одном REST-модуле, потому что относятся к одному внешнему сервису.
|
||||
|
||||
## Клиент
|
||||
|
||||
Клиент — ручной слой, который настраивает работу с API: `baseUrl`, заголовки, авторизацию, обработку ошибок и создание инстанса сервиса.
|
||||
|
||||
Даже если методы генерируются из OpenAPI, `client.ts` остаётся ручным файлом проекта.
|
||||
|
||||
`client.ts` — только сборочная точка клиента. В нём не размещаются DTO, `declare module`, `Extended`-типы, GET-хуки и бизнес-логика.
|
||||
|
||||
## Методы
|
||||
|
||||
Методы описывают конкретные запросы к API.
|
||||
|
||||
Они появляются одним из двух способов:
|
||||
|
||||
- генерируются из OpenAPI в `generated/`;
|
||||
- создаются вручную в `methods/`.
|
||||
|
||||
Подробности:
|
||||
|
||||
- [Автогенерация из OpenAPI](./auto.md)
|
||||
- [Ручное создание](./manual.md)
|
||||
|
||||
## GET-хуки
|
||||
|
||||
Для GET-запросов добавляются GET-хуки REST-клиента.
|
||||
|
||||
Это прозрачные SWR-обёртки над GET-методами клиента. Они живут в `hooks/` этого же REST-модуля и нужны для использования данных в Client Components.
|
||||
|
||||
GET-хуки именуются с префиксом `useGet`: `useGetPetList`, `useGetPetDetail`, `useGetCurrentUser`.
|
||||
|
||||
Подробности:
|
||||
|
||||
- [GET-хуки REST-клиента](./hooks.md)
|
||||
|
||||
## Структура модуля
|
||||
|
||||
```text
|
||||
src/infrastructure/{service-name}/
|
||||
├── client.ts # самописная оболочка и инстанс клиента
|
||||
├── generated/ или methods/ # методы API
|
||||
├── hooks/ # GET-хуки REST-клиента
|
||||
├── types/ # DTO, типы API и расширения типов
|
||||
├── errors/ # ошибки API, если нужны
|
||||
└── index.ts # публичный API
|
||||
```
|
||||
|
||||
`index.ts` — единственная точка входа в REST-модуль для внешнего кода.
|
||||
|
||||
## Что делаем дальше
|
||||
|
||||
1. Создайте методы клиента: [Автогенерация из OpenAPI](./auto.md) или [Ручное создание](./manual.md).
|
||||
2. Добавьте GET-хуки для GET-запросов: [GET-хуки REST-клиента](./hooks.md).
|
||||
3. После создания клиента переходите к [Стратегиям получения данных](../strategies/index.md).
|
||||
187
ai/nextjs-style-guide/data/rest/clients/manual.md
Normal file
187
ai/nextjs-style-guide/data/rest/clients/manual.md
Normal file
@@ -0,0 +1,187 @@
|
||||
---
|
||||
title: Ручное создание
|
||||
description: Создание REST-клиента вручную, когда OpenAPI нет или он неполный.
|
||||
keywords: [rest, ручной клиент, fetch, methods, dto, errors, infrastructure]
|
||||
---
|
||||
|
||||
# Ручное создание
|
||||
|
||||
Ручной REST-клиент используется, когда у API нет OpenAPI-спецификации или она недостаточно точная для автогенерации.
|
||||
|
||||
Задача ручного клиента — дать такую же точку входа, как у автогенерированного клиента: именованный API-объект, методы по сущностям, DTO и GET-хуки рядом с клиентом.
|
||||
|
||||
## Что нужно создать
|
||||
|
||||
```text
|
||||
src/infrastructure/
|
||||
└── pet-project-api/
|
||||
├── methods/
|
||||
│ └── posts.ts
|
||||
├── hooks/
|
||||
│ └── index.ts
|
||||
├── types/
|
||||
│ ├── client.ts
|
||||
│ ├── post.ts
|
||||
│ └── index.ts
|
||||
├── errors/
|
||||
│ └── pet-project-api.error.ts
|
||||
├── client.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
| Файл | Роль |
|
||||
|------|------|
|
||||
| `client.ts` | Базовый транспорт и создание инстанса клиента |
|
||||
| `methods/` | Методы API по сущностям |
|
||||
| `types/` | DTO запросов, ответов и типы клиента |
|
||||
| `errors/` | Ошибки конкретного API |
|
||||
| `hooks/` | GET-хуки REST-клиента, если данные нужны в Client Components |
|
||||
| `index.ts` | Публичный API REST-модуля |
|
||||
|
||||
## DTO и типы API
|
||||
|
||||
DTO запросов и ответов живут в `types/`. `client.ts` не содержит DTO и доменные типы.
|
||||
|
||||
```ts
|
||||
// src/infrastructure/pet-project-api/types/post.ts
|
||||
export type PostDto = {
|
||||
id: string
|
||||
slug: string
|
||||
title: string
|
||||
}
|
||||
|
||||
export type PostListQueryDto = {
|
||||
limit?: number
|
||||
category?: string
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// src/infrastructure/pet-project-api/types/index.ts
|
||||
export type { PostDto, PostListQueryDto } from './post'
|
||||
```
|
||||
|
||||
Типы, которые нужны только базовому транспорту, можно держать отдельно:
|
||||
|
||||
```ts
|
||||
// src/infrastructure/pet-project-api/types/client.ts
|
||||
export type QueryParams = Record<string, string | number | boolean>
|
||||
```
|
||||
|
||||
## Ошибка API
|
||||
|
||||
Ошибка API тоже относится к REST-модулю.
|
||||
|
||||
```ts
|
||||
// src/infrastructure/pet-project-api/errors/pet-project-api.error.ts
|
||||
export class PetProjectApiError extends Error {
|
||||
constructor(
|
||||
public readonly status: number,
|
||||
message: string,
|
||||
) {
|
||||
super(message)
|
||||
this.name = 'PetProjectApiError'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Базовый клиент
|
||||
|
||||
`client.ts` содержит только транспортную оболочку и сборку инстанса. Прямой `fetch` живёт здесь, а не в компонентах и не в методах верхних слоёв.
|
||||
|
||||
```ts
|
||||
// src/infrastructure/pet-project-api/client.ts
|
||||
import { PetProjectApiError } from './errors/pet-project-api.error'
|
||||
import type { QueryParams } from './types/client'
|
||||
|
||||
export class PetProjectApiClient {
|
||||
constructor(
|
||||
private readonly baseUrl: string,
|
||||
private readonly defaultHeaders: Record<string, string> = {},
|
||||
) {}
|
||||
|
||||
async get<T>(path: string, params: QueryParams = {}): Promise<T> {
|
||||
const base = `${this.baseUrl.replace(/\/+$/, '')}/`
|
||||
const url = new URL(path.replace(/^\/+/, ''), base)
|
||||
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
url.searchParams.set(key, String(value))
|
||||
})
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
...this.defaultHeaders,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new PetProjectApiError(response.status, response.statusText)
|
||||
}
|
||||
|
||||
return response.json() as Promise<T>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Это минимальный шаблон. Авторизация, дополнительные заголовки, `next.revalidate`, `post`, `formdata` и другие детали добавляются только когда они реально нужны API.
|
||||
|
||||
## Методы API
|
||||
|
||||
Методы группируются по сущностям в `methods/`. Они не знают про React, SWR и UI.
|
||||
|
||||
```ts
|
||||
// src/infrastructure/pet-project-api/methods/posts.ts
|
||||
import type { PetProjectApiClient } from '../client'
|
||||
import type { PostDto, PostListQueryDto } from '../types/post'
|
||||
|
||||
export function postsMethods(client: PetProjectApiClient) {
|
||||
return {
|
||||
/** GET /posts */
|
||||
list: (query: PostListQueryDto = {}) =>
|
||||
client.get<PostDto[]>('posts', query),
|
||||
|
||||
/** GET /posts/{slug} */
|
||||
get: (slug: string) =>
|
||||
client.get<PostDto>(`posts/${slug}`),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Метод возвращает DTO в форме API. Если данным нужен доменный смысл, маппинг делается выше, в `business/`.
|
||||
|
||||
## Публичный API
|
||||
|
||||
`index.ts` собирает именованный API-объект и открывает наружу только публичные части модуля.
|
||||
|
||||
```ts
|
||||
// src/infrastructure/pet-project-api/index.ts
|
||||
import { PetProjectApiClient } from './client'
|
||||
import { postsMethods } from './methods/posts'
|
||||
|
||||
const client = new PetProjectApiClient(
|
||||
process.env.NEXT_PUBLIC_API_URL ?? '',
|
||||
{ 'Content-Type': 'application/json' },
|
||||
)
|
||||
|
||||
export const petProjectApi = {
|
||||
posts: postsMethods(client),
|
||||
}
|
||||
|
||||
export { PetProjectApiError } from './errors/pet-project-api.error'
|
||||
export type { PostDto, PostListQueryDto } from './types'
|
||||
export * from './hooks'
|
||||
```
|
||||
|
||||
Внешний код импортирует только из `infrastructure/pet-project-api`, не из внутренних файлов модуля.
|
||||
|
||||
## Правила
|
||||
|
||||
- `fetch` используется только внутри базового клиента.
|
||||
- DTO запросов и ответов живут в `types/`.
|
||||
- `client.ts` не содержит DTO, GET-хуки и бизнес-логику.
|
||||
- Методы лежат в `methods/` и возвращают DTO.
|
||||
- GET-хуки добавляются отдельно в `hooks/`, если данные нужны в Client Components.
|
||||
- Доменные типы и маппинг DTO живут не в REST-клиенте, а в `business/`.
|
||||
|
||||
Следующий шаг: [GET-хуки REST-клиента](./hooks.md) или [Стратегии получения данных](../strategies/index.md).
|
||||
74
ai/nextjs-style-guide/data/rest/index.md
Normal file
74
ai/nextjs-style-guide/data/rest/index.md
Normal file
@@ -0,0 +1,74 @@
|
||||
---
|
||||
title: REST
|
||||
description: Как правильно работать с REST API в проекте.
|
||||
keywords: [rest, api, данные, infrastructure, клиент, swr, стратегии]
|
||||
---
|
||||
|
||||
# REST
|
||||
|
||||
Раздел описывает, как правильно работать с REST API в проекте: создать клиент сервиса и выбрать способ получения данных в приложении.
|
||||
|
||||
REST в проекте проходит через два главных этапа:
|
||||
|
||||
1. Создание клиента.
|
||||
2. Использование.
|
||||
|
||||
## 1. Создание клиента
|
||||
|
||||
На этом этапе внешний API оформляется как модуль слоя `infrastructure/`.
|
||||
|
||||
Клиент отвечает за:
|
||||
|
||||
- генерацию или ручное описание методов API;
|
||||
- настройку `baseUrl`;
|
||||
- заголовки и авторизацию;
|
||||
- обработку ошибок;
|
||||
- кастомизацию и расширение типов;
|
||||
- GET-хуки для клиентских компонентов;
|
||||
- публичный API модуля.
|
||||
|
||||
Если у API есть OpenAPI-спецификация — клиент генерируется автоматически. Если OpenAPI нет или он неполный — клиент создаётся вручную.
|
||||
|
||||
GET-хуки относятся к клиенту, потому что это прозрачные SWR-обёртки над GET-методами этого клиента.
|
||||
|
||||
Подробнее:
|
||||
|
||||
- [Создание клиента](./clients/index.md)
|
||||
- [Автогенерация из OpenAPI](./clients/auto.md)
|
||||
- [Ручное создание](./clients/manual.md)
|
||||
- [GET-хуки REST-клиента](./clients/hooks.md)
|
||||
|
||||
## 2. Использование
|
||||
|
||||
После создания клиента нужно определить рендер страницы и выбрать, как получать данные в конкретном месте приложения.
|
||||
|
||||
Раздел использования отвечает на вопросы:
|
||||
|
||||
- как понять, можно ли сохранить static/ISR;
|
||||
- когда страница становится dynamic/SSR;
|
||||
- когда получать данные через серверный `await`;
|
||||
- когда запускать несколько серверных запросов параллельно;
|
||||
- когда передавать промис ниже по дереву;
|
||||
- когда передавать начальные данные клиентским GET-хукам;
|
||||
- когда использовать GET-хук в клиентском компоненте;
|
||||
- когда выносить композицию и бизнес-смысл в `business/`.
|
||||
|
||||
Подробнее:
|
||||
|
||||
- [Стратегии получения данных](./strategies/index.md)
|
||||
- [Серверный await](./strategies/server-await.md)
|
||||
- [Параллельные серверные запросы](./strategies/parallel-server-requests.md)
|
||||
- [Передача промиса ниже](./strategies/pass-promise-down.md)
|
||||
- [Начальные данные для клиентских хуков](./strategies/client-hooks-initial-data.md)
|
||||
- [Клиентский GET-хук](./strategies/client-get-hook.md)
|
||||
- [Business-композиция](./strategies/business-composition.md)
|
||||
|
||||
## Как читать раздел
|
||||
|
||||
Если API ещё не подключён — начните с [Создания клиента](./clients/index.md).
|
||||
|
||||
Если клиент уже есть, но непонятно как получить данные — начните со [Стратегий получения данных](./strategies/index.md).
|
||||
|
||||
Если данные нужны в Client Component — сначала проверьте, есть ли [GET-хук REST-клиента](./clients/hooks.md).
|
||||
|
||||
Если в коде появляется бизнес-смысл вроде `isAuth`, `canEdit`, `hasAccess` — это уже не REST-клиент, а `business/`.
|
||||
@@ -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