React / Vue 组件设计与状态管理
🧱 优秀组件的关键不是框架语法,而是清晰的职责、稳定的数据流、可组合的 API 和合理的状态归属。
组件设计原则
- 单一职责:组件围绕一个稳定目的组织。
- 组合优于继承:通过 children、slots、render props 或组合式函数扩展。
- 受控与非受控明确:避免同一状态出现两个真相来源。
- 数据向下、事件向上:让状态变化路径可追踪。
- 业务与展示适度分离:复杂请求和业务规则可提取到 hooks / composables。
状态分类
- 局部 UI 状态:弹窗开关、输入状态,放在组件附近。
- 跨组件客户端状态:主题、用户偏好、复杂编辑状态,按作用域选择 Context、Pinia、Zustand 等。
- 服务端状态:请求缓存、失效、重试,优先交给 TanStack Query 等工具。
- URL 状态:筛选、分页、可分享视图,应进入路由参数。
- 派生状态:能由现有状态计算时不要重复存储。
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用于与外部系统同步,而不是处理所有派生逻辑。- 不要默认使用
memo、useMemo、useCallback;先测量。 - 列表
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 应清晰暴露状态、操作和生命周期。
- 谨慎使用深度监听;优先监听明确的数据切片。
状态归属决策
依次询问:
- 只有一个组件使用吗?放在本地。
- 最近公共父组件可以管理吗?提升到父组件。
- 属于一个页面或功能域吗?使用局部 store / provider。
- 属于服务器数据吗?使用请求缓存。
- 需要刷新、分享、前进后退吗?使用 URL。
常见反模式
- 全部状态都放进全局 store。
- 同时维护 props、store 和本地 state 的相同副本。
- 超大组件混合请求、权限、表单和视图逻辑。
- 用 Effect / watch 修复错误的数据模型。
- 组件 API 暴露过多布尔属性,形成组合爆炸。
设计检查清单
- 状态有唯一真相来源
- 组件 API 能表达合法状态并排除非法组合
- 异步状态包含 loading、error、empty 与成功场景
- 键盘操作和 ARIA 语义完整
- 可测试业务逻辑已与 DOM 细节适度分离