Appearance
Быстрый старт
Соберём минимальный сценарий блога: получим неизвестные данные со списком постов, проверим их Valibot, преобразуем DTO, сохраним результат в MobX store и покажем в React.
1. Создайте минимальный slice
txt
data/posts/
posts.schema.ts
posts.dto.ts
posts.mapper.ts
posts.store.ts
posts.types.ts
index.tsНе создавайте дополнительные папки заранее. Плоская структура легче читается, пока файлов немного.
2. Опишите внешний контракт
ts
// data/posts/posts.schema.ts
import { array, object, string } from "valibot"
export const PostSchema = object({
id: string(),
title: string(),
published_at: string(),
})
export const PostsResponseSchema = object({
posts: array(PostSchema),
})Schema отвечает на вопрос: какие данные приложение готово принять.
3. Получите DTO из schema
ts
// data/posts/posts.dto.ts
import type { InferOutput } from "valibot"
import { PostSchema, PostsResponseSchema } from "./posts.schema"
export type PostDto = InferOutput<typeof PostSchema>
export type PostsResponseDto = InferOutput<typeof PostsResponseSchema>Не описывайте тот же контракт вручную второй раз.
4. Проверьте unknown
ts
// data/posts/posts.mapper.ts
import { parse } from "valibot"
import type { PostsResponseDto } from "./posts.dto"
import { PostsResponseSchema } from "./posts.schema"
export const parsePostsResponse = (input: unknown): PostsResponseDto =>
parse(PostsResponseSchema, input)До parse приложение не доверяет значению. После успешного parse форма данных известна.
5. Опишите внутренний тип
ts
// data/posts/posts.types.ts
export interface IPost {
id: string
title: string
publishedAt: Date
}Внутренний тип не обязан повторять формат внешних данных.
6. Добавьте mapper
ts
// data/posts/posts.mapper.ts
import type { PostDto } from "./posts.dto"
import type { IPost } from "./posts.types"
export const mapPostDtoToPost = (dto: PostDto): IPost => ({
id: dto.id,
title: dto.title,
publishedAt: new Date(dto.published_at),
})Теперь naming внешнего контракта не протекает в UI.
7. Реализуйте MobX store
Store получает уже проверенные данные. Способ их доставки не входит в scope DSA.
ts
// data/posts/posts.store.ts
import { makeAutoObservable } from "mobx"
import type { IPost } from "./posts.types"
export interface IPostsStore {
readonly posts: IPost[]
readonly hasPosts: boolean
setPosts(posts: IPost[]): void
reset(): void
}
export class PostsStore implements IPostsStore {
posts: IPost[] = []
constructor() {
makeAutoObservable(this)
}
get hasPosts(): boolean {
return this.posts.length > 0
}
setPosts(posts: IPost[]): void {
this.posts = posts
}
reset(): void {
this.posts = []
}
}
export const postsStore = new PostsStore()8. Откройте минимальный public API
ts
// data/posts/index.ts
import { PostsStore, postsStore } from "./posts.store"
import type { IPost, IPostsStore } from "./posts.types"
export {
PostsStore,
postsStore,
type IPost,
type IPostsStore,
}DTO и schema остаются внутренними, если внешнему коду они не нужны.
9. Подключите store к React
tsx
// components/posts/widgets/PostsWidget.tsx
import { observer } from "mobx-react-lite"
import { postsStore } from "@/data/posts"
import { PostsList } from "../../ui/PostsList"
export const PostsWidget = observer(() => (
<PostsList posts={postsStore.posts} />
))
PostsWidget.displayName = "PostsWidget"Dumb-компонент знает только props:
tsx
// components/posts/ui/PostsList.tsx
import type { IPost } from "@/data/posts"
interface IPostsListProps {
posts: IPost[]
}
export const PostsList = ({ posts }: IPostsListProps) => (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
PostsList.displayName = "PostsList"Что должно остаться в голове
Не запоминайте структуру файлов как ритуал. Запомните цепочку:
txt
unknown → Valibot → DTO → mapper → тип приложения → MobX store → ReactА React Router и React Hook Form подключаются там, где появляются маршруты и формы.
Что дальше
Core-пример выше намеренно не показывает HTTP-клиент и стили. Если хотите увидеть тот же поток на полном стеке, откройте сквозной сценарий полного стека, а затем Example project.