.agents/skills/how-to-write-component/SKILL.md
Use this as the component decision guide for Dify web. Existing code is reference material, not automatic precedent; if touched code violates these rules, adapt it and fix equivalent patterns in the same feature branch.
| Question | Default | Promote or extract only when |
|---|---|---|
| Where should code live? | Keep it local to the feature workflow, route, or owner. | Multiple verticals need the same stable primitive. |
| How should route/tab folders be named? | Match the current route segment, tab name, or user-visible surface. | Keep a historical or broader parent only when it still owns multiple surfaces. |
| Who owns state, data, and handlers? | The lowest component that uses them. | A parent coordinates shared loading, errors, empty UI, selection, submission, navigation, or one consistent snapshot. |
| Should this become Jotai state? | Keep synchronous UI/form state in component or DOM state. | Siblings need one source of truth, the value drives atoms, or scoped workflow state must survive hidden/unmounted steps. |
| Should URL state enter Jotai? | Let Next.js route params and nuqs own URL state and updates. | Query atoms or shared derived atoms need a read-only bridge hydrated at the route/surface boundary. |
| Should this query/mutation become an atom? | Use TanStack Query hooks at the lowest owner. | It reads atom state, feeds derived atoms, or participates in shared Jotai workflow orchestration. |
| Should this be a helper/wrapper? | Prefer direct readable code at the use site. | The name captures a stable domain rule or the wrapper owns real behavior, validation, state, error handling, or semantics. |
| Where should a hotkey live? | Keep a single-owner hotkey constant in its component. | Multiple production files share one command, or the feature owns a real command registry with shared metadata and behavior. |
| Is an Effect needed? | No. Derive during render or handle the user action in the event handler. | It synchronizes with an external system such as browser APIs, subscriptions, timers, analytics, or imperative DOM/non-React widgets. |
packages/dify-ui/README.md and packages/dify-ui/AGENTS.md. Prefer design-system tokens, utilities, and radius mappings over generic Tailwind choices.@langgenius/dify-ui/* controls when available, because components such as Button and form/control primitives carry the standard Dify UI focus-visible styling. Do not assume every Dify UI export provides visual focus styles: headless anatomy parts and direct Base UI re-exports such as dialog/popover/tooltip/drawer triggers usually only provide behavior and semantics. When using native button / a, custom trigger render props, clickable rows, icon buttons, menu-like items, or direct trigger parts, verify the rendered focusable element has a visible focus state. If it does not, add the standard Dify UI focus style: outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid. Do not hide outlines without an equivalent visible focus-visible indicator. Component-specific focus styles should follow an existing styled primitive pattern or a concrete design constraint, not a new ad hoc style.README.md as a boundary note. Start with the module name, a brief one-sentence description, then split dependencies into Internal Modules and External Modules sections; keep both sections and write None. when one category is empty. Internal Modules lists modules inside the same overall feature using paths from that feature root, such as shared/domain/runtime-status; External Modules lists project modules outside the feature using paths from the web root without a web/ prefix, such as app/components/base/skeleton. Omit npm packages, workspace package dependencies, and whitelisted plumbing modules. Do not copy caller-relative import paths into the README.@/service/client, @/next/*.ui/ owners that match real visual regions.components/ file.@langgenius/dify-ui/form and @langgenius/dify-ui/field controls for edit/create forms whose fields are read only at submit time. Initialize query-backed defaults with defaultValue and keyed remounts.useParams, route args, and nuqs query state as framework-owned state. When atom logic needs those values, hydrate primitive atoms at the route or surface boundary, such as with useHydrateAtoms(..., { dangerouslyForceHydrate: true }); keep URL updates in the route/query-state APIs instead of write atoms.atomWithQuery or atomWithMutation; write atoms should update only the inputs that drive those atoms. This applies to pure frontend async work as well as network requests, so do not hand-roll loading/error/in-flight state with useState or useRef for atom-orchestrated async behavior. For component-owned remote work, use useQuery or useMutation directly.jotai-tanstack-query query atoms do not support TanStack Query tracked properties. A component that reads useAtomValue(queryAtom) subscribes to the whole query result, even if it only accesses data, isLoading, or isError. Export field-specific derived atoms and have components read the exact fields they render; use selectAtom(queryAtom, result => result.field) for query-result fields so unchanged selections do not notify subscribers. Keep direct useAtomValue(queryAtom) only when the component or hook genuinely needs the full observer result.ScopeProvider, prefer atomWithLazy<T>(() => { throw new Error(...) }) when consumers should see a non-null type.useAtomValue or useSetAtom.open state usually stays local, but a scoped atom is acceptable when a composed menu plus secondary surface would otherwise pass confusing open/onClose props through unrelated layers. Scope that primitive with the surface instance so reset behavior stays local.FC or React.FC.function for top-level components and module helpers. Use arrow functions for local callbacks, handlers, and lambda-style APIs.index.tsx is acceptable for a route/tab entry component; import header controls, switches, sections, and row owners from their concrete owner files.Props type only when reused, exported, complex, or clearer.string; keep wrappers and option value carriers typed from their feature option collection.common.tsx buckets for shared UI. Use a feature-local components/ folder with concrete filenames that describe the shared role.@tanstack/react-hotkeys for application commands. Keep menu navigation, dialog Escape handling owned by a primitive, editor commands, and other widget-scoped ARIA interactions in their local component or primitive.useHotkey or useHotkeys for registered commands. For a command intentionally owned by an existing onKeyDown, use matchesKeyboardEvent instead of hand-written metaKey / ctrlKey parsing or a second global listener.satisfies Hotkey and an object-form command with satisfies RawHotkey. Reserve RegisterableHotkey for API boundaries that intentionally accept either form. A one-time inline literal passed directly to TanStack is already type-checked; extract it when registration, display, metadata, or another production consumer needs the same source.IndividualKey with useKeyHold for held-key interactions, and use an explicitly named displayKey for local widget accelerators that are not registered Hotkey values.['Mod', ...] display array.hotkeys.ts only when multiple production files consume the same command. Keep a dedicated definitions/registry module when a feature owns a real command system with IDs, metadata, alternate bindings, and centralized registration. Tests do not count as another production owner, and file-name uniformity alone is not a reason to extract.enabled for business or surface lifecycle, ignoreInputs for whether input-like elements may trigger the command, and target when the command belongs to a concrete DOM subtree. Global application commands may use the document target; inline editors and composed overlays should prefer the actual editor or Base UI Popup ref when that owner is exposed.preventDefault and stopPropagation according to the existing product behavior and browser interaction. Do not silently accept TanStack defaults when migrating from another listener if that changes typing, submission, or propagation semantics.packages/contracts/generated/enterprise/*.'' in query, derived model, or payload-building code. Keep null or undefined until the final boundary requiring a string.value || undefined for mutation fields where '' means "clear this value". Trim or normalize at the form boundary, then preserve intentional empty strings.flatMap, a local loop, or an early return. Avoid truthiness guards, filter(Boolean), filter(item => item.id), and ! after filters.undefined placeholders followed by narrowing filters.web/contract/* as the API shape source of truth and follow the { params, query?, body? } input shape.useQuery(consoleQuery.xxx.queryOptions(...)) or useQuery(marketplaceQuery.xxx.queryOptions(...)).atomWithQuery; do not unwrap the atom in a component just to call useQuery.useMutation(consoleQuery.xxx.mutationOptions(...)) or useMutation(marketplaceQuery.xxx.mutationOptions(...)) when pending/error state is not consumed by feature atoms.atomWithQuery, atomWithInfiniteQuery, and atomWithMutation, return generated queryOptions(), infiniteOptions(), or mutationOptions() directly. Pass enabled, retry, placeholderData, select, and pagination options into the generated call instead of spreading options into a hand-built object.input: condition ? validInput : skipToken and enabled: Boolean(condition). Never place skipToken inside a nested placeholder payload or coerce required IDs to ''.prefetchQuery and useQuery/atomWithQuery share the exact options.queryOptions(...) or mutationOptions(...).queryOptions(...) into a helper solely to share input construction; extract only when prefetch/render must share exact options or the helper owns real domain behavior.web/service/use-* wrappers that only rename generated options. Keep feature hooks for real orchestration, workflow state, or shared domain behavior.createTanstackQueryUtils(...experimental_defaults...). Component or atom callbacks may handle local toasts, closing dialogs, and navigation, but should not replace shared invalidation or patch shared server state locally.queryClient.prefetchQuery(queryOptions) when onOpenChange is available. Do not mount hidden subscribers just to warm cache.useInvalid or useReset.mutate(...); use mutateAsync(...) only when Promise semantics are required, and wrap awaited calls in try/catch.open, mount it unconditionally unless unmounting is required for performance or reset semantics. Use keyed scope or local state reset instead of {open && <Surface />} wrappers.open wiring and put query/mutation hooks inside the content component when work should mount only after the overlay opens.onOpenChange for side effects and CSS/data selectors for open-state styling.Link for normal navigation. Use router APIs only for command-flow side effects such as mutation success, guarded redirects, or form submission.memo, move changing state down to the smallest component that uses it. If state must wrap stable content, lift the stable content up and pass it as children.memo, useMemo, and useCallback unless there is a clear performance reason.