pedantic-react

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Pedantic React

严谨型React规范

Opinionated house style for React + TypeScript frontends.
一套针对React + TypeScript前端项目的约定式内部编码风格。

TypeScript

TypeScript

  • Prefer explicit types at all boundaries (props, return types, API responses).
  • Use
    import type
    for type-only imports.
  • Type component props explicitly with
    interface
    . Use
    PropsWithChildren
    when children are accepted.
  • Wrap component prop inputs in
    Readonly<...>
    .
  • Prefer
    unknown
    over
    any
    ; narrow with type guards.
  • Avoid non-null assertions (
    !
    ); prefer optional chaining and explicit guards.
  • 在所有边界(Props、返回类型、API响应)都优先使用显式类型。
  • 仅类型导入时使用
    import type
  • 使用
    interface
    显式定义组件Props。当组件接收子元素时,使用
    PropsWithChildren
  • 将组件Props输入包裹在
    Readonly<...>
    中。
  • 优先使用
    unknown
    而非
    any
    ;通过类型守卫进行类型收窄。
  • 避免使用非空断言符(
    !
    );优先使用可选链和显式守卫。

Imports

导入规则

  • Prefer a
    @/
    alias for imports from
    src/
    .
  • Group imports: external libraries → internal modules → types. Separate groups with a blank line.
  • All imports at the top of the file, never inline or inside try blocks.
  • Prefer named exports for components and hooks. Avoid default exports; reserve them only for entrypoints, reducers, or interop files that require them.
  • 优先使用
    @/
    别名导入
    src/
    目录下的文件。
  • 对导入进行分组:外部库 → 内部模块 → 类型。组与组之间用空行分隔。
  • 所有导入都放在文件顶部,绝不要内联或放在try块内部。
  • 组件和Hooks优先使用命名导出。避免默认导出;仅在入口文件、reducer或需要互操作的文件中使用默认导出。

JSX

JSX

  • Prefer rendering variables over inline conditions. Derive named booleans before the return:
    const showBanner = isLoaded && hasError && !isDismissed
    then
    showBanner ? <Banner /> : null
    .
  • For a collection of flags, prefer
    [a, b, c].every(Boolean)
    or
    [a, b, c].some(Boolean)
    over chaining
    &&
    or
    ||
    .
  • Never use nested ternaries. Use
    if
    statements or rendering variables for anything beyond a single obvious ternary.
  • Prefer
    condition ? <Thing /> : null
    over
    condition && <Thing />
    .
  • Remove all instances of
    {' '}
    or
    {" "}
    , use layout gap, margin, or padding instead.
  • Prefer destructuring to prop drilling. Stop drilling beyond two levels; lift state or use shared state instead.
  • Prefer composition and small focused helpers over large boolean-heavy JSX branches.
  • Keep JSX return values small; extract sub-trees into named components when a render block grows unwieldy.
  • Prefer named handler functions over inlined arrow functions. Extract to a handler when the body reaches 3+ lines or is async:
    const handleSubmit = async () => { ... }
    rather than
    onClick={async () => { ... }}
    .
  • For fire-and-forget async handlers, use
    void
    to avoid blocking:
    void handleSubmit()
    .
  • 优先使用渲染变量而非内联条件。在return语句前推导命名布尔值:
    const showBanner = isLoaded && hasError && !isDismissed
    ,然后使用
    showBanner ? <Banner /> : null
  • 处理多个标志位时,优先使用
    [a, b, c].every(Boolean)
    [a, b, c].some(Boolean)
    ,而非链式使用
    &&
    ||
  • 绝不使用嵌套三元表达式。对于超出单个简单三元的场景,使用
    if
    语句或渲染变量。
  • 优先使用
    condition ? <Thing /> : null
    而非
    condition && <Thing />
  • 删除所有
    {' '}
    {" "}
    这类空格节点,改用布局间隙(gap)、外边距(margin)或内边距(padding)替代。
  • 优先使用解构而非Props透传。透传不要超过两层;否则应提升状态或使用共享状态。
  • 优先使用组合和小型专用辅助组件,而非包含大量布尔判断的庞大JSX分支。
  • 保持JSX返回值简洁;当渲染块变得臃肿时,将子树提取为命名组件。
  • 优先使用命名处理函数而非内联箭头函数。当函数体达到3行及以上或为异步函数时,提取为处理函数:
    const handleSubmit = async () => { ... }
    而非
    onClick={async () => { ... }}
  • 对于无需等待结果的异步处理函数,使用
    void
    避免阻塞:
    void handleSubmit()

