React 组件设计
React 组件设计的原则和实践方法
问题
如何设计可维护、可复用的 React 组件?
解答
1. 单一职责原则
每个组件只做一件事。
// ❌ 职责混乱
function UserPage() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
// 获取用户、获取文章、渲染用户信息、渲染文章列表全在一起
return (
<div>
<div>{user?.name}</div>
<ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
</div>
);
}
// ✅ 职责分离
function UserPage() {
return (
<div>
<UserProfile />
<UserPosts />
</div>
);
}
function UserProfile() {
const { user } = useUser();
return <div>{user?.name}</div>;
}
function UserPosts() {
const { posts } = usePosts();
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
2. 组合优于继承
通过 children 和 props 组合组件。
// 通用卡片组件
function Card({ children, title, footer }) {
return (
<div className="card">
{title && <div className="card-header">{title}</div>}
<div className="card-body">{children}</div>
{footer && <div className="card-footer">{footer}</div>}
</div>
);
}
// 通过组合创建特定卡片
function ProductCard({ product }) {
return (
<Card
title={product.name}
footer={<button>购买</button>}
>
<p>{product.description}</p>
<span>¥{product.price}</span>
</Card>
);
}
3. 受控与非受控组件
根据场景选择合适的模式。
// 受控组件 - 状态由父组件管理
function ControlledInput({ value, onChange }) {
return <input value={value} onChange={e => onChange(e.target.value)} />;
}
// 非受控组件 - 内部管理状态,通过 ref 获取值
function UncontrolledInput({ defaultValue, inputRef }) {
return <input defaultValue={defaultValue} ref={inputRef} />;
}
// 同时支持两种模式
function FlexibleInput({ value, defaultValue, onChange }) {
// 判断是否受控
const isControlled = value !== undefined;
const [internalValue, setInternalValue] = useState(defaultValue || '');
const currentValue = isControlled ? value : internalValue;
const handleChange = (e) => {
if (!isControlled) {
setInternalValue(e.target.value);
}
onChange?.(e.target.value);
};
return <input value={currentValue} onChange={handleChange} />;
}
4. 逻辑与 UI 分离
使用自定义 Hook 抽离逻辑。
// 自定义 Hook - 处理逻辑
function useToggle(initial = false) {
const [state, setState] = useState(initial);
const toggle = useCallback(() => setState(s => !s), []);
const setTrue = useCallback(() => setState(true), []);
const setFalse = useCallback(() => setState(false), []);
return { state, toggle, setTrue, setFalse };
}
// UI 组件 - 只负责渲染
function Modal({ isOpen, onClose, children }) {
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
{children}
</div>
</div>
);
}
// 使用
function App() {
const { state: isOpen, setTrue: open, setFalse: close } = useToggle();
return (
<>
<button onClick={open}>打开</button>
<Modal isOpen={isOpen} onClose={close}>
<p>内容</p>
</Modal>
</>
);
}
5. 合理的 Props 设计
// ❌ props 过多
<Button
text="提交"
textColor="white"
bgColor="blue"
fontSize={14}
padding={10}
borderRadius={4}
onClick={handleClick}
/>
// ✅ 使用 variant 归类样式
<Button variant="primary" onClick={handleClick}>
提交
</Button>
// 组件实现
function Button({ variant = 'default', size = 'medium', children, ...rest }) {
const classNames = `btn btn-${variant} btn-${size}`;
return <button className={classNames} {...rest}>{children}</button>;
}
6. 复合组件模式
适合有关联的组件组合。
// 创建 Context
const SelectContext = createContext();
// 父组件
function Select({ value, onChange, children }) {
return (
<SelectContext.Provider value={{ value, onChange }}>
<div className="select">{children}</div>
</SelectContext.Provider>
);
}
// 子组件
function Option({ value: optionValue, children }) {
const { value, onChange } = useContext(SelectContext);
const isSelected = value === optionValue;
return (
<div
className={`option ${isSelected ? 'selected' : ''}`}
onClick={() => onChange(optionValue)}
>
{children}
</div>
);
}
// 挂载子组件
Select.Option = Option;
// 使用 - 结构清晰
function App() {
const [value, setValue] = useState('a');
return (
<Select value={value} onChange={setValue}>
<Select.Option value="a">选项 A</Select.Option>
<Select.Option value="b">选项 B</Select.Option>
<Select.Option value="c">选项 C</Select.Option>
</Select>
);
}
关键点
- 单一职责:一个组件只做一件事,大组件拆成小组件
- 组合优于继承:通过 children 和 props 组合,而非继承
- 逻辑分离:用自定义 Hook 抽离业务逻辑,组件只负责渲染
- Props 设计:使用 variant/size 等语义化 props,避免过多样式 props
- 复合组件:关联组件用 Context 共享状态,保持 API 简洁
目录