React中如何用布局各异的组件实现自动编号的步骤列表
可行实现方案
一共3种生产环境常用的实现,按复杂度从低到高选择即可:
方案1:父组件注入编号(无额外依赖,最稳妥)
如果步骤组件都是步骤容器的直接子节点,完全不需要Context或者HOC,直接在父组件遍历子节点时注入编号属性即可。
核心实现代码:
// 步骤容器组件 const StepContainer = ({ children }) => { let stepCount = 0 // 遍历子节点,给有效Step组件注入number属性 const renderedChildren = React.Children.map(children, child => { if (!React.isValidElement(child) || child.type !== Step) return child stepCount += 1 return React.cloneElement(child, { number: stepCount }) }) return <div className="step-list">{renderedChildren}</div> } // 单个步骤组件 const Step = ({ number, title, children }) => { return ( <div className="step-item"> <h3>Step {number}: {title}</h3> <div className="step-content">{children}</div> </div> ) }
使用时直接嵌套即可,编号自动生成:
<StepContainer> <Step title="填写基础信息">{/* 第一步自定义内容 */}</Step> <Step title="上传材料">{/* 第二步自定义内容 */}</Step> <Step title="确认提交">{/* 第三步自定义内容 */}</Step> </StepContainer>
这个方案的局限是:Step组件必须是StepContainer的直接子节点,如果中间套了<div>、栅格组件这类布局节点,就无法识别注入编号。
方案2:Context + 自动计数HOC(支持嵌套布局,灵活度最高)
如果步骤组件可能被其他布局组件包裹,不是容器的直接子节点,就用Context做跨层级编号传递,配合HOC自动获取编号,不需要手动传属性。
核心实现:
- 先创建Context,存储计数递增方法
const StepContext = React.createContext({ getNextStepNumber: () => 1 })
- 改造StepContainer,在组件内部维护计数,提供递增方法
const StepContainer = ({ children }) => { const [count, setCount] = React.useState(0) const getNextStepNumber = React.useCallback(() => { setCount(prev => prev + 1) return count + 1 }, [count]) return ( <StepContext.Provider value={{ getNextStepNumber }}> <div className="step-list">{children}</div> </StepContext.Provider> ) }
- 编写HOC,给Step组件自动注入编号,组件挂载时自动获取序号
const withStepNumber = (WrappedComponent) => { return (props) => { const { getNextStepNumber } = React.useContext(StepContext) const numberRef = React.useRef(null) // 首次渲染时获取编号,避免重渲染导致序号错乱 if (numberRef.current === null) { numberRef.current = getNextStepNumber() } return <WrappedComponent {...props} number={numberRef.current} /> } } // 用HOC包装Step组件 const Step = withStepNumber(({ number, title, children }) => { return ( <div className="step-item"> <h3>Step {number}: {title}</h3> <div className="step-content">{children}</div> </div> ) })
这种方案下,哪怕Step组件被多层布局包裹,编号也能正常生成,比如下面的写法完全可以正常运行:
<StepContainer> <div className="row"> <div className="col"> <Step title="填写基础信息">{/* 内容 */}</Step> </div> </div> <Grid> <Grid.Col> <Step title="上传材料">{/* 内容 */}</Step> </Grid.Col> </Grid> <Step title="确认提交">{/* 内容 */}</Step> </StepContainer>
注意不要用模块级别的全局变量存计数,否则同一个页面渲染多个StepContainer的时候,序号会互相干扰,出现跨容器累加的问题。以上两种方案的计数都存在组件实例内部,多实例渲染不会有冲突。
方案3:CSS计数器(零JS逻辑,纯样式实现)
如果不需要在JS逻辑里拿到步骤编号,只是要在标题上显示Step #:的文本,甚至可以不用写JS计数逻辑,直接用CSS的counter-reset和counter-increment实现,代码量最少。
核心代码:
.step-list { counter-reset: step-counter; /* 初始化计数器 */ } .step-item { counter-increment: step-counter; /* 每个步骤项计数器+1 */ } .step-title::before { content: "Step " counter(step-counter) ": "; /* 自动生成编号前缀 */ }
对应的JSX只需要正常写结构,不需要传任何编号相关属性:
<div className="step-list"> <div className="step-item"> <h3 className="step-title">填写基础信息</h3> {/* 内容 */} </div> {/* 其他步骤项结构一致 */} </div>
这个方案最轻量,但缺点是编号只存在于CSS渲染层,JS逻辑里拿不到对应序号,如果后续需要做步骤跳转、步骤校验这类依赖编号的逻辑,扩展成本很高。
选型参考
- 步骤都是直接子节点、逻辑简单选方案1,代码少坑少
- 布局嵌套多、后续可能扩展步骤相关逻辑选方案2,灵活度最高
- 纯展示需求、不需要JS操作编号选方案3,几行CSS就能搞定
内容的提问来源于stack exchange,提问作者Evgeny Vinnik
相关产品推荐
相关产品推荐