Components and hooks

组件与Hooks

  • Prefer named
    export function
    declarations.
  • Destructure props in the function signature or immediately at the top of the function body. Do not thread
    props.foo
    chains.
  • Prefer full variable names over abbreviations.
  • Avoid comments that merely restate the code.
  • Co-locate a custom hook with the component that owns it unless it is reused.
  • Before creating a new component, enumerate what exists and confirm nothing covers the use case.
  • Prefer existing page, hook, query, type, and layout patterns over new abstractions.
  • 优先使用命名的
    export function
    声明。
  • 在函数签名中或函数体顶部立即解构Props。不要使用
    props.foo
    这类链式调用。
  • 优先使用完整变量名而非缩写。
  • 避免仅重复代码内容的注释。
  • 自定义Hook与其所属组件放在同一位置,除非该Hook会被复用。
  • 创建新组件前,先列举现有组件,确认没有能覆盖当前用例的组件。
  • 优先使用现有的页面、Hook、查询、类型和布局模式,而非创建新的抽象。

useEffect
, the last resort

useEffect
,最后的选择

useEffect
is almost never the right tool. Work through this list first:
ProblemPrefer instead
Derived value from state or propsCompute inline during render;
useMemo
if expensive
Fetching dataTanStack Query
Resetting state when a prop changes
key
prop to remount, or compute during render
Syncing with an external store
useSyncExternalStore
DOM measurements before paint
useLayoutEffect
Triggering animations
useLayoutEffect
or a dedicated animation hook
useEffect
is appropriate only for genuine external side effects with cleanup, event listeners, WebSocket connections, third-party library setup, or browser APIs that have no React abstraction. If there is no cleanup and no external system, it is almost certainly the wrong choice.
Never use
useEffect
to sync one piece of state into another, that is always a sign the state model needs restructuring.
useEffect
几乎从来都不是正确的工具。请先按以下列表排查:
问题优先替代方案
从状态或Props派生值在渲染时内联计算;若计算成本高则使用
useMemo
获取数据TanStack Query
当Props变化时重置状态使用
key
属性重新挂载组件,或在渲染时计算
与外部存储同步
useSyncExternalStore
绘制前测量DOM
useLayoutEffect
触发动画
useLayoutEffect
或专用的动画Hook
只有在处理真正需要清理的外部副作用时,
useEffect
才是合适的选择,比如事件监听器、WebSocket连接、第三方库初始化,或没有React抽象的浏览器API。如果不需要清理且没有涉及外部系统,那它几乎肯定不是正确的选择。
绝不要使用
useEffect
将一段状态同步到另一段状态,这永远是状态模型需要重构的信号。

State and data ownership

状态与数据所有权

  • Prefer local state first.
  • Lift state only when two or more components genuinely share it.
  • Prefer Redux (or equivalent global store) for shared non-serializable state or when an existing slice already owns that UI state. Do not duplicate it in local state.
  • Use TanStack Query for all server state. Keep
    queryOptions(...)
    factories in a dedicated
    query/
    directory. Do not put fetch results in Redux.
  • Keep server state separate from UI state, do not mix fetch results into UI slices.
  • Keep HTTP access in a dedicated
    api/
    layer. Components should not call
    fetch
    directly.
  • Keep contract types in
    src/types/
    . Do not invent frontend-only API contracts, align with the actual producer.
  • Prefer optimistic UI updates for mutations that are unlikely to fail. Roll back on error using the query client's
    onMutate
    /
    onError
    pattern.
  • 优先使用局部状态。
  • 仅当两个或更多组件真正需要共享状态时,才提升状态。
  • 对于共享的非可序列化状态,或当已有状态切片已经管理该UI状态时,优先使用Redux(或等效的全局存储)。不要在局部状态中重复存储。
  • 所有服务端状态都使用TanStack Query管理。将
    queryOptions(...)
    工厂函数放在专门的
    query/
    目录中。不要将请求结果存入Redux。
  • 保持服务端状态与UI状态分离,不要将请求结果混入UI状态切片。
  • 将HTTP请求放在专门的
    api/
    层中。组件不应直接调用
    fetch
  • 将契约类型放在
    src/types/
    目录中。不要自行定义仅前端使用的API契约,要与实际的后端生产者保持一致。
  • 对于不太可能失败的变更操作,优先使用乐观UI更新。通过查询客户端的
    onMutate
    /
    onError
    模式在出错时回滚。

