docs: отказаться от FC и interface в пропсах компонентов
- переписана типизация: type вместо interface, убран FC - введена система типов: Params + RootAttrs + Props - добавлены обоснования: почему type, почему не FC, почему types/ - обновлены примеры в components, page-level, templates, documentation - убраны упоминания FC из таблиц index.md и README - перегенерированы RULES.md
This commit is contained in:
@@ -30,7 +30,7 @@ Rules and standards for NextJS and TypeScript development: architecture, typing,
|
|||||||
| Code Style | How to format code: indentation, quotes, imports, early return? |
|
| Code Style | How to format code: indentation, quotes, imports, early return? |
|
||||||
| Naming | How to name files, variables, components, hooks? |
|
| Naming | How to name files, variables, components, hooks? |
|
||||||
| Documentation | How to write JSDoc: what to document and what not? |
|
| Documentation | How to write JSDoc: what to document and what not? |
|
||||||
| Typing | How to type: type vs interface, any/unknown, FC? |
|
| Typing | How to type: type vs interface, any/unknown? |
|
||||||
|
|
||||||
### Applied Sections
|
### Applied Sections
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ Rules and standards for NextJS and TypeScript development: architecture, typing,
|
|||||||
| Section | Answers the question |
|
| Section | Answers the question |
|
||||||
|---------|---------------------|
|
|---------|---------------------|
|
||||||
| Project Structure | How are folders and files organized by FSD? |
|
| Project Structure | How are folders and files organized by FSD? |
|
||||||
| Components | How is a component structured: files, props, clsx, FC? |
|
| Components | How is a component structured: files, props, clsx? |
|
||||||
| Page-level Components | How to define layout, page, loading, error, not-found? |
|
| Page-level Components | How to define layout, page, loading, error, not-found? |
|
||||||
| Templates & Code Generation | How do templates work: syntax, variables, modifiers? |
|
| Templates & Code Generation | How do templates work: syntax, variables, modifiers? |
|
||||||
| Styles | How to write CSS: PostCSS Modules, nesting, media, tokens? |
|
| Styles | How to write CSS: PostCSS Modules, nesting, media, tokens? |
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
| Стиль кода | Как оформлять код: отступы, кавычки, импорты, early return? |
|
| Стиль кода | Как оформлять код: отступы, кавычки, импорты, early return? |
|
||||||
| Именование | Как называть файлы, переменные, компоненты, хуки? |
|
| Именование | Как называть файлы, переменные, компоненты, хуки? |
|
||||||
| Документирование | Как писать JSDoc: что документировать, а что нет? |
|
| Документирование | Как писать JSDoc: что документировать, а что нет? |
|
||||||
| Типизация | Как типизировать: type vs interface, any/unknown, FC? |
|
| Типизация | Как типизировать: type vs interface, any/unknown? |
|
||||||
|
|
||||||
### Прикладные разделы
|
### Прикладные разделы
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
|--------|-------------------|
|
|--------|-------------------|
|
||||||
| Настройка VS Code | Как настроить редактор для проекта? |
|
| Настройка VS Code | Как настроить редактор для проекта? |
|
||||||
| Структура проекта | Как организованы папки и файлы по FSD? |
|
| Структура проекта | Как организованы папки и файлы по FSD? |
|
||||||
| Компоненты | Как устроен компонент: файлы, пропсы, clsx, FC? |
|
| Компоненты | Как устроен компонент: файлы, пропсы, clsx? |
|
||||||
| Page-level компоненты | Как описывать layout, page, loading, error, not-found? |
|
| Page-level компоненты | Как описывать layout, page, loading, error, not-found? |
|
||||||
| Шаблоны и генерация кода | Как работают шаблоны, синтаксис и инструменты генерации? |
|
| Шаблоны и генерация кода | Как работают шаблоны, синтаксис и инструменты генерации? |
|
||||||
| Стили | Как писать CSS: PostCSS Modules, вложенность, медиа, токены? |
|
| Стили | Как писать CSS: PostCSS Modules, вложенность, медиа, токены? |
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ Rules and standards for NextJS and TypeScript development: architecture, typing,
|
|||||||
| Code Style | How to format code: indentation, quotes, imports, early return? |
|
| Code Style | How to format code: indentation, quotes, imports, early return? |
|
||||||
| Naming | How to name files, variables, components, hooks? |
|
| Naming | How to name files, variables, components, hooks? |
|
||||||
| Documentation | How to write JSDoc: what to document and what not? |
|
| Documentation | How to write JSDoc: what to document and what not? |
|
||||||
| Typing | How to type: type vs interface, any/unknown, FC? |
|
| Typing | How to type: type vs interface, any/unknown? |
|
||||||
|
|
||||||
### Applied Sections
|
### Applied Sections
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ Rules and standards for NextJS and TypeScript development: architecture, typing,
|
|||||||
| Section | Answers the question |
|
| Section | Answers the question |
|
||||||
|---------|---------------------|
|
|---------|---------------------|
|
||||||
| Project Structure | How are folders and files organized by FSD? |
|
| Project Structure | How are folders and files organized by FSD? |
|
||||||
| Components | How is a component structured: files, props, clsx, FC? |
|
| Components | How is a component structured: files, props, clsx? |
|
||||||
| Page-level Components | How to define layout, page, loading, error, not-found? |
|
| Page-level Components | How to define layout, page, loading, error, not-found? |
|
||||||
| Templates & Code Generation | How do templates work: syntax, variables, modifiers? |
|
| Templates & Code Generation | How do templates work: syntax, variables, modifiers? |
|
||||||
| Styles | How to write CSS: PostCSS Modules, nesting, media, tokens? |
|
| Styles | How to write CSS: PostCSS Modules, nesting, media, tokens? |
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ container/
|
|||||||
├── styles/
|
├── styles/
|
||||||
│ └── container.module.css
|
│ └── container.module.css
|
||||||
├── types/
|
├── types/
|
||||||
│ └── container.interface.ts
|
│ └── container.type.ts
|
||||||
├── container.tsx
|
├── container.tsx
|
||||||
└── index.ts
|
└── index.ts
|
||||||
```
|
```
|
||||||
@@ -33,13 +33,17 @@ container/
|
|||||||
## Именования
|
## Именования
|
||||||
|
|
||||||
- Имя корневого css класса всегда `.root`
|
- Имя корневого css класса всегда `.root`
|
||||||
- Интерфейс именуется `{ComponentName}Props`.
|
- Тип пропсов именуется `{ComponentName}Props`.
|
||||||
|
- Тип пользовательских параметров именуется `{ComponentName}Params`.
|
||||||
|
|
||||||
## Типизация
|
## Типизация
|
||||||
|
|
||||||
- Компонент типизируется через `FC<Props>`.
|
Структура типов компонента показана в [примере](#пример). Ниже — обоснования ключевых решений.
|
||||||
- Интерфейс пропсов наследует HTML-атрибуты своего корневого элемента.
|
|
||||||
- `children` отдельно не объявляется — приходит из `HTMLAttributes`.
|
- **`type` вместо `interface`** — гибче для пропсов: поддерживает union, intersection, mapped types. Declaration merging пропсам не нужно.
|
||||||
|
- **Без `FC`** — неявно добавляет `children`, усложняет дженерики, не даёт преимуществ перед аннотацией параметра.
|
||||||
|
- **Типы в `types/`, а не в `.tsx`** — предотвращает циклические зависимости (компонент импортирует хук, хук импортирует тип из компонента) и разделяет ответственность: `.tsx` для рендера, `.type.ts` для данных.
|
||||||
|
- **Без возвращаемого типа** — TypeScript выводит из JSX. Осознанное исключение из [базового правила](/basics/typing).
|
||||||
|
|
||||||
## Реализация
|
## Реализация
|
||||||
|
|
||||||
@@ -50,7 +54,7 @@ container/
|
|||||||
|
|
||||||
## Пример
|
## Пример
|
||||||
|
|
||||||
`container/types/container.interface.ts`
|
`container/types/container.type.ts`
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import type { HTMLAttributes } from 'react'
|
import type { HTMLAttributes } from 'react'
|
||||||
@@ -58,7 +62,12 @@ import type { HTMLAttributes } from 'react'
|
|||||||
/**
|
/**
|
||||||
* Параметры компонента Container.
|
* Параметры компонента Container.
|
||||||
*/
|
*/
|
||||||
export interface ContainerProps extends HTMLAttributes<HTMLDivElement> {}
|
export type ContainerParams = {}
|
||||||
|
|
||||||
|
/** HTML-атрибуты корневого элемента. */
|
||||||
|
type RootAttrs = HTMLAttributes<HTMLDivElement>
|
||||||
|
|
||||||
|
export type ContainerProps = RootAttrs & ContainerParams
|
||||||
```
|
```
|
||||||
|
|
||||||
`container/styles/container.module.css`
|
`container/styles/container.module.css`
|
||||||
@@ -74,9 +83,8 @@ export interface ContainerProps extends HTMLAttributes<HTMLDivElement> {}
|
|||||||
`container/container.tsx`
|
`container/container.tsx`
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import type { FC } from 'react'
|
|
||||||
import cl from 'clsx'
|
import cl from 'clsx'
|
||||||
import type { ContainerProps } from './types/container.interface'
|
import type { ContainerProps } from './types/container.type'
|
||||||
import styles from './styles/container.module.css'
|
import styles from './styles/container.module.css'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -86,7 +94,7 @@ import styles from './styles/container.module.css'
|
|||||||
* - обёртки контента страниц с ограничением ширины
|
* - обёртки контента страниц с ограничением ширины
|
||||||
* - центрирования блоков в лейауте
|
* - центрирования блоков в лейауте
|
||||||
*/
|
*/
|
||||||
export const Container: FC<ContainerProps> = (props) => {
|
export const Container = (props: ContainerProps) => {
|
||||||
const { children, className, ...htmlAttr } = props
|
const { children, className, ...htmlAttr } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -102,4 +110,3 @@ export const Container: FC<ContainerProps> = (props) => {
|
|||||||
```ts
|
```ts
|
||||||
export { Container } from './container'
|
export { Container } from './container'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export const metadata: Metadata = {
|
|||||||
description: 'Страница профиля пользователя',
|
description: 'Страница профиля пользователя',
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProfilePageProps {
|
type ProfilePageProps = {
|
||||||
params: Promise<{ id: string }>
|
params: Promise<{ id: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,15 +47,14 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
|
|||||||
```tsx
|
```tsx
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import type { FC } from 'react'
|
|
||||||
import { ErrorScreen } from '@/screens/error'
|
import { ErrorScreen } from '@/screens/error'
|
||||||
|
|
||||||
interface ErrorPageProps {
|
type ErrorPageProps = {
|
||||||
error: Error & { digest?: string }
|
error: Error & { digest?: string }
|
||||||
reset: () => void
|
reset: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const ErrorPage: FC<ErrorPageProps> = ({ error, reset }) => {
|
const ErrorPage = ({ error, reset }: ErrorPageProps) => {
|
||||||
return <ErrorScreen error={error} reset={reset} />
|
return <ErrorScreen error={error} reset={reset} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ title: Шаблоны и генерация кода
|
|||||||
│ ├── styles/
|
│ ├── styles/
|
||||||
│ │ └── {{name.kebabCase}}.module.css
|
│ │ └── {{name.kebabCase}}.module.css
|
||||||
│ ├── types/
|
│ ├── types/
|
||||||
│ │ └── {{name.kebabCase}}.interface.ts
|
│ │ └── {{name.kebabCase}}.type.ts
|
||||||
│ ├── {{name.kebabCase}}.tsx
|
│ ├── {{name.kebabCase}}.tsx
|
||||||
│ └── index.ts
|
│ └── index.ts
|
||||||
└── store/ # шаблон Zustand стора
|
└── store/ # шаблон Zustand стора
|
||||||
@@ -86,26 +86,30 @@ export { {{name.pascalCase}} } from './{{name.kebabCase}}'
|
|||||||
```
|
```
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// .templates/component/types/{{name.kebabCase}}.interface.ts
|
// .templates/component/types/{{name.kebabCase}}.type.ts
|
||||||
import type { HTMLAttributes } from 'react'
|
import type { HTMLAttributes } from 'react'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Параметры {{name.pascalCase}}.
|
* Параметры {{name.pascalCase}}.
|
||||||
*/
|
*/
|
||||||
export interface {{name.pascalCase}}Props extends HTMLAttributes<HTMLDivElement> {}
|
export type {{name.pascalCase}}Params = {}
|
||||||
|
|
||||||
|
/** HTML-атрибуты корневого элемента. */
|
||||||
|
type RootAttrs = HTMLAttributes<HTMLDivElement>
|
||||||
|
|
||||||
|
export type {{name.pascalCase}}Props = RootAttrs & {{name.pascalCase}}Params
|
||||||
```
|
```
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
// .templates/component/{{name.kebabCase}}.tsx
|
// .templates/component/{{name.kebabCase}}.tsx
|
||||||
import type { FC } from 'react'
|
|
||||||
import cl from 'clsx'
|
import cl from 'clsx'
|
||||||
import type { {{name.pascalCase}}Props } from './types/{{name.kebabCase}}.interface'
|
import type { {{name.pascalCase}}Props } from './types/{{name.kebabCase}}.type'
|
||||||
import styles from './styles/{{name.kebabCase}}.module.css'
|
import styles from './styles/{{name.kebabCase}}.module.css'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {{name.pascalCase}}.
|
* {{name.pascalCase}}.
|
||||||
*/
|
*/
|
||||||
export const {{name.pascalCase}}: FC<{{name.pascalCase}}Props> = (props) => {
|
export const {{name.pascalCase}} = (props: {{name.pascalCase}}Props) => {
|
||||||
const { children, className, ...htmlAttr } = props
|
const { children, className, ...htmlAttr } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export const buildCategoryTree = (categories: Category[]): CategoryTree[] => { .
|
|||||||
* - обёртки контента страниц с ограничением ширины
|
* - обёртки контента страниц с ограничением ширины
|
||||||
* - центрирования блоков в лейауте
|
* - центрирования блоков в лейауте
|
||||||
*/
|
*/
|
||||||
export const Container: FC<ContainerProps> = (props) => { ... }
|
export const Container = (props: ContainerProps) => { ... }
|
||||||
```
|
```
|
||||||
|
|
||||||
**Плохо**
|
**Плохо**
|
||||||
@@ -90,7 +90,7 @@ export const Container: FC<ContainerProps> = (props) => { ... }
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Плохо: нет описания вообще.
|
// Плохо: нет описания вообще.
|
||||||
export const Container: FC<ContainerProps> = (props) => { ... }
|
export const Container = (props: ContainerProps) => { ... }
|
||||||
```
|
```
|
||||||
|
|
||||||
## Типы, интерфейсы, enum
|
## Типы, интерфейсы, enum
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
| Стиль кода | Как оформлять код: отступы, кавычки, импорты, early return? |
|
| Стиль кода | Как оформлять код: отступы, кавычки, импорты, early return? |
|
||||||
| Именование | Как называть файлы, переменные, компоненты, хуки? |
|
| Именование | Как называть файлы, переменные, компоненты, хуки? |
|
||||||
| Документирование | Как писать JSDoc: что документировать, а что нет? |
|
| Документирование | Как писать JSDoc: что документировать, а что нет? |
|
||||||
| Типизация | Как типизировать: type vs interface, any/unknown, FC? |
|
| Типизация | Как типизировать: type vs interface, any/unknown? |
|
||||||
|
|
||||||
### Прикладные разделы
|
### Прикладные разделы
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
|--------|-------------------|
|
|--------|-------------------|
|
||||||
| Настройка VS Code | Как настроить редактор для проекта? |
|
| Настройка VS Code | Как настроить редактор для проекта? |
|
||||||
| Структура проекта | Как организованы папки и файлы по FSD? |
|
| Структура проекта | Как организованы папки и файлы по FSD? |
|
||||||
| Компоненты | Как устроен компонент: файлы, пропсы, clsx, FC? |
|
| Компоненты | Как устроен компонент: файлы, пропсы, clsx? |
|
||||||
| Page-level компоненты | Как описывать layout, page, loading, error, not-found? |
|
| Page-level компоненты | Как описывать layout, page, loading, error, not-found? |
|
||||||
| Шаблоны и генерация кода | Как работают шаблоны, синтаксис и инструменты генерации? |
|
| Шаблоны и генерация кода | Как работают шаблоны, синтаксис и инструменты генерации? |
|
||||||
| Стили | Как писать CSS: PostCSS Modules, вложенность, медиа, токены? |
|
| Стили | Как писать CSS: PostCSS Modules, вложенность, медиа, токены? |
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ Rules and standards for NextJS and TypeScript development: architecture, typing,
|
|||||||
| Code Style | How to format code: indentation, quotes, imports, early return? |
|
| Code Style | How to format code: indentation, quotes, imports, early return? |
|
||||||
| Naming | How to name files, variables, components, hooks? |
|
| Naming | How to name files, variables, components, hooks? |
|
||||||
| Documentation | How to write JSDoc: what to document and what not? |
|
| Documentation | How to write JSDoc: what to document and what not? |
|
||||||
| Typing | How to type: type vs interface, any/unknown, FC? |
|
| Typing | How to type: type vs interface, any/unknown? |
|
||||||
|
|
||||||
### Applied Sections
|
### Applied Sections
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ Rules and standards for NextJS and TypeScript development: architecture, typing,
|
|||||||
| Section | Answers the question |
|
| Section | Answers the question |
|
||||||
|---------|---------------------|
|
|---------|---------------------|
|
||||||
| Project Structure | How are folders and files organized by FSD? |
|
| Project Structure | How are folders and files organized by FSD? |
|
||||||
| Components | How is a component structured: files, props, clsx, FC? |
|
| Components | How is a component structured: files, props, clsx? |
|
||||||
| Page-level Components | How to define layout, page, loading, error, not-found? |
|
| Page-level Components | How to define layout, page, loading, error, not-found? |
|
||||||
| Templates & Code Generation | How do templates work: syntax, variables, modifiers? |
|
| Templates & Code Generation | How do templates work: syntax, variables, modifiers? |
|
||||||
| Styles | How to write CSS: PostCSS Modules, nesting, media, tokens? |
|
| Styles | How to write CSS: PostCSS Modules, nesting, media, tokens? |
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
| Стиль кода | Как оформлять код: отступы, кавычки, импорты, early return? |
|
| Стиль кода | Как оформлять код: отступы, кавычки, импорты, early return? |
|
||||||
| Именование | Как называть файлы, переменные, компоненты, хуки? |
|
| Именование | Как называть файлы, переменные, компоненты, хуки? |
|
||||||
| Документирование | Как писать JSDoc: что документировать, а что нет? |
|
| Документирование | Как писать JSDoc: что документировать, а что нет? |
|
||||||
| Типизация | Как типизировать: type vs interface, any/unknown, FC? |
|
| Типизация | Как типизировать: type vs interface, any/unknown? |
|
||||||
|
|
||||||
### Прикладные разделы
|
### Прикладные разделы
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
|--------|-------------------|
|
|--------|-------------------|
|
||||||
| Настройка VS Code | Как настроить редактор для проекта? |
|
| Настройка VS Code | Как настроить редактор для проекта? |
|
||||||
| Структура проекта | Как организованы папки и файлы по FSD? |
|
| Структура проекта | Как организованы папки и файлы по FSD? |
|
||||||
| Компоненты | Как устроен компонент: файлы, пропсы, clsx, FC? |
|
| Компоненты | Как устроен компонент: файлы, пропсы, clsx? |
|
||||||
| Page-level компоненты | Как описывать layout, page, loading, error, not-found? |
|
| Page-level компоненты | Как описывать layout, page, loading, error, not-found? |
|
||||||
| Шаблоны и генерация кода | Как работают шаблоны, синтаксис и инструменты генерации? |
|
| Шаблоны и генерация кода | Как работают шаблоны, синтаксис и инструменты генерации? |
|
||||||
| Стили | Как писать CSS: PostCSS Modules, вложенность, медиа, токены? |
|
| Стили | Как писать CSS: PostCSS Modules, вложенность, медиа, токены? |
|
||||||
@@ -616,7 +616,7 @@ export const buildCategoryTree = (categories: Category[]): CategoryTree[] => { .
|
|||||||
* - обёртки контента страниц с ограничением ширины
|
* - обёртки контента страниц с ограничением ширины
|
||||||
* - центрирования блоков в лейауте
|
* - центрирования блоков в лейауте
|
||||||
*/
|
*/
|
||||||
export const Container: FC<ContainerProps> = (props) => { ... }
|
export const Container = (props: ContainerProps) => { ... }
|
||||||
```
|
```
|
||||||
|
|
||||||
**Плохо**
|
**Плохо**
|
||||||
@@ -627,7 +627,7 @@ export const Container: FC<ContainerProps> = (props) => { ... }
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Плохо: нет описания вообще.
|
// Плохо: нет описания вообще.
|
||||||
export const Container: FC<ContainerProps> = (props) => { ... }
|
export const Container = (props: ContainerProps) => { ... }
|
||||||
```
|
```
|
||||||
|
|
||||||
### Типы, интерфейсы, enum
|
### Типы, интерфейсы, enum
|
||||||
@@ -845,7 +845,7 @@ container/
|
|||||||
├── styles/
|
├── styles/
|
||||||
│ └── container.module.css
|
│ └── container.module.css
|
||||||
├── types/
|
├── types/
|
||||||
│ └── container.interface.ts
|
│ └── container.type.ts
|
||||||
├── container.tsx
|
├── container.tsx
|
||||||
└── index.ts
|
└── index.ts
|
||||||
```
|
```
|
||||||
@@ -853,13 +853,17 @@ container/
|
|||||||
### Именования
|
### Именования
|
||||||
|
|
||||||
- Имя корневого css класса всегда `.root`
|
- Имя корневого css класса всегда `.root`
|
||||||
- Интерфейс именуется `{ComponentName}Props`.
|
- Тип пропсов именуется `{ComponentName}Props`.
|
||||||
|
- Тип пользовательских параметров именуется `{ComponentName}Params`.
|
||||||
|
|
||||||
### Типизация
|
### Типизация
|
||||||
|
|
||||||
- Компонент типизируется через `FC<Props>`.
|
Структура типов компонента показана в [примере](#пример). Ниже — обоснования ключевых решений.
|
||||||
- Интерфейс пропсов наследует HTML-атрибуты своего корневого элемента.
|
|
||||||
- `children` отдельно не объявляется — приходит из `HTMLAttributes`.
|
- **`type` вместо `interface`** — гибче для пропсов: поддерживает union, intersection, mapped types. Declaration merging пропсам не нужно.
|
||||||
|
- **Без `FC`** — неявно добавляет `children`, усложняет дженерики, не даёт преимуществ перед аннотацией параметра.
|
||||||
|
- **Типы в `types/`, а не в `.tsx`** — предотвращает циклические зависимости (компонент импортирует хук, хук импортирует тип из компонента) и разделяет ответственность: `.tsx` для рендера, `.type.ts` для данных.
|
||||||
|
- **Без возвращаемого типа** — TypeScript выводит из JSX. Осознанное исключение из [базового правила](/basics/typing).
|
||||||
|
|
||||||
### Реализация
|
### Реализация
|
||||||
|
|
||||||
@@ -870,7 +874,7 @@ container/
|
|||||||
|
|
||||||
### Пример
|
### Пример
|
||||||
|
|
||||||
`container/types/container.interface.ts`
|
`container/types/container.type.ts`
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import type { HTMLAttributes } from 'react'
|
import type { HTMLAttributes } from 'react'
|
||||||
@@ -878,7 +882,12 @@ import type { HTMLAttributes } from 'react'
|
|||||||
/**
|
/**
|
||||||
* Параметры компонента Container.
|
* Параметры компонента Container.
|
||||||
*/
|
*/
|
||||||
export interface ContainerProps extends HTMLAttributes<HTMLDivElement> {}
|
export type ContainerParams = {}
|
||||||
|
|
||||||
|
/** HTML-атрибуты корневого элемента. */
|
||||||
|
type RootAttrs = HTMLAttributes<HTMLDivElement>
|
||||||
|
|
||||||
|
export type ContainerProps = RootAttrs & ContainerParams
|
||||||
```
|
```
|
||||||
|
|
||||||
`container/styles/container.module.css`
|
`container/styles/container.module.css`
|
||||||
@@ -894,9 +903,8 @@ export interface ContainerProps extends HTMLAttributes<HTMLDivElement> {}
|
|||||||
`container/container.tsx`
|
`container/container.tsx`
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import type { FC } from 'react'
|
|
||||||
import cl from 'clsx'
|
import cl from 'clsx'
|
||||||
import type { ContainerProps } from './types/container.interface'
|
import type { ContainerProps } from './types/container.type'
|
||||||
import styles from './styles/container.module.css'
|
import styles from './styles/container.module.css'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -906,7 +914,7 @@ import styles from './styles/container.module.css'
|
|||||||
* - обёртки контента страниц с ограничением ширины
|
* - обёртки контента страниц с ограничением ширины
|
||||||
* - центрирования блоков в лейауте
|
* - центрирования блоков в лейауте
|
||||||
*/
|
*/
|
||||||
export const Container: FC<ContainerProps> = (props) => {
|
export const Container = (props: ContainerProps) => {
|
||||||
const { children, className, ...htmlAttr } = props
|
const { children, className, ...htmlAttr } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -924,100 +932,25 @@ export { Container } from './container'
|
|||||||
```
|
```
|
||||||
|
|
||||||
<!-- /applied/page-level -->
|
<!-- /applied/page-level -->
|
||||||
## Страницы (App Router)
|
## Файлы роутинга
|
||||||
|
|
||||||
Специальные файлы Next.js App Router, которые фреймворк использует по соглашению: `layout.tsx`, `page.tsx`, `loading.tsx`, `error.tsx`, `not-found.tsx`, `template.tsx`.
|
Правила для специальных файлов App Router (`page.tsx`, `layout.tsx`, `error.tsx`, `not-found.tsx` и др.) — чем наш подход отличается от дефолтного.
|
||||||
|
|
||||||
### Общие правила
|
### Организация
|
||||||
|
|
||||||
- Экспорт через `export default function` — конвенция Next.js.
|
- `page.tsx` — тонкий файл: только `metadata` и рендер экрана. Логика, стили и зависимости живут в экране, не в `page.tsx`.
|
||||||
- Типизация через `PropsWithChildren` или явный интерфейс.
|
- `error.tsx` и `not-found.tsx` делегируют разметку экранам по тому же принципу.
|
||||||
- Каждая страница (`page.tsx`) должна содержать `metadata` с `title` и `description`.
|
- `layout.tsx` — точка подключения провайдеров и глобальных стилей. Вёрстка layout-обёрток выносится в слой `layouts/`.
|
||||||
- Минимум логики — page-level компоненты делегируют работу экранам, виджетам и фичам.
|
- Стили в файлах роутинга не используются — стилизация только внутри вызываемых компонентов.
|
||||||
- Стили в page-level компонентах не используются — стилизация внутри вызываемых компонентов.
|
|
||||||
|
|
||||||
### layout.tsx
|
### Реализация
|
||||||
|
|
||||||
Корневой layout — точка подключения провайдеров, глобальных стилей и метаданных.
|
- Каждый `page.tsx` экспортирует `metadata` с `title` — он подставляется в шаблон корневого layout (`%s | App`).
|
||||||
|
- Корневой `layout.tsx` задаёт `metadata` с `title.template`, `description`, `metadataBase` и OpenGraph-настройками.
|
||||||
|
|
||||||
```tsx
|
### Примеры
|
||||||
import type { PropsWithChildren } from 'react'
|
|
||||||
import type { Metadata } from 'next'
|
|
||||||
import { Providers } from './providers'
|
|
||||||
import './styles/index.css'
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
`src/app/profile/[id]/page.tsx`
|
||||||
title: {
|
|
||||||
default: 'App',
|
|
||||||
template: '%s | App',
|
|
||||||
},
|
|
||||||
description: 'Описание приложения',
|
|
||||||
metadataBase: new URL('https://example.com'),
|
|
||||||
openGraph: {
|
|
||||||
type: 'website',
|
|
||||||
locale: 'ru_RU',
|
|
||||||
siteName: 'App',
|
|
||||||
images: [
|
|
||||||
{
|
|
||||||
url: '/og-image.png',
|
|
||||||
width: 1200,
|
|
||||||
height: 630,
|
|
||||||
alt: 'App',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
twitter: {
|
|
||||||
card: 'summary_large_image',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function RootLayout({ children }: PropsWithChildren) {
|
|
||||||
return (
|
|
||||||
<html lang="ru" suppressHydrationWarning>
|
|
||||||
<body>
|
|
||||||
<Providers>
|
|
||||||
{children}
|
|
||||||
</Providers>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Вложенный layout — для секции с общей обёрткой (sidebar, header):
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import type { PropsWithChildren } from 'react'
|
|
||||||
import { DashboardLayout } from '@/shared/ui/dashboard-layout'
|
|
||||||
|
|
||||||
export default function Layout({ children }: PropsWithChildren) {
|
|
||||||
return (
|
|
||||||
<DashboardLayout>
|
|
||||||
{children}
|
|
||||||
</DashboardLayout>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### page.tsx
|
|
||||||
|
|
||||||
Тонкий файл — только импорт и рендер экрана. Логика, стили и зависимости размещаются в экране, не в `page.tsx`.
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import type { Metadata } from 'next'
|
|
||||||
import { HomeScreen } from '@/screens/home'
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: 'Главная',
|
|
||||||
description: 'Главная страница приложения',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function HomePage() {
|
|
||||||
return <HomeScreen />
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
С параметрами маршрута:
|
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
@@ -1028,7 +961,7 @@ export const metadata: Metadata = {
|
|||||||
description: 'Страница профиля пользователя',
|
description: 'Страница профиля пользователя',
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProfilePageProps {
|
type ProfilePageProps = {
|
||||||
params: Promise<{ id: string }>
|
params: Promise<{ id: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1039,70 +972,25 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Каждая страница должна содержать `metadata` с `title` — он подставится в шаблон из корневого layout: `Профиль | App`.
|
`src/app/error.tsx`
|
||||||
|
|
||||||
### loading.tsx
|
|
||||||
|
|
||||||
Состояние загрузки. Показывается пока загружается контент страницы.
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
export default function Loading() {
|
|
||||||
return <div>Загрузка...</div>
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### error.tsx
|
|
||||||
|
|
||||||
Обработка ошибок. Обязательно `'use client'` — error boundary работает только на клиенте. Разметку выносим в экран.
|
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import type { FC } from 'react'
|
|
||||||
import { ErrorScreen } from '@/screens/error'
|
import { ErrorScreen } from '@/screens/error'
|
||||||
|
|
||||||
interface ErrorPageProps {
|
type ErrorPageProps = {
|
||||||
error: Error & { digest?: string }
|
error: Error & { digest?: string }
|
||||||
reset: () => void
|
reset: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const ErrorPage: FC<ErrorPageProps> = ({ error, reset }) => {
|
const ErrorPage = ({ error, reset }: ErrorPageProps) => {
|
||||||
return <ErrorScreen error={error} reset={reset} />
|
return <ErrorScreen error={error} reset={reset} />
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ErrorPage
|
export default ErrorPage
|
||||||
```
|
```
|
||||||
|
|
||||||
### not-found.tsx
|
|
||||||
|
|
||||||
Страница 404. Показывается когда маршрут не найден. Разметку выносим в экран.
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import type { Metadata } from 'next'
|
|
||||||
import { NotFoundScreen } from '@/screens/not-found'
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: 'Страница не найдена',
|
|
||||||
description: 'Запрашиваемая страница не существует',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function NotFound() {
|
|
||||||
return <NotFoundScreen />
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### template.tsx
|
|
||||||
|
|
||||||
Аналог layout, но пересоздаётся при каждой навигации (не сохраняет состояние). Используется редко — для анимаций переходов между страницами.
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import type { PropsWithChildren } from 'react'
|
|
||||||
|
|
||||||
export default function Template({ children }: PropsWithChildren) {
|
|
||||||
return <div>{children}</div>
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
<!-- /applied/templates-generation -->
|
<!-- /applied/templates-generation -->
|
||||||
<!-- @formatter:off -->
|
<!-- @formatter:off -->
|
||||||
::: v-pre
|
::: v-pre
|
||||||
@@ -1122,7 +1010,7 @@ export default function Template({ children }: PropsWithChildren) {
|
|||||||
│ ├── styles/
|
│ ├── styles/
|
||||||
│ │ └── {{name.kebabCase}}.module.css
|
│ │ └── {{name.kebabCase}}.module.css
|
||||||
│ ├── types/
|
│ ├── types/
|
||||||
│ │ └── {{name.kebabCase}}.interface.ts
|
│ │ └── {{name.kebabCase}}.type.ts
|
||||||
│ ├── {{name.kebabCase}}.tsx
|
│ ├── {{name.kebabCase}}.tsx
|
||||||
│ └── index.ts
|
│ └── index.ts
|
||||||
└── store/ # шаблон Zustand стора
|
└── store/ # шаблон Zustand стора
|
||||||
@@ -1188,26 +1076,30 @@ export { {{name.pascalCase}} } from './{{name.kebabCase}}'
|
|||||||
```
|
```
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// .templates/component/types/{{name.kebabCase}}.interface.ts
|
// .templates/component/types/{{name.kebabCase}}.type.ts
|
||||||
import type { HTMLAttributes } from 'react'
|
import type { HTMLAttributes } from 'react'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Параметры {{name.pascalCase}}.
|
* Параметры {{name.pascalCase}}.
|
||||||
*/
|
*/
|
||||||
export interface {{name.pascalCase}}Props extends HTMLAttributes<HTMLDivElement> {}
|
export type {{name.pascalCase}}Params = {}
|
||||||
|
|
||||||
|
/** HTML-атрибуты корневого элемента. */
|
||||||
|
type RootAttrs = HTMLAttributes<HTMLDivElement>
|
||||||
|
|
||||||
|
export type {{name.pascalCase}}Props = RootAttrs & {{name.pascalCase}}Params
|
||||||
```
|
```
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
// .templates/component/{{name.kebabCase}}.tsx
|
// .templates/component/{{name.kebabCase}}.tsx
|
||||||
import type { FC } from 'react'
|
|
||||||
import cl from 'clsx'
|
import cl from 'clsx'
|
||||||
import type { {{name.pascalCase}}Props } from './types/{{name.kebabCase}}.interface'
|
import type { {{name.pascalCase}}Props } from './types/{{name.kebabCase}}.type'
|
||||||
import styles from './styles/{{name.kebabCase}}.module.css'
|
import styles from './styles/{{name.kebabCase}}.module.css'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {{name.pascalCase}}.
|
* {{name.pascalCase}}.
|
||||||
*/
|
*/
|
||||||
export const {{name.pascalCase}}: FC<{{name.pascalCase}}Props> = (props) => {
|
export const {{name.pascalCase}} = (props: {{name.pascalCase}}Props) => {
|
||||||
const { children, className, ...htmlAttr } = props
|
const { children, className, ...htmlAttr } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user