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 简洁