SuperAPI AI Directory

react-ddt

Frontend & websites

View repository

Updated 10-15 23:43

About this skill

react-ddt

react-ddt is React dont-do-that packaging.

This skill specifically constrains a class of issues that most easily recur in React components and hooks: coupling pure functions, static constants, and type definitions unrelated to React state inside .tsx or the hook's main file, causing component files to become increasingly heavy and making it hard during review to quickly determine which parts are rendering logic and which are merely ordinary computation logic.

It and ddt are not mutually substitutable.ddt is responsible for opposing unbeneficial over-abstraction,react-ddt is responsible for placing non-React responsibilities in React files at project-conventional locations. When executing, both sets of rules must be considered at the same time: pure functions that should be extracted must be extracted, and a pile of thin wrappers must not be added merely for form's sake.

Specification reading requirements

If this skill is to review or modify project code, it must first read the repository's CLAUDE.md and any existing AGENTS.md, then read relevant module documentation and notes alongside source directories as required by the task.

If the current change belongs to a React page, component, hook, or frontend state chain, also check whether it hits the project's page splitting, type placement, documentation maintenance, i18n, and verification requirements.

When to trigger

When the user expresses the following meanings, prioritize using this skill:

  • Requests review of React hooks, React components,.tsx file structure
  • Points out that pure functions should not be placed in component or hook main files
  • Requests that constants be placed in enum.ts
  • Requests that types be moved out of component or hook main files and placed according to project conventions
  • Requests that types be placed in types.ts, or questions that types should not be placed in types.ts
  • Requests that the TS / TSX files just modified be re-reviewed according to directory responsibilities
  • UI, state logic, pure computation, static configuration, and type definitions appear simultaneously in a component file
  • Judgment, construction, parsing, and comparison functions unrelated to hook state appear in a hook main file

Core rules

React component files should retain only rendering, props destructuring, React hook calls, event handlers, logic strongly bound to the current component's state / ref / effect, and necessary JSX branches.

React hook main files should retain only the hook's own state, ref, memo, effect, and callback orchestration, as well as closure logic that must read state within the hook.

If a component or hook contains pure functions unrelated to React state, ref, or effect lifecycle, they must be extracted to the util.tsin the current directory. If there is no util.ts , create one. This rule applies to pure logic such as parsing IDs, constructing summaries, filtering lists, comparing objects, formatting display text, converting DTOs, calculating checked states, and generating className configurations.

If static constants exist, they must be extracted to the enum.tsin the current directory. If there is no enum.ts , create one. This rule applies to values that do not change with component runtime state, such as UI dimensions, spacing, thresholds, ID prefixes, regular expressions, fixed keys, status codes, static Sets, static Maps, and fixed tool names.

If type definitions exist, first determine whether they should be moved out of the React main file, then decide the destination according to project type conventions and existing directory patterns; do not uniformly create new ones or move them to the current directory types.ts. This rule applies to component props, hook parameters, hook return values, local business types, selector state, internal mapping structures, etc. Only when the project has no explicit type placement requirement and the same directory already has or is suitable for hosting local types, use the same directory types.ts as the default location.

enum.ts In this project can host semantic const、Set, regular expressions, and true enum. Do not force all values to TypeScript enumjust because the file is named enum. Only semantically clear numeric constants such as business status codes, task codes, and type codes should preferentially use true enum or the project's existing enum pattern.

Pre-check: first determine whether it should be extracted

react-ddt The order is to first determine whether splitting brings benefits, then determine whether to place it in util.ts、enum.ts or a type file. Do not mechanically extract something just because a piece of logic is a pure function, a static constant, or a type definition.

看到 React 文件里的函数 / 常量 / 类型 / 兜底分支
        │
        ▼
它是否属于 React 主流程?
        ├─ 是:保留在组件或 hook 主文件
        │
        └─ 否
             │
             ▼
它是否有稳定复用、复杂逻辑、外部协议或测试边界?
             ├─ 否:内联、合并类型、保留局部字面量或删除重复兜底
             │
             └─ 是
                  │
                  ▼
       按职责落点:纯逻辑 -> util.ts;固定配置 -> enum.ts;类型 -> 项目类型规范

When a small function has only one caller, is 2 to 5 lines long, has a name that is merely an implementation description such as parse / build / normalize / to , requires the caller to still click in to see details, and has no independent test value, inline it by default. Only keep the function when it is stably reused, can significantly reduce the complexity of the main flow, or expresses an external protocol or complex business boundary.

When a thin type serves only one parent type, is merely a rename / field extraction / one-level alias, and splitting increases indirection, merge it by default. Only extract it when the type itself is an API response body, event payload, message body, stable nested structure, or is independently consumed by multiple modules.

