You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 21:09:58