pr-check-frontend

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

PR Review — Frontend Patterns

PR评审——前端模式

Overview

概述

This skill runs a frontend-specific self-review checklist covering React, Next.js, Vue.js, Tailwind CSS, and React Query. Use alongside
pr-review-static
and
pr-review-human
for a complete review.
本技能会运行针对React、Next.js、Vue.js、Tailwind CSS和React Query的前端专属自我检查清单。可搭配
pr-review-static
pr-review-human
完成完整评审。

Required Workflow

必备工作流

Follow these steps in order.
请按以下顺序执行步骤。

Step 1: Understand the diff

步骤1:理解代码差异

Run
git diff main...HEAD
to see all changed files and understand the scope.
运行
git diff main...HEAD
查看所有变更文件,了解修改范围。

Step 2: Apply the Checklist

步骤2:应用检查清单

Work through each category. Flag every issue with file path and line number.

逐一检查每个分类,标记所有问题并注明文件路径和行号。

Checklist

检查清单

F1. React Hooks Rules Violations

F1. React钩子规则违反

React hooks must be called at the top level — never inside conditions, loops, or nested functions.
"Hook called inside a condition — if the condition is false on the first render, hook order changes on subsequent renders and causes state corruption."
Check:
  • Are any hooks (
    useState
    ,
    useEffect
    ,
    useMemo
    ,
    useCallback
    ,
    useQuery
    , etc.) called inside
    if
    ,
    for
    , or nested functions?
  • Are custom hooks always called at the top level of the component?
  • Are hooks called from React function components or custom hooks only (not regular functions)?
React钩子必须在顶层调用——绝不能在条件语句、循环或嵌套函数内部调用。
"钩子在条件语句内部调用——如果首次渲染时条件不成立,后续渲染时钩子顺序会改变,导致状态损坏。"
检查项:
  • 是否有任何钩子(
    useState
    useEffect
    useMemo
    useCallback
    useQuery
    等)在
    if
    for
    或嵌套函数内部调用?
  • 自定义钩子是否始终在组件的顶层调用?
  • 钩子是否仅在React函数组件或自定义钩子中调用(而非普通函数)?

F2. Missing or Incorrect useEffect Dependencies

F2. useEffect依赖缺失或错误

useEffect
with a missing or incorrect dependency array causes stale closures or infinite loops.
"The effect reads
userId
but it's missing from the dependency array — the effect will always run with the initial value even after userId changes." "Adding the callback directly in deps without
useCallback
causes the effect to re-run on every render."
Check:
  • Does every
    useEffect
    have a dependency array?
  • Are all variables/functions used inside the effect listed in the deps array?
  • Are functions in deps wrapped in
    useCallback
    to prevent infinite loops?
  • Are objects/arrays in deps stable (memoized) or will they cause infinite re-renders?
useEffect
依赖数组缺失或错误会导致闭包过期或无限循环。
"该effect读取了
userId
但未将其加入依赖数组——即使userId发生变化,effect也始终使用初始值运行。" "直接将回调函数加入依赖数组却未用
useCallback
包裹,会导致effect在每次渲染时都重新运行。"
检查项:
  • 每个
    useEffect
    是否都有依赖数组?
  • effect内部使用的所有变量/函数是否都已列入依赖数组?
  • 依赖数组中的函数是否用
    useCallback
    包裹以避免无限循环?
  • 依赖数组中的对象/数组是否稳定(已被记忆化),是否会导致无限重渲染?

F3. Unnecessary Re-renders

F3. 不必要的重渲染

Components re-rendering on every parent render when they don't need to.
Check:
  • Are expensive components wrapped in
    React.memo
    where appropriate?
  • Are callback props passed to memoized child components wrapped in
    useCallback
    ?
  • Are derived values that are expensive to compute wrapped in
    useMemo
    ?
  • Are new object/array literals created inline as props? (e.g.,
    style={{ margin: 0 }}
    on every render)
  • Are anonymous functions passed as props causing unnecessary re-renders?