Keep fallback logic only when the input comes from unstable boundaries such as users, the network, browser APIs, external SDKs, or historical dirty data. When the state is already guaranteed by the React lifecycle, event order, state machine, or type definitions, do not add redundant "more robust" fallbacks.

Determine whether a pure function should be extracted

Meeting these conditions only means it is a candidate for extraction; whether to actually extract it still requires the pre-check first:

  • Does not call React hooks
  • Does not read state, ref, context, or dispatch inside a component or hook
  • Does not call setState
  • Does not depend on DOM instances, event objects, or the component lifecycle
  • Inputs are fully provided by parameters
  • Output is determined only by input, or only performs deterministic parsing, construction, filtering, comparison, or formatting

If a function is written inside a component but only because it needs to read props or state, and it can be fully passed in through parameters, it should be extracted, leaving the component responsible only for passing the parameters in.

This judgment has another premise: extraction should reduce reading cost. The extracted method must contain complex logic worth naming, reuse value, or a clear business boundary. A thin helper that is called only once and whose body is merely simple field mapping,typeof checks, a one-off type guard, wrapping an existing utility function, type conversion, or null fallback should not add a new function just to satisfy "pure functions go in util"; write the expression directly at the call site, and use a local variable to hold it if necessary.

If a function must directly operate on setState, read a ref, handle an event object, close a modal, or trigger a side effect of the current component, it usually stays in the component or hook main file. It belongs to React interaction logic; do not extract it into util just for form's sake.

Determine whether a constant should be extracted

When these conditions are met, extract by default to the same directory enum.ts:

  • UI width/height, spacing, maximum count, delay time, and threshold defined at the top level of the file
  • String ID prefix, storage key, metadata key, tool name, agent id
  • Regular expressions, static allowlists, static state sets
  • Fixed label, mode, type, status reused across multiple branches

A Tailwind className that appears only once in JSX and reads as the style itself does not need to be extracted just for constant-ization. Extracting constants is meant to eliminate scattered configuration, not to turn JSX styles into another layer of indirection.

Deciding where types should go

First read the project conventions, existing files in the same directory, and upper-level module agreements, then decide where the types belong. The goal of type extraction is to make the main React file more focused, not to forcibly create types.ts。

