跳到主要内容

React / Vue 组件设计与状态管理

🧱 优秀组件的关键不是框架语法,而是清晰的职责、稳定的数据流、可组合的 API 和合理的状态归属。

组件设计原则

  • 单一职责:组件围绕一个稳定目的组织。
  • 组合优于继承:通过 children、slots、render props 或组合式函数扩展。
  • 受控与非受控明确:避免同一状态出现两个真相来源。
  • 数据向下、事件向上:让状态变化路径可追踪。
  • 业务与展示适度分离:复杂请求和业务规则可提取到 hooks / composables。

状态分类

  1. 局部 UI 状态:弹窗开关、输入状态,放在组件附近。
  2. 跨组件客户端状态:主题、用户偏好、复杂编辑状态,按作用域选择 Context、Pinia、Zustand 等。
  3. 服务端状态:请求缓存、失效、重试,优先交给 TanStack Query 等工具。
  4. URL 状态:筛选、分页、可分享视图,应进入路由参数。
  5. 派生状态:能由现有状态计算时不要重复存储。

React 示例

type TabsProps = {
value: string
onChange: (value: string) => void
items: Array<{ value: string; label: string }>
}

function Tabs({ value, onChange, items }: TabsProps) {
return (
<div role="tablist">
{items.map(item => (
<button
key={item.value}
role="tab"
aria-selected={item.value === value}
onClick={() => onChange(item.value)}
>
{item.label}
</button>
))}
</div>
)
}

React 注意点:

  • 保持渲染纯粹,不在 render 中执行副作用。
  • useEffect 用于与外部系统同步,而不是处理所有派生逻辑。
  • 不要默认使用 memouseMemouseCallback;先测量。
  • 列表 key 表达元素身份,不要在可重排列表中使用索引。

Vue 示例

<script setup lang="ts">
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
</script>

<template>
<input
:value="props.modelValue"
@input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
/>
</template>

Vue 注意点:

  • computed 表达派生状态,watch 处理副作用。
  • 不要解构普通 reactive 对象后期待属性仍保持响应式。
  • composable 应清晰暴露状态、操作和生命周期。
  • 谨慎使用深度监听;优先监听明确的数据切片。

状态归属决策

依次询问:

  1. 只有一个组件使用吗?放在本地。
  2. 最近公共父组件可以管理吗?提升到父组件。
  3. 属于一个页面或功能域吗?使用局部 store / provider。
  4. 属于服务器数据吗?使用请求缓存。
  5. 需要刷新、分享、前进后退吗?使用 URL。

常见反模式

  • 全部状态都放进全局 store。
  • 同时维护 props、store 和本地 state 的相同副本。
  • 超大组件混合请求、权限、表单和视图逻辑。
  • 用 Effect / watch 修复错误的数据模型。
  • 组件 API 暴露过多布尔属性,形成组合爆炸。

设计检查清单

  • 状态有唯一真相来源
  • 组件 API 能表达合法状态并排除非法组合
  • 异步状态包含 loading、error、empty 与成功场景
  • 键盘操作和 ARIA 语义完整
  • 可测试业务逻辑已与 DOM 细节适度分离