ReactJS实现Section组件自动计数 解决StrictMode计数翻倍问题
React 自动递增Section序号实现方案

需求说明
需要实现以下能力:
- 无需手动给
Section组件传入序号属性 - 在
Container组件内渲染多个无传参的Section时,自动生成从1开始递增的连续序号 - 兼容
React.StrictMode,不会出现序号翻倍异常
初始手动传参实现(不符合需求)
export const Section = ({ number }) => { return <div>Section {number}</div>; }; export const Container = () => { return ( <div> <Section number={1} /> <Section number={2} /> <Section number={3} /> <Section number={4} /> </div> ); };
目标代码结构
export const Container = () => { return ( <div> <Section /> <Section /> <Section /> <Section /> </div> ); };
之前Context方案序号翻倍的原因
React.StrictMode在开发环境会重复执行组件渲染、初始化逻辑,用来检测代码中的不规范写法。之前的Context实现出现2、4、6、8的翻倍问题,核心是违反了React的渲染纯函数原则:在组件渲染阶段直接修改Context中的共享计数器变量,重复渲染时会重复执行递增逻辑,最终导致计数异常。
典型错误写法示例:
// 错误:渲染阶段直接修改外部状态 const CountContext = createContext({ count: 0, setCount: () => {} }); const Section = () => { const { count, setCount } = useContext(CountContext); setCount(count + 1); // 渲染阶段直接更新状态,StrictMode下会重复执行 return <div>Section {count}</div>; };
可行实现方案
方案1:Children遍历注入属性(简单场景首选)
如果Section都是Container的直接子元素,这个方案实现最简单、稳定性最高,从根源避免StrictMode兼容问题:
import React from 'react'; export const Section = ({ number }) => { return <div>Section {number}</div>; }; export const Container = ({ children }) => { let sectionIndex = 0; // 遍历所有子元素,给Section组件自动注入序号 const renderNodes = React.Children.map(children, (child) => { if (child?.type === Section) { sectionIndex++; return React.cloneElement(child, { number: sectionIndex }); } return child; }); return <div>{renderNodes}</div>; };
方案特性:
- 逻辑完全收敛在Container层,无额外状态管理成本
- 支持Container内混写其他非Section组件,不会打乱计数顺序
- 完全兼容StrictMode,无重复计数问题
- 缺点:不支持Section嵌套在Container的深层子节点中(比如中间包裹了div、其他布局组件)
方案2:修正版Context实现(支持深层嵌套场景)
如果需要支持Section在Container下任意层级嵌套,可使用基于注册机制的Context方案,将注册逻辑放在副作用中执行,避免渲染阶段修改状态:
import React, { createContext, useContext, useRef, useLayoutEffect, useState } from 'react'; const SectionContext = createContext(null); const SectionCounterProvider = ({ children }) => { // 用ref存储已注册的Section实例,避免渲染阶段触发状态更新 const sectionMap = useRef(new Map()); const [, reRender] = useState({}); // 注册Section,返回注销方法 const register = (refId) => { sectionMap.current.set(refId, true); reRender({}); return () => { sectionMap.current.delete(refId); reRender({}); }; }; // 按注册顺序计算当前Section的序号 const getNumber = (refId) => { return Array.from(sectionMap.current.keys()).indexOf(refId) + 1; }; return ( <SectionContext.Provider value={{ register, getNumber }}> {children} </SectionContext.Provider> ); }; export const Section = () => { const { register, getNumber } = useContext(SectionContext); const selfRef = useRef(Symbol()); const [num, setNum] = useState(0); useLayoutEffect(() => { // 注册逻辑放在副作用中执行,StrictMode下重复执行时清理函数会自动注销冗余实例 const unregister = register(selfRef.current); setNum(getNumber(selfRef.current)); return unregister; }, [register, getNumber]); return <div>Section {num}</div>; }; export const Container = ({ children }) => { return ( <SectionCounterProvider> <div>{children}</div> </SectionCounterProvider> ); };
方案特性:
- 支持任意层级嵌套,即使Section包裹在多层布局组件内也能正确计数
- 注册/注销逻辑放在
useLayoutEffect中执行,StrictMode下重复挂载的实例会被清理函数正确移除,不会出现计数翻倍 - 支持动态增删Section,序号会自动更新
内容的提问来源于stack exchange,提问作者Stephan
相关产品推荐
相关产品推荐