If the project specifies that component props, hook parameters, or hook return values go in the component directory types.ts, reuse the existing types.ts; if there is none, then decide whether a new one is needed. If the project specifies placing them in module-level types.ts、src/types/**, feature-level type files, or interface-layer type files, you must follow the project conventions.

If project conventions allow very small local props types to stay in the component file, and the type has no reuse value and will not weigh down the main flow, it can remain in the component file. Do not mechanically move all local types away just for the sake of "separating types from logic."

API request parameters, response DTOs, and database structures should not go in the React component directory's types.ts; continue to follow the project's API type conventions.

Types that are stably reused across modules should not go in a local types.ts; they should go into the project's existing public type location. Do not sneak global types into a local directory just for the convenience of the current component.

Execution steps

When reviewing the current changes, first list all the React hooks, React components, and .tsx files that were modified, then check them file by file.

For each file, first find the top-level const、function、type、interface. First determine whether they belong to the main React flow, then determine whether splitting brings real benefit. After confirming it is worth splitting, move pure functions to util.ts, move static constants to enum.ts, and move types according to the project's type conventions; only when there are no clear conventions should you consider a same-directory types.ts。

After moving, update the imports.import and import type must not be split from the same path into duplicate imports; they must be merged. Type imports use import type。

After moving, check for circular references.util.ts can import type files and enum.ts, components can import util.ts、enum.ts and project-specified type files. Do not let type files import components in reverse, and do not let enum.ts import React components.

After moving, check the comments. All retained methods must have a brief method-level JSDoc; if the JSDoc can only restate the function name or surface behavior, first determine whether the method is over-encapsulated and inline it. Keep inline comments for key business decisions in complex pure logic, and do not delete valuable Chinese explanations.

Finally, run the minimum necessary verification. By default, at least run ESLint on the affected files, then supplement with npm run check-types、npm run verify:i18n or manual page verification according to the task's impact.

and the boundary with ddt

react-ddt should move pure functions, constants, and types that should be extracted out of the main React file, but it does not encourage adding intermediate layers with no benefit.

If a piece of logic is originally just two or three lines of state updates in a component event handler, do not extract it into a util.

If a pure function is called only once, and the function body is just simple field mapping,typeof For one-off type guards, single-line wrappers around existing utility functions, type conversions, or null fallbacks, prefer inlining at the call site; do not extract them into util functions. Extraction is for reuse or isolating complex logic, not for giving a simple expression a longer name.

If a type is just a one-line field alias for some props, do not create an extra thin type just to split it out; write it directly into the props type.

Only give a constant a stronger semantic name if it is used solely to explain complex business state or is reused across multiple files. Do not move every literal that appears only once into enum.ts。

When unsure, use this sentence as the criterion: in React files, leave only the React-related reading path; ordinary computation logic goes to util, fixed configuration goes to enum, and types are placed according to project conventions; but thin wrapper logic used only once and very small local types should preferentially stay in the most direct location.

Anti-pattern checklist

The following by default need to be fixed:

  • ExamplePanel.tsx Defining both the panel size constant and buildSelectedSummary()
  • ExampleList.tsx Defining at the top level buildInitialRange(); it only constructs a default range from the input parameters, does not read component state, ref, or lifecycle, and should be placed in the same-level util.ts
  • useExampleSelection.ts Defining in isSameSelection()
  • util.ts Adding in normalizeSignInTemplateCourseId(); the function body is just normalizePositiveFiniteNumber(courseId) ?? null, and it should be inlined at the call site
  • util.ts Adding in buildUserInfo(), the function body just maps several fields by typeof into an object, and it should be coupled into the function that generates the context
  • util.ts Adding in isAppJwtClaims(), the function body is just a one-off object / array check, and it should be inlined into the parsing function that uses it
  • Stuffing very long props, mapping structures, or reusable types into a component file, while project conventions require such types to be placed in a separate type file
  • Defining a complex parameter interface in the hook main file, while the project already has a unified location for hook types
  • Scattering in TSX example-、item- such ID prefix strings
  • Scattering in TSX the width, height, gap, and padding numbers used for overlay positioning
  • Repeatedly writing in util fixed strings or numbers with the same semantics as those in enum
  • Adding for JSX strings, style values, or simple boolean checks that appear only once and are self-explanatory in context enum.ts or util.ts

Example

Incorrect way:

const PANEL_WIDTH = 280;

interface UseExampleSelectionParams {
  selectedIds: string[];
  allIds: string[];
}

function isAllSelected(selectedIds: string[], allIds: string[]) {
  return selectedIds.length === allIds.length;
}

export function useExampleSelection(params: UseExampleSelectionParams) {
  const allSelected = useMemo(() => isAllSelected(params.selectedIds, params.allIds), [params.selectedIds, params.allIds]);

  return { allSelected };
}

More appropriate placement:

This check has only one caller, and it is just a length comparison; after inlining, the reading path is shorter. The example below assumes the project convention places the current hook's local types in the same directory types.ts. Real projects should replace the type location according to their type conventions.

// enum.ts
export const optionPanelWidth = 280;
// types.ts
export interface UseExampleSelectionParams {
  selectedIds: string[];
  allIds: string[];
}
// useExampleSelection.ts
import type { UseExampleSelectionParams } from "./types";

/** 管理示例选择状态。 */
export function useExampleSelection(params: UseExampleSelectionParams) {
  const allSelected = useMemo(() => params.selectedIds.length === params.allIds.length, [params.selectedIds, params.allIds]);

  return { allSelected };
}

Another incorrect way:

// ExampleList.tsx
function buildInitialRange(value?: string): { start: string; end: string } | null {
  const trimmedValue = value?.trim();

  if (!trimmedValue) return null;

  return {
    start: `${trimmedValue}-start`,
    end: `${trimmedValue}-end`,
  };
}

export function ExampleList(props: ExampleListProps) {
  const [range] = useState(() => buildInitialRange(props.initialValue));

  return <RangeView value={range} />;
}

More appropriate placement:

// util.ts
/** 根据外部传入值构造初始范围;空值不生成范围。 */
export function buildInitialRange(value?: string): { start: string; end: string } | null {
  const trimmedValue = value?.trim();

  if (!trimmedValue) return null;

  return {
    start: `${trimmedValue}-start`,
    end: `${trimmedValue}-end`,
  };
}
// ExampleList.tsx
import { buildInitialRange } from "./util";

export function ExampleList(props: ExampleListProps) {
  const [range] = useState(() => buildInitialRange(props.initialValue));

  return <RangeView value={range} />;
}

Incorrect way of over-extraction:

// util.ts
export function normalizeSignInTemplateCourseId(courseId: string | number) {
  return normalizePositiveFiniteNumber(courseId) ?? null;
}
// SignInTemplate.tsx
const courseId = normalizeSignInTemplateCourseId(params.courseId);

More appropriate way:

// SignInTemplate.tsx
const courseId = normalizePositiveFiniteNumber(params.courseId) ?? null;

This helper is called only once, and the function body just adds to the existing normalizePositiveFiniteNumber the ?? null. Extracting it out has no reuse benefit, and instead makes readers jump through one more file.

Output requirements

If the user asks for a direct fix, complete the file moves and import updates before replying with the result.

If the user asks for a review, point out by file which pure functions, constants, and types are misplaced, and give minimal modification suggestions. Do not make general comments about component complexity.

The reply should state the verification results. If browser manual verification was not run, clearly say it was not run. Do not describe ESLint warnings as errors.