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.

Quellinformationen

Repository
webiny/data-mock
Letzte Quellaktivität
4. September 2026 um 10:12
Erkannte Sprache von SKILL.md
Englisch
Sterne
1
Forks
0

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
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(); }, }); ```
Auf GitHub ansehen