组件在父组件每次渲染时都重新渲染,但实际上并无必要。
检查项:
  • 开销较大的组件是否在合适的场景下用
    React.memo
    包裹?
  • 传递给已记忆化子组件的回调属性是否用
    useCallback
    包裹?
  • 计算开销较大的派生值是否用
    useMemo
    包裹?
  • 是否在属性中内联创建新的对象/数组字面量?(例如,每次渲染时的
    style={{ margin: 0 }}
  • 是否将匿名函数作为属性传递,导致不必要的重渲染?

F4. Missing key Prop or Unstable Keys

F4. 缺失key属性或key不稳定

React uses
key
to track list items — missing or unstable keys cause incorrect reconciliation.
"Using array index as key means inserting an item at the top shifts all keys, causing React to re-render every item instead of just inserting one."
Check:
  • Does every
    .map()
    rendering JSX have a
    key
    prop?
  • Are keys unique and stable — not array indexes, not random values (e.g.,
    Math.random()
    )?
  • Are keys derived from a stable ID from the data (e.g.,
    item.id
    )?
React使用
key
跟踪列表项——缺失或不稳定的key会导致错误的协调过程。
"使用数组索引作为key意味着在顶部插入项时会改变所有key,导致React重新渲染每个项而非仅插入新项。"
检查项:
  • 每个渲染JSX的
    .map()
    是否都有
    key
    属性?
  • key是否唯一且稳定——不是数组索引,也不是随机值(例如
    Math.random()
    )?
  • key是否来自数据中的稳定ID(例如
    item.id
    )?

F5. Next.js SSR/SSG Gotchas

F5. Next.js SSR/SSG陷阱

Code that runs fine on the client can crash during server-side rendering.
"
window.localStorage
accessed at module level — throws
ReferenceError: window is not defined
during SSR." "
useRouter()
called outside a component — works in browser but fails during static generation."
Check:
  • Is
    window
    ,
    document
    ,
    navigator
    , or
    localStorage
    accessed outside of
    useEffect
    or a client-only check?
  • Are browser-only APIs guarded with
    typeof window !== 'undefined'
    ?
  • Are third-party libraries that access
    window
    imported dynamically with
    { ssr: false }
    ?
  • Is
    useSearchParams()
    used without wrapping in a
    Suspense
    boundary (Next.js 13+)?
在客户端运行正常的代码可能在服务器端渲染时崩溃。
"在模块级别访问
window.localStorage
——SSR期间会抛出
ReferenceError: window is not defined
错误。" "在组件外部调用
useRouter()
——在浏览器中可用但在静态生成时失败。"
检查项:
  • 是否在
    useEffect
    或客户端专属检查之外访问
    window
    document
    navigator
    localStorage
  • 仅浏览器可用的API是否用
    typeof window !== 'undefined'
    进行了防护?
  • 访问
    window
    的第三方库是否通过
    { ssr: false }
    进行动态导入?
  • Next.js 13+中使用
    useSearchParams()
    时是否未包裹在
    Suspense
    边界中?

F6. Next.js Image and Font Optimization

F6. Next.js图片和字体优化

Using
<img>
instead of
<Image>
bypasses Next.js optimization.
Check:
  • Are all images using Next.js
    <Image>
    component instead of
    <img>
    ?
  • Do
    <Image>
    components have
    width
    ,
    height
    , or
    fill
    specified?
  • Are external image domains added to
    next.config.js
    images.domains
    ?
  • Are fonts loaded via
    next/font
    instead of a
    <link>
    tag in
    _document
    ?
使用
<img>
而非
<Image>
会绕过Next.js的优化机制。
检查项:
  • 所有图片是否都使用Next.js的
    <Image>
    组件而非
    <img>
  • <Image>
    组件是否指定了
    width
    height
    fill
  • 外部图片域名是否已添加到
    next.config.js
    images.domains
    中?
  • 字体是否通过
    next/font
    加载,而非在
    _document
    中使用
    <link>
    标签?

F7. React Query — Stale Data and Error Handling

F7. React Query—— stale数据和错误处理

Incorrect React Query configuration leads to stale UI or silent failures.
Check:
  • Is
    staleTime
    configured appropriately? Default is
    0
    — every focus triggers a refetch.
  • Are query errors handled — is there an
    error
    state shown to the user, not just
    isLoading
    ?
  • Are mutations using
    onError
    to surface failures to the user?
  • After a mutation, is
    queryClient.invalidateQueries
    called to refresh affected queries?
  • Are dependent queries using
    enabled: !!dependency
    to prevent firing before data is ready?
  • Are query keys consistent and specific — does the key include all variables the query depends on?
React Query配置错误会导致UI数据过期或静默失败。
检查项:
  • staleTime
    配置是否合理?默认值为
    0
    ——每次聚焦都会触发重新获取。
  • 查询错误是否已处理——是否向用户展示了
    error
    状态,而非仅显示
    isLoading
  • 突变操作是否使用
    onError
    向用户展示失败信息?
  • 突变操作后是否调用
    queryClient.invalidateQueries
    刷新受影响的查询?
  • 依赖查询是否使用
    enabled: !!dependency
    来防止在数据准备就绪前触发?
  • 查询key是否一致且具体——key是否包含查询依赖的所有变量?

F8. Vue.js Reactivity Pitfalls

F8. Vue.js响应式陷阱

Vue's reactivity system has known edge cases where changes are not detected.
Check:
  • Are new properties added to a reactive object using
    set()
    /
    reactive()
    re-assignment, not direct assignment (
    obj.newProp = value
    is not reactive in Vue 2)?
  • In Vue 3 Composition API: are
    ref
    values accessed with
    .value
    inside
    <script setup>
    ?
  • Are
    computed
    properties used for derived state rather than recalculating in the template?
  • Are
    watch
    dependencies stable — not causing infinite loops from mutating the watched value?
  • Are
    v-for
    directives paired with
    :key
    bindings using stable IDs?
Vue的响应式系统存在一些已知的边界情况,其中变更无法被检测到。
检查项:
  • 向响应式对象添加新属性时是否使用
    set()
    /
    reactive()
    重新赋值,而非直接赋值(Vue 2中
    obj.newProp = value
    不具有响应式)?
  • Vue 3组合式API中:
    <script setup>
    内的
    ref
    值是否通过
    .value
    访问?
  • 派生状态是否使用
    computed
    属性而非在模板中重新计算?
  • watch
    的依赖是否稳定——是否因修改被监听的值而导致无限循环?
  • v-for
    指令是否搭配使用
    :key
    绑定,且key为稳定ID?

F9. Tailwind CSS — Purge and Dynamic Classes

F9. Tailwind CSS——清除和动态类

Tailwind purges unused classes at build time — dynamically constructed class names are stripped.
"
'text-' + color
will be purged because Tailwind can't statically analyse the full class name."
Check:
  • Are class names constructed dynamically? (e.g.,
    `text-${color}`
    ) — these will be purged in production.
  • Use full class names in conditionals:
    condition ? 'text-red-500' : 'text-green-500'
    not
    'text-' + color
    .
  • Are custom classes in
    tailwind.config.js
    safelist
    if they must be dynamic?
  • Are responsive breakpoints (
    sm:
    ,
    md:
    ,
    lg:
    ) applied mobile-first?
Tailwind会在构建时清除未使用的类——动态构造的类名会被移除。
"
'text-' + color
会被清除,因为Tailwind无法静态分析完整的类名。"
检查项:
  • 是否存在动态构造的类名?(例如
    `text-${color}`
    )——这些类在生产环境中会被清除。
  • 在条件语句中使用完整类名:
    condition ? 'text-red-500' : 'text-green-500'
    而非
    'text-' + color
  • 必须动态使用的自定义类是否已加入
    tailwind.config.js
    safelist
  • 响应式断点(
    sm:
    md:
    lg:
    )是否采用移动优先的方式应用?

F10. Accessibility (a11y)

F10. 无障碍(a11y)

Check:
  • Do all
    <img>
    elements have an
    alt
    attribute (empty
    alt=""
    for decorative images)?
  • Do interactive elements (
    <div onClick>
    ,
    <span onClick>
    ) use a proper
    <button>
    or
    <a>
    instead?
  • Are form inputs associated with a
    <label>
    (via
    htmlFor
    /
    for
    or
    aria-label
    )?
  • Are modal/dialog components trapping focus correctly and dismissible with Escape?
  • Are icon-only buttons labeled with
    aria-label
    ?
  • Is color contrast sufficient for text against background?
检查项:
  • 所有
    <img>
    元素是否都有
    alt
    属性(装饰性图片使用空的
    alt=""
    )?
  • 交互式元素(
    <div onClick>
    <span onClick>
    )是否改用合适的
    <button>
    <a>
  • 表单输入是否与
    <label>
    关联(通过
    htmlFor
    /
    for
    aria-label
    )?
  • 模态框/对话框组件是否正确捕获焦点,且可通过Escape键关闭?
  • 仅含图标的按钮是否用
    aria-label
    标注?
  • 文本与背景的颜色对比度是否足够?

F11. TypeScript — Component Props Typing

F11. TypeScript——组件属性类型

Check:
  • Are component props typed with an interface or type — not
    any
    ?
  • Are optional props marked with
    ?
    and required props enforced?
  • Are event handler types correct:
    React.ChangeEvent<HTMLInputElement>
    not
    any
    ?
  • Are
    children
    props typed as
    React.ReactNode
    ?
  • Are
    ref
    props typed with
    React.Ref<ElementType>
    when using
    forwardRef
    ?
检查项:
  • 组件属性是否使用接口或类型定义——而非
    any
  • 可选属性是否标记了
    ?
    ,必填属性是否被强制要求?
  • 事件处理函数类型是否正确:使用
    React.ChangeEvent<HTMLInputElement>
    而非
    any
  • children
    属性是否被定义为
    React.ReactNode
    类型?
  • 使用
    forwardRef
    时,
    ref
    属性是否用
    React.Ref<ElementType>
    定义类型?

F12. State Management — Minimal and Co-located State

F12. 状态管理——最小化且就近放置的状态

Check:
  • Is state lifted only as high as necessary? State shared by two siblings belongs in their parent, not a global store.
  • Is server state (data from API) managed via React Query / SWR rather than manually in
    useState
    ?
  • Is derived state computed (via
    useMemo
    or
    computed
    ) rather than stored in a separate
    useState
    ?
  • Are multiple related
    useState
    calls that always update together combined into one object?
检查项:
  • 状态是否仅提升到必要的高度?两个子组件共享的状态应放在父组件中,而非全局存储。
  • 服务端状态(来自API的数据)是否通过React Query/SWR管理,而非手动用
    useState
    处理?
  • 派生状态是否通过
    useMemo
    computed
    计算,而非存储在单独的
    useState
    中?
  • 多个总是一起更新的相关
    useState
    调用是否合并为一个对象?

F13. Environment Variables

F13. 环境变量

Check:
  • Are client-side env vars prefixed with
    NEXT_PUBLIC_
    (Next.js) or
    VITE_
    (Vite)?
  • Are secret/server-only env vars never prefixed with
    NEXT_PUBLIC_
    — this exposes them to the browser bundle?
  • Are all new env vars added to
    .env.example
    with a placeholder value?
检查项:
  • 客户端环境变量是否带有
    NEXT_PUBLIC_
    (Next.js)或
    VITE_
    (Vite)前缀?
  • 机密/仅服务端环境变量是否从未添加
    NEXT_PUBLIC_
    前缀——这会将其暴露给浏览器打包文件?
  • 所有新增环境变量是否都已添加到
    .env.example
    中并带有占位值?

F14. Bundle Size Awareness

F14. 包体积意识

Check:
  • Are large libraries imported in full when only a part is needed? (e.g.,
    import _ from 'lodash'
    vs
    import debounce from 'lodash/debounce'
    )
  • Are heavy components (charts, editors, maps) lazy-loaded with
    dynamic(() => import(...), { ssr: false })
    ?
  • Are new dependencies checked for bundle size impact (bundlephobia.com)?
检查项:
  • 是否在仅需部分功能时导入了完整的大型库?(例如
    import _ from 'lodash'
    vs
    import debounce from 'lodash/debounce'
  • 重型组件(图表、编辑器、地图)是否通过
    dynamic(() => import(...), { ssr: false })
    懒加载?
  • 是否已检查新增依赖对包体积的影响(可使用bundlephobia.com)?

F15. Error Boundaries

F15. 错误边界

Check:
  • Are async components or data-fetching components wrapped in an Error Boundary?
  • In Next.js App Router, is there an
    error.tsx
    for route segments that can fail?
  • Are loading states handled with
    loading.tsx
    /
    Suspense
    rather than conditionally rendering null?
检查项:
  • 异步组件或数据获取组件是否用错误边界包裹?
  • Next.js App Router中,可能失败的路由段是否有对应的
    error.tsx
  • 加载状态是否通过
    loading.tsx
    /
    Suspense
    处理,而非条件渲染null?

F16. Forms and Validation

F16. 表单与验证

Check:
  • Are forms using a form library (React Hook Form, Formik, VeeValidate) rather than manual
    useState
    per field?
  • Is validation both client-side (UX) and server-side (security)?
  • Are form submissions debounced or disabled while in-flight to prevent double submissions?
  • Are error messages shown next to the relevant field, not just a generic toast?

检查项:
  • 表单是否使用表单库(React Hook Form、Formik、VeeValidate)而非为每个字段手动使用
    useState
  • 是否同时进行客户端验证(用户体验)和服务端验证(安全性)?
  • 表单提交是否已防抖或在请求进行中禁用,以防止重复提交?
  • 错误信息是否显示在相关字段旁,而非仅显示通用提示框?

Step 3: Report Findings

步骤3:报告发现

For each issue found, output:
[F3] components/ProductList.tsx:42
Severity: Medium
Issue: Anonymous callback passed as onClick prop to memoized child — defeats memo.
Fix: Wrap the callback in useCallback with appropriate dependencies.

[F9] components/Badge.tsx:8
Severity: High
Issue: Class name constructed as `'bg-' + color` — will be purged in production build.
Fix: Use full class names: condition ? 'bg-red-500' : 'bg-green-500'.
对于每个发现的问题,输出如下格式:
[F3] components/ProductList.tsx:42
Severity: Medium
Issue: Anonymous callback passed as onClick prop to memoized child — defeats memo.
Fix: Wrap the callback in useCallback with appropriate dependencies.

[F9] components/Badge.tsx:8
Severity: High
Issue: Class name constructed as `'bg-' + color` — will be purged in production build.
Fix: Use full class names: condition ? 'bg-red-500' : 'bg-green-500'.

Step 4: Summary

步骤4:总结

End with:
  • Total issues by severity (High / Medium / Low)
  • Files most affected
  • Verdict: ready for review / needs fixes before submitting
结尾需包含:
  • 按严重程度分类的问题总数(高/中/低)
  • 受影响最严重的文件
  • 结论:可提交评审 / 需要修复后再提交",