DEV Community

ZeeshanAli-0704
ZeeshanAli-0704

Posted on

Nested Checkox - React Interview

// App.js (React, CodeSandbox-ready)
// Nested checkboxes with parent/child sync and indeterminate states.

import React from "react";

// ----- Sample tree data -----
const tree = {
  id: "a",
  label: "A",
  children: [
    { id: "a1", label: "A1" },
    {
      id: "a2",
      label: "A2",
      children: [
        { id: "a21", label: "A2.1" },
        { id: "a22", label: "A2.2" },
      ],
    },
  ],
};

// ----- Utilities -----
function visit(node, fn) {
  fn(node);
  node.children?.forEach((c) => visit(c, fn));
}

function initState(root) {
  const s = {};
  visit(root, (n) => {
    s[n.id] = { checked: false, indeterminate: false };
  });
  return s;
}

function setSubtree(node, checked, state) {
  state[node.id] = { checked, indeterminate: false };
  node.children?.forEach((child) => setSubtree(child, checked, state));
}

function findParent(root, targetId) {
  let parent = undefined;
  function dfs(cur) {
    for (const child of cur.children ?? []) {
      if (child.id === targetId) {
        parent = cur;
        return true;
      }
      if (dfs(child)) return true;
    }
    return false;
  }
  dfs(root);
  return parent;
}

function computeStateFromChildren(children, state) {
  const statuses = children.map((c) => state[c.id]);
  const allChecked = statuses.every((s) => s.checked && !s.indeterminate);
  const noneChecked = statuses.every((s) => !s.checked && !s.indeterminate);
  return {
    checked: allChecked,
    indeterminate: !allChecked && !noneChecked,
  };
}

// ----- Components -----
function TreeNode({ node, root, state, setState, level = 0 }) {
  const checkboxRef = React.useRef(null);
  const s = state[node.id];

  React.useEffect(() => {
    if (checkboxRef.current)
      checkboxRef.current.indeterminate = s.indeterminate;
  }, [s.indeterminate]);

  const onToggle = (checked) => {
    setState((prev) => {
      const next = { ...prev };

      // Update this subtree
      setSubtree(node, checked, next);

      // Recompute ancestors up to root
      let cur = node;
      while (true) {
        const parent = findParent(root, cur.id);
        if (!parent) break;
        next[parent.id] = computeStateFromChildren(parent.children ?? [], next);
        cur = parent;
      }

      return next;
    });
  };

  return (
    <div style={{ paddingLeft: level ? 16 : 0 }}>
      <label style={{ display: "inline-flex", gap: 8, alignItems: "center" }}>
        <input
          ref={checkboxRef}
          type="checkbox"
          checked={s.checked}
          aria-checked={
            s.indeterminate ? "mixed" : s.checked ? "true" : "false"
          }
          onChange={(e) => onToggle(e.target.checked)}
        />
        {node.label}
      </label>

      {node.children?.length ? (
        <div role="group" style={{ marginTop: 4 }}>
          {node.children.map((child) => (
            <TreeNode
              key={child.id}
              node={child}
              root={root}
              state={state}
              setState={setState}
              level={level + 1}
            />
          ))}
        </div>
      ) : null}
    </div>
  );
}

export default function App() {
  const [state, setState] = React.useState(() => initState(tree));

  return (
    <div style={{ fontFamily: "sans-serif", padding: 16 }}>
      <h3>Nested Checkboxes</h3>
      <TreeNode node={tree} root={tree} state={state} setState={setState} />
      <pre style={{ background: "#f6f8fa", padding: 8, marginTop: 16 }}>
        {JSON.stringify(state, null, 2)}
      </pre>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

Top comments (0)