如何实现支持可选受控/非受控模式且处理Props依赖的Collapse组件?
这个问题在组件设计里太常见了!我来分享一下业界通用的解决思路,分两部分帮你搞定这两个困扰:
一、实现受控/非受控模式的通用方案
受控组件和非受控组件的核心区别在于状态的控制权:非受控组件自己管理状态,受控组件由外部父组件接管状态。通用的实现步骤是:
- 判断受控模式:通过检查外部是否传入了
expanded(即expanded !== undefined)来区分模式 - 状态合并处理:受控时直接使用外部传入的
expanded,非受控时使用内部useState维护状态 - 统一回调逻辑:受控模式下状态变更需要通知外部,所以要依赖外部传入的
onChange;非受控模式下内部自行处理状态切换
修改后的组件代码示例:
import { useState, useEffect } from 'react'; export const Collapse = ({ ExpandIcon = StyledExpandIcon, CollapseIcon = StyledCollapseIcon, expanded, onChange, noButton = false, ...props }) => { // 标记是否为受控模式 const isControlled = expanded !== undefined; // 非受控模式下的内部状态,默认值用外部传入的expanded(如果有),否则为false const [internalExpanded, setInternalExpanded] = useState(expanded ?? false); // 统一当前生效的展开状态 const currentExpanded = isControlled ? expanded : internalExpanded; // 处理展开/收起触发逻辑 const handleToggle = () => { if (isControlled) { // 受控模式下必须调用外部的onChange,把新状态传递出去 onChange?.(!currentExpanded); } else { // 非受控模式下直接更新内部状态 setInternalExpanded(prev => !prev); } }; return ( <> {!noButton && ( <StyledIconButton onClick={handleToggle}> {currentExpanded ? <CollapseIcon /> : <ExpandIcon />} </StyledIconButton> )} <StyledCollapse in={currentExpanded} {...props} /> </> ); };
二、处理Props联动必填的通用方案
你提到的「传入expanded或noButton其中一个时,另一个必须必填」,本质是限制组件的使用场景:要么用默认的非受控+自带按钮,要么用受控+自定义按钮(移除默认按钮)。通用的处理方式分两种:
1. 运行时校验(JS/TS通用)
在组件内部加入校验逻辑,当不符合规则时抛出明确的错误提示,帮助开发者快速定位问题。可以用useEffect或者直接在组件开头判断:
// 放在上面组件的handleToggle之后 useEffect(() => { // 规则:要么同时传expanded和noButton=true,要么都不传 const hasExpanded = expanded !== undefined; const hasNoButton = noButton === true; if ((hasExpanded && !hasNoButton) || (!hasExpanded && hasNoButton)) { throw new Error('Collapse组件使用规则:当传入expanded时必须设置noButton为true;当设置noButton为true时必须传入expanded'); } }, [expanded, noButton]);
2. 静态类型校验(TypeScript专属)
如果用TypeScript,可以通过联合类型拆分不同场景的Props,让编辑器在编译阶段就报错,提前规避问题:
// 定义两种场景的Props类型 type CollapseUncontrolledProps = { // 非受控模式下,expanded和onChange都不能传 expanded?: never; onChange?: never; // 默认保留按钮 noButton?: false; }; type CollapseControlledProps = { // 受控模式下必须传expanded expanded: boolean; // 必须传onChange来接收状态变更 onChange: (expanded: boolean) => void; // 必须移除默认按钮 noButton: true; }; // 合并通用Props和两种场景的Props type CollapseProps = (CollapseUncontrolledProps | CollapseControlledProps) & { ExpandIcon?: React.ElementType; CollapseIcon?: React.ElementType; // 这里可以添加StyledCollapse支持的其他Props } & React.ComponentPropsWithoutRef<typeof StyledCollapse>; // 组件的Props类型换成上面定义的CollapseProps export const Collapse = ({ ExpandIcon = StyledExpandIcon, CollapseIcon = StyledCollapseIcon, expanded, onChange, noButton = false, ...props }: CollapseProps) => { // 内部逻辑和之前一致 };
这样当开发者错误地只传expanded或者只传noButton=true时,TypeScript会直接在编辑器里提示错误,比运行时校验更高效。
总结一下通用方案
- 受控/非受控组件:通过判断props是否存在来区分模式,统一状态获取逻辑,受控模式依赖外部回调
- Props联动校验:优先用TypeScript联合类型做静态校验,再配合运行时校验做兜底,确保组件使用符合预期
内容的提问来源于stack exchange,提问作者Darkbound
相关产品推荐
相关产品推荐

