Skip to main content

ui-developer

Use when building UI features in src/ui/. Layered architecture: Gateway (HTTP) → Repository (state) → UseCase (logic) → Presenter (MobX vm) → React (display). React is dumb. All state and logic in TypeScript classes via DI. Invoke BEFORE writing any UI code. References project-architecture for DI patterns.

소스 정보

저장소
webiny/data-mock
최근 소스 활동
2026년 9월 4일 10:12
감지된 SKILL.md 언어
영어
스타
1
포크
0

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
ui-developer
description
Use when building UI features in src/ui/. Layered architecture: Gateway (HTTP) → Repository (state) → UseCase (logic) → Presenter (MobX vm) → React (display). React is dumb. All state and logic in TypeScript classes via DI. Invoke BEFORE writing any UI code. References project-architecture for DI patterns.
# UI Developer Guide UI lives in `src/ui/`. Strict layered architecture — React is a dumb display layer. All state, logic, and data transformation live in plain TypeScript classes wired through `@webiny/di`. Shared types, errors, and domain models go in `src/shared/`. ## Layer Rules ``` React (display only) ← reads vm, calls actions ↑ useFeature() hook Presenter (MobX observable) ← owns vm() getter, single source of UI truth ↑ DI constructor injection UseCase (orchestration) ← all reads and writes ↑ Repository (plain class) ← holds domain state, NOT MobX observable ↑ Gateway (HTTP calls) ← translates API responses to domain types ``` | Layer | MobX | Holds state | React access | DI Scope | |-------|------|-------------|--------------|----------| | Gateway | No | No | Never | Singleton | | Repository | No | Yes | Never | Singleton | | UseCase | No | No | Never | Transient | | Presenter | `makeAutoObservable` | Yes (derived) | Via `vm()` only | Transient | ### Result Pattern (no throws) All layers use `Result<T, E>` from `@webiny/stdlib` — same as CLI. **Nothing throws.** Gateways catch HTTP errors and return `Result.fail()`. Use cases return `Result`. Presenters check `result.isOk()` / `result.isFail()` and update state accordingly. Presenters that trigger async work must use `try/finally` to reset loading state. ## Directory Structure ``` src/ui/ ├── features/ # Headless layer (Gateway + Repository) │ └── {featureName}/ │ ├── abstractions/ │ │ ├── {Action}{Entity}Gateway.ts │ │ ├── {Entity}Repository.ts │ │ └── index.ts │ ├── {Action}{Entity}Gateway.ts │ ├── {Entity}Repository.ts │ └── feature.ts ├── presentation/ # Presentation layer (Presenter + React) │ └── {domain}/ │ └── {Page}/ │ ├── abstractions/ │ │ ├── {Page}Presenter.ts │ │ └── index.ts │ ├── useCases/ │ │ └── {Action}{Entity}/ │ │ ├── abstractions/ │ │ └── {Action}{Entity}UseCase.ts │ ├── {Page}Presenter.ts │ ├── {Page}Provider.tsx │ ├── feature.ts │ └── components/ │ ├── {Page}Page.tsx │ └── {Feature}List.tsx └── feature.ts # Root UiFeature ``` ## Gateway — HTTP Only Calls the backend API. Returns `Result<T, E>`. ```ts // features/projects/abstractions/ProjectsGateway.ts import { createAbstraction } from "@webiny/stdlib"; import type { Result } from "@webiny/stdlib"; export interface IProjectDto { id: string; name: string; apiUrl: string; } export interface IProjectsGateway { getAll(): Promise<Result<IProjectDto[], ProjectsGateway.Error>>; create(input: ProjectsGateway.CreateInput): Promise<Result<IProjectDto, ProjectsGateway.Error>>; } export const ProjectsGateway = createAbstraction<IProjectsGateway>("Ui/ProjectsGateway"); export namespace ProjectsGateway { export type Interface = IProjectsGateway; export type ProjectDto = IProjectDto; export type CreateInput = { name: string; apiUrl: string; token: string }; export type Error = import("../errors.ts").HttpError; } ``` ## Repository — Plain Class, NO MobX Holds domain state. Exposes via getter methods. Singleton scope. ```ts class ProjectsRepositoryImpl implements Abstraction.Interface { private projects: Abstraction.Project[] = []; public constructor( private readonly gateway: ProjectsGateway.Interface, ) {} public getProjects(): Abstraction.Project[] { return this.projects; } public async loadProjects(): Promise<Result<void, Abstraction.Error>> { const result = await this.gateway.getAll(); if (result.isFail()) { return Result.fail(result.error); } this.projects = result.value; return Result.ok(undefined); } } ``` ## UseCase — Orchestration Coordinates repositories. Transient scope. Presenters call use cases, never repositories or gateways directly. ## Presenter — The Only MobX Layer Owns `vm()` getter. Maps domain state to view-ready data. Transient scope. ```ts import { makeAutoObservable, runInAction, computed } from "mobx"; class ProjectListPresenterImpl implements Abstraction.Interface { private loading = false; public constructor( private readonly loadProjectsUseCase: LoadProjectsUseCase.Interface, private readonly projectsRepository: ProjectsRepository.Interface, ) { makeAutoObservable(this, { vm: computed }); } public get vm(): Abstraction.ViewModel { const projects = this.projectsRepository.getProjects(); return { loading: this.loading, projects: projects.map(p => ({ id: p.id, name: p.name, apiUrl: p.apiUrl })), isEmpty: projects.length === 0, }; } public load = async (): Promise<void> => { this.loading = true; try { await this.loadProjectsUseCase.execute(); } finally { runInAction(() => { this.loading = false; }); } }; } ``` Rules: - `makeAutoObservable(this, { vm: computed })` in constructor - `vm` is a computed getter returning a plain object with view-ready data - All methods (except `vm`) are arrow function class properties (lexical `this`) - Async mutations wrapped in `runInAction()` - Presenter provides correct values — views never do `value ?? ""` or null coercion ## React — Display Only Zero business logic. Zero data transformation. Reads `vm`, calls presenter actions. ```tsx import { useEffect } from "react"; import { observer } from "mobx-react-lite"; import type { ProjectListPresenter } from "../abstractions/ProjectListPresenter.ts"; interface ProjectListPageProps { presenter: ProjectListPresenter.Interface; } export const ProjectListPage = observer(function ProjectListPage({ presenter }: ProjectListPageProps) { const { vm } = presenter; useEffect(() => { presenter.load(); }, [presenter]); if (vm.loading) return <div>Loading...</div>; if (vm.isEmpty) return <div>No projects configured.</div>; return ( <ul> {vm.projects.map(p => <li key={p.id}>{p.name} — {p.apiUrl}</li>)} </ul> ); }); ``` ## Provider Connects DI to React via render props: ```tsx import type React from "react"; import { useFeature } from "../../shared/di/useFeature.ts"; import { ProjectListFeature } from "./feature.ts"; import type { ProjectListPresenter } from "./abstractions/ProjectListPresenter.ts"; interface Props { children: (params: { presenter: ProjectListPresenter.Interface }) => React.ReactNode; } export function ProjectListProvider({ children }: Props) { const { presenter } = useFeature(ProjectListFeature); return children({ presenter }); } ``` ## Anti-Patterns | Wrong | Right | |-------|-------| | Component reads from repository | Component reads from `presenter.vm` | | `makeAutoObservable` in repository | Only in presenter | | Business logic in component | Move to presenter or use case | | Presenter calls gateway/repository | All reads/writes through use cases | | `useEffect` chain for business logic | Logic in presenter; useEffect only for load-on-mount | | Class method `async load() {}` | Arrow property `load = async () => {}` | | View does `value ?? ""` | Presenter provides view-ready values | | Inline complex JSX | Extract into separate component files | | Barrel exports implementation/feature | Barrel exports abstractions only | ## Feature Registration UI features use `createFeature` from `@webiny/stdlib` with `dependencies` support for auto-registration. ```ts import { createFeature } from "@webiny/stdlib"; export const ProjectsFeature = createFeature({ name: "Ui/ProjectsFeature", register(container) { container.register(ProjectsGateway).inSingletonScope(); container.register(ProjectsRepository).inSingletonScope(); }, }); ```
GitHub에서 보기