Routing

路由

  • Validate search params explicitly at the route level.
  • Derive loader dependencies from search/route state rather than component effects.
  • Preload with route loaders when route state is required before render.
  • Preserve route and layout structure unless the task intentionally changes it.
  • 在路由层面显式验证搜索参数。
  • 从搜索/路由状态派生加载器依赖,而非组件的副作用。
  • 当路由状态需要在渲染前获取时,使用路由加载器进行预加载。
  • 除非任务有意改变路由和布局结构,否则保持其原有结构。

Error and loading states

错误与加载状态

  • Always handle loading, error, and empty states explicitly, avoid silent no-ops.
  • Prefer graceful missing-data behavior over throwing or crashing.
  • Surface errors at the boundary closest to the user action.
  • 始终显式处理加载、错误和空状态,避免静默无操作。
  • 优先使用优雅的缺失数据处理行为,而非抛出错误或崩溃。
  • 在最接近用户操作的边界处展示错误信息。

Review checklist

评审检查清单

  • JSX conditionals written as ternaries returning
    null
    instead of
    &&
    ?
  • No nested ternaries, replaced with rendering variables or
    if
    statements?
  • Multi-flag conditions use
    [].every(Boolean)
    /
    [].some(Boolean)
    where readable?
  • No stray
    {' '}
    or
    {" "}
    spacer nodes?
  • Props destructured at the boundary? No drilling beyond two levels?
  • Async handlers named and invoked with
    void
    where fire-and-forget?
  • Inline arrow functions 3+ lines extracted to named handlers?
  • Could any
    useEffect
    be replaced with derived state,
    useMemo
    ,
    useLayoutEffect
    ,
    useSyncExternalStore
    , or TanStack Query?
  • Server state owned by TanStack Query, not local state or Redux?
  • Mutations use optimistic updates where failure is unlikely?
  • Shared state stays local unless a global store was actually warranted?
  • Imports use
    @/
    and
    import type
    where appropriate?
  • Loading, error, and empty states all handled?
  • No frontend-only API contracts invented, types align with the actual producer?
  • Change preserves the app's routing and data-loading model?
  • Before creating a component, was the existing component inventory checked?
  • JSX条件是否使用返回
    null
    的三元表达式而非
    &&
  • 是否没有嵌套三元表达式,改用渲染变量或
    if
    语句?
  • 多标志位条件是否在可读性允许的情况下使用
    [].every(Boolean)
    /
    [].some(Boolean)
  • 是否没有多余的
    {' '}
    {" "}
    空格节点?
  • Props是否在边界处解构?透传是否不超过两层?
  • 异步处理函数是否已命名,且在无需等待结果时使用
    void
    调用?
  • 3行及以上的内联箭头函数是否已提取为命名处理函数?
  • 是否有
    useEffect
    可以被派生状态、
    useMemo
    useLayoutEffect
    useSyncExternalStore
    或TanStack Query替代?
  • 服务端状态是否由TanStack Query管理,而非局部状态或Redux?
  • 变更操作是否在失败可能性低的情况下使用了乐观更新?
  • 共享状态是否保持局部,除非确实需要全局存储?
  • 导入是否在合适的地方使用了
    @/
    import type
  • 是否处理了加载、错误和空状态?
  • 是否没有自行定义仅前端使用的API契约,类型与实际生产者保持一致?
  • 变更是否保留了应用的路由和数据加载模型?
  • 创建组件前是否检查了现有组件库?",