React动态遍历可变JSON数据并实现父子层级复选框展示的逻辑
React动态遍历任意JSON并实现父子层级复选框的实现方案
核心思路
由于JSON结构不固定,必须通过递归组件处理嵌套的对象/数组结构;同时维护一个全局的选中状态映射,通过节点的唯一路径(如"members[0].powers[2]")追踪每个节点的选中状态,进而实现父子复选框联动:父节点选中时自动选中所有子节点,子节点状态变化时自动更新父节点的选中/半选状态。
实现步骤
1. 状态管理
用useState维护checkedMap对象,键为节点的唯一路径字符串,值为布尔值(记录节点是否选中),以此统一管理所有节点的选中状态。
2. 递归组件设计
创建JsonNode递归组件,接收当前节点的键名、数据、唯一路径、全局选中状态映射及状态变更回调,根据节点类型做不同渲染:
- 基本类型(字符串/数字/布尔):渲染键值对+单个复选框
- 对象类型:渲染父复选框+展开所有子键对应的
JsonNode - 数组类型:渲染父复选框+展开所有数组元素对应的
JsonNode(用索引作为子路径)
3. 复选框联动逻辑
实现两个核心辅助函数:
getAllChildPaths:递归获取当前节点下所有子节点的路径,用于父节点选中时批量更新子节点状态updateParentStatus:子节点状态变化时,向上回溯更新所有父节点的选中/半选状态
完整代码示例
import { useState, useEffect, useRef } from 'react'; // 判断节点类型 const getNodeType = (value) => { if (Array.isArray(value)) return 'array'; if (value !== null && typeof value === 'object') return 'object'; return 'primitive'; }; // 递归获取当前节点下所有子节点的路径 const getAllChildPaths = (path, value) => { const paths = [path]; const type = getNodeType(value); if (type === 'object') { Object.keys(value).forEach(key => { paths.push(...getAllChildPaths(`${path}.${key}`, value[key])); }); } else if (type === 'array') { value.forEach((_, index) => { paths.push(...getAllChildPaths(`${path}[${index}]`, value[index])); }); } return paths; }; // 根据路径获取对应节点数据 const getNodeByPath = (path, data) => { return path.split(/\.(?=[^\[]+)|\[(?=\d+\])/).reduce((acc, segment) => { if (segment.includes(']')) { const index = parseInt(segment.slice(0, -1), 10); return acc[index]; } return acc[segment]; }, data); }; // 更新父节点的选中/半选状态 const updateParentStatus = (path, checkedMap, setCheckedMap, rawData) => { const pathSegments = path.split(/\.(?=[^\[]+)|\[(?=\d+\])/); pathSegments.pop(); if (pathSegments.length === 0) return; const parentPath = pathSegments.join('.').replace(/\.(?=\[)/, ''); const parentNode = getNodeByPath(parentPath, rawData); const childPaths = getAllChildPaths(parentPath, parentNode).filter(p => p !== parentPath); const checkedCount = childPaths.filter(p => checkedMap[p]).length; const totalCount = childPaths.length; let newCheckedMap = { ...checkedMap }; if (checkedCount === totalCount) { newCheckedMap[parentPath] = true; } else if (checkedCount === 0) { newCheckedMap[parentPath] = false; } else { delete newCheckedMap[parentPath]; } setCheckedMap(newCheckedMap); updateParentStatus(parentPath, newCheckedMap, setCheckedMap, rawData); }; const JsonNode = ({ keyName, value, path, checkedMap, onCheckChange, rawData }) => { const checkboxRef = useRef(null); const type = getNodeType(value); // 处理半选状态 useEffect(() => { if (type !== 'primitive' && !(path in checkedMap)) { checkboxRef.current.indeterminate = true; } else { checkboxRef.current.indeterminate = false; } }, [checkedMap, path, type]); // 父节点复选框变更逻辑 const handleParentCheck = (e) => { const isChecked = e.target.checked; const allPaths = getAllChildPaths(path, value); const newCheckedMap = { ...checkedMap }; allPaths.forEach(p => { newCheckedMap[p] = isChecked; }); onCheckChange(newCheckedMap); }; // 基础类型节点复选框变更逻辑 const handlePrimitiveCheck = (e) => { const isChecked = e.target.checked; const newCheckedMap = { ...checkedMap, [path]: isChecked }; onCheckChange(newCheckedMap); updateParentStatus(path, newCheckedMap, onCheckChange, rawData); }; if (type === 'primitive') { return ( <div style={{ marginLeft: '20px' }}> <input type="checkbox" checked={checkedMap[path] || false} onChange={handlePrimitiveCheck} /> <span>{keyName}: {JSON.stringify(value)}</span> </div> ); } return ( <div> <div> <input ref={checkboxRef} type="checkbox" checked={checkedMap[path] || false} onChange={handleParentCheck} /> <strong>{keyName || 'Root'}</strong> </div> {type === 'object' && Object.keys(value).map(key => ( <JsonNode key={key} keyName={key} value={value[key]} path={path ? `${path}.${key}` : key} checkedMap={checkedMap} onCheckChange={onCheckChange} rawData={rawData} /> ))} {type === 'array' && value.map((item, index) => ( <JsonNode key={index} keyName={`[${index}]`} value={item} path={path ? `${path}[${index}]` : `[${index}]`} checkedMap={checkedMap} onCheckChange={onCheckChange} rawData={rawData} /> ))} </div> ); }; const JsonViewerWithCheckbox = ({ data }) => { const [checkedMap, setCheckedMap] = useState({}); return ( <div style={{ padding: '20px' }}> <JsonNode value={data} path="" checkedMap={checkedMap} onCheckChange={setCheckedMap} rawData={data} /> </div> ); }; // 使用示例 const App = () => { const data = { "squadName": "Super hero squad", "homeTown": "Metro City", "formed": 2016, "secretBase": "Super tower", "active": true, "members": [ { "name": "Molecule Man", "age": 29, "secretIdentity": "Dan Jukes", "powers": [ "Radiation resistance", "Turning tiny", "Radiation blast" ] }, { "name": "Madame Uppercut", "age": 39, "secretIdentity": "Jane Wilson", "powers": [ "Million tonne punch", "Damage resistance", "Superhuman reflexes" ] }, { "name": "Eternal Flame", "age": 1000000, "secretIdentity": "Unknown", "powers": [ "Immortality", "Heat Immunity", "Inferno", "Teleportation", "Interdimensional travel" ] } ] }; return <JsonViewerWithCheckbox data={data} />; }; export default App;
关键注意事项
- 路径唯一性:通过拼接键名和数组索引生成唯一路径,避免节点状态冲突
- 半选状态处理:利用
useRef手动设置复选框的indeterminate属性(React无法直接通过状态控制该属性) - 性能优化:若JSON数据量极大,可使用
useMemo缓存递归生成的节点,减少不必要的重渲染 - 空值兼容:代码已处理
null和undefined场景,可根据实际需求调整节点类型判断逻辑
内容的提问来源于stack exchange,提问作者sourabh
相关产品推荐
相关产品推荐

