Search code examples
javascriptreactjsreact-hooksreact-state-managementnested-object

Not able to update Nested object array in treeview Reactjs


Thanks in advance !!!

Question : I am not able to update nested object of array in treeview Reactjs. Please refer below codesand box link for code

https://codesandbox.io/s/cocky-leakey-ptjt50?file=/src/Family.js

Data Object :

const data = [
  {
    key: "mammal",
    label: "Mammal",
    isFolder: true,
    checked: false,
    nodes: [
      {
        key: "canidae",
        label: "Canidae",
        checked: false,
        nodes: [
          {
            key: "dog",
            label: "Dog",
            checked: false,
            nodes: [],
            url: "https://www.google.com/search?q=dog"
          },
          {
            key: "fox",
            label: "Fox",
            checked: false,

            nodes: [],
            url: "https://www.google.com/search?q=fox"
          },
          {
            key: "wolf",
            label: "Wolf",
            checked: false,

            nodes: [],
            url: "https://www.google.com/search?q=wolf"
          }
        ],
        url: "https://www.google.com/search?q=canidae"
      }
    ],
    url: "https://www.google.com/search?q=mammal"
  },
  {
    key: "reptile",
    label: "Reptile",
    isFolder: true,
    checked: false,

    nodes: [
      {
        key: "squamata",
        label: "Squamata",
        checked: false,

        nodes: [
          {
            key: "lizard",
            label: "Lizard",
            checked: false,
            nodes: [],
            url: "https://www.google.com/search?q=lizard"
          },
          {
            key: "snake",
            label: "Snake",
            checked: false,
            nodes: [],
            url: "https://www.google.com/search?q=snake"
          },
          {
            key: "gekko",
            label: "Gekko",
            checked: false,
            nodes: [],
            url: "https://www.google.com/search?q=gekko"
          }
        ],
        url: "https://www.google.com/search?q=squamata"
      }
    ],
    url: "https://www.google.com/search?q=reptile"
  }
];

Requirement is that once i have clicked on parent checkbox then child checkbox automatically get selected .Please find below image for reference enter image description here

So if I clicked on the parent i.e "Mammal" then automatically it should select child element like Canidae,Dog, Fox and Wolf.but this code is updating the single value only not nested object.

On click of checkbox I am iterating the nested object using recursion see below code.

const updateObject = (labelkey, objectData, value) => {
    return objectData.map((object) => {
      if (object.label.trim() === labelkey.trim()) {
        object.checked = value;
      } else {
        if (
          object.nodes &&
          object.nodes.length > 0 &&
          typeof object.nodes === "object" &&
          object.nodes !== null
        ) {
          object.nodes = updateObject(labelkey, object.nodes, value);
        }
      }

      return object;
    });
  };
  
  const checkBoxChicked = (label) => {
    var newData = JSON.parse(JSON.stringify(treeData));
  // Calling updateobject method
    const result = updateObject(label.label, newData, !label.checked);
    var resultData = JSON.parse(JSON.stringify(result));

    setTreeData(resultData);
  };
return (
    <div style={{ paddingLeft: "20px" }}>
      {treeData?.map((parent) => {
        return (
          <div key={parent.label}>
            {parent.isFolder && (
              <div className="parent">
                <input
                  type="checkbox"
                  id={parent.label}
                  name={parent.label}
                  value={parent.label}
                  checked={parent?.checked ? parent.checked : false}
                  onClick={() => checkBoxChicked(parent)}
                />
                <label for={parent.label}> {parent.label}</label>
              </div>
            )}
            {/* rendering files */}
            {!parent.isFolder && (
              <div style={{ paddingLeft: "32px" }}>
                <input
                  type="checkbox"
                  id={parent.label}
                  name={parent.label}
                  value="Bike"
                  checked={parent?.checked ? parent.checked : false}
                  onClick={() => checkBoxChicked(parent)}
                />
                <label for={parent.label}>{parent.label}</label>
              </div>
            )}
            {/* Base Condition and Rendering recursive component from inside itself */}
            <div>
              {parent.nodes && parent.nodes.length > 0 && (
                <Family familyTree={parent.nodes} />
              )}
            </div>
          </div>
        );
      })}
    </div>
  );


Solution

  • You can use recursion in-place, rather than "returning" a modified object. Once you make the copy, you are free to mutate the copy. Furthermore, updateObject should not be a method of your <Family> component. It takes all the parameters it need to perform the update.

    Note: In React, the prop for a label is htmlFor, since for (along with class) is a reserved word in JavaScript.

    const updateNodes = (nodes, key, value, found) => {
      if (nodes == null) {
        return;
      }
      nodes.forEach((node) => {
        if (found || node.label.trim() === key.trim()) {
          node.checked = value;
          updateNodes(node.nodes, key, value, true);
        } else {
          if (node.nodes && node.nodes.length) {
            updateNodes(node.nodes, key, value, found);
          }
        }
      });
    };
    

    Instead of calling JSON.(parse|stringify), you could recursively map your nodes:

    function cloneForest(forest) {
      if (forest == null) return null;
      return forest.map((tree) => {
        return {
          key: tree.key,
          label: tree.label,
          isFolder: tree.isFolder,
          checked: tree.checked,
          nodes: cloneForest(tree.nodes)
        };
      });
    }
    

    Also, when calling "setState", you should not reference the "state". You should use the function version:

    const checkBoxClicked = useCallback(({ checked, label }) => {
      setTreeData((currentTreeData) => {
        const newTreeData = cloneForest(currentTreeData);
        updateNodes(newTreeData, label, !checked, false);
        return newTreeData;
      });
    }, []);
    

    UPDATE

    I noticed that since the <Family> component is recursive itself, each recursive child has its own concept of the treeData state. I would think about how you render the children. The state should probably be passed down to the <Family> component instead.

    Steps to reproduce problem:

    1. Click "Squamata" (file)
    2. Click "Mammal" (folder)
    3. Notice that ALL the Reptiles are now unchecked

    I would probably leave <Family> as is in terms of state keeping, but I would render <Folder> and <File> components instead. They would take their current version of the tree, along with a click handler to pass back up to their parent component i.e. <Family> to make the state updates. This way the correct state is always passed down. It's how React works.

    Full demo

    Putting it all together (without the changes in the update above):

    const { useCallback, useEffect, useState } = React;
    
    /* Family.jsx */
    const updateNodes = (nodes, key, checked, parentFound) => {
      if (nodes == null) {
        return;
      }
      nodes.forEach((node) => {
        const isFoundOrChild = parentFound || node.key === key;
        console.log(`Node ${node.key} is currently ${node.checked} and will now be ${isFoundOrChild}`);
        if (isFoundOrChild) {
          node.checked = checked;
        }
        updateNodes(node.nodes, key, checked, isFoundOrChild);
      });
    };
    
    function cloneForest(forest) {
      if (forest == null) return null;
      return forest.map((tree) => {
        return {
          key: tree.key,
          label: tree.label,
          isFolder: tree.isFolder,
          checked: tree.checked,
          nodes: cloneForest(tree.nodes)
        };
      });
    }
    
    function Family({ familyTree }) {
      const [showNested, setShowNested] = useState(false);
      const [treeData, setTreeData] = useState([]);
    
      useEffect(() => {
        setTreeData(familyTree);
      }, [familyTree]);
    
      const checkBoxClicked = useCallback(({ key }, checked) => {
        setTreeData((currentTreeData) => {
          const newTreeData = cloneForest(currentTreeData);
          console.log('UPDATE');
          console.log(newTreeData);
          updateNodes(newTreeData, key, checked, false);
          return newTreeData;
        });
      }, []);
    
      return (
        <div style={{ paddingLeft: "20px" }}>
          {treeData.map((node) => {
            return (
              <div key={node.label}>
                {/* Rendering folders */}
                {node.isFolder && (
                  <div className="parent">
                    <input
                      type="checkbox"
                      id={node.label}
                      name={node.label}
                      value={node.label}
                      checked={node.checked ? node.checked : false}
                      onChange={({ target: { checked } }) => checkBoxClicked(node, checked)}
                    />
                    <label htmlFor={node.label}> {node.label}</label>
                  </div>
                )}
                {/* Rendering files */}
                {!node.isFolder && (
                  <div style={{ paddingLeft: "32px" }}>
                    <input
                      type="checkbox"
                      id={node.label}
                      name={node.label}
                      value="Bike"
                      checked={node.checked ? node.checked : false}
                      onChange={({ target: { checked } }) => checkBoxClicked(node, checked)}
                    />
                    <label htmlFor={node.label}>{node.label}</label>
                  </div>
                )}
                {/* Base Condition and Rendering recursive component from inside itself */}
                <div>
                  {node.nodes && node.nodes.length > 0 && (
                    <Family familyTree={node.nodes} />
                  )}
                </div>
              </div>
            );
          })}
        </div>
      );
    }
    
    /* App.jsx */
    const data = [
      {
        key: "mammal",
        label: "Mammal",
        isFolder: true,
        checked: false,
        nodes: [
          {
            key: "canidae",
            label: "Canidae",
            checked: false,
            nodes: [
              {
                key: "dog",
                label: "Dog",
                checked: false,
                nodes: [],
                url: "https://www.google.com/search?q=dog"
              },
              {
                key: "fox",
                label: "Fox",
                checked: false,
    
                nodes: [],
                url: "https://www.google.com/search?q=fox"
              },
              {
                key: "wolf",
                label: "Wolf",
                checked: false,
    
                nodes: [],
                url: "https://www.google.com/search?q=wolf"
              }
            ],
            url: "https://www.google.com/search?q=canidae"
          }
        ],
        url: "https://www.google.com/search?q=mammal"
      },
      {
        key: "reptile",
        label: "Reptile",
        isFolder: true,
        checked: false,
    
        nodes: [
          {
            key: "squamata",
            label: "Squamata",
            checked: false,
    
            nodes: [
              {
                key: "lizard",
                label: "Lizard",
                checked: false,
                nodes: [],
                url: "https://www.google.com/search?q=lizard"
              },
              {
                key: "snake",
                label: "Snake",
                checked: false,
                nodes: [],
                url: "https://www.google.com/search?q=snake"
              },
              {
                key: "gekko",
                label: "Gekko",
                checked: false,
                nodes: [],
                url: "https://www.google.com/search?q=gekko"
              }
            ],
            url: "https://www.google.com/search?q=squamata"
          }
        ],
        url: "https://www.google.com/search?q=reptile"
      }
    ];
    
    function App() {
      return (
        <div className="App">
          <Family familyTree={data} />
        </div>
      );
    }
    
    ReactDOM
      .createRoot(document.getElementById('root'))
      .render(<App />);
    .App {
      text-align: center;
      background-color: #23395d;
      background-color: #0e1432;
    
      width: 50%;
      padding: 64px;
      color: #fff;
    }
    
    .App-logo {
      height: 40vmin;
      pointer-events: none;
    }
    
    @media (prefers-reduced-motion: no-preference) {
      .App-logo {
        animation: App-logo-spin infinite 20s linear;
      }
    }
    
    .App-header {
      background-color: #282c34;
      min-height: 100vh;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      font-size: calc(10px + 2vmin);
      color: white;
    }
    
    .App-link {
      color: #61dafb;
    }
    
    @keyframes App-logo-spin {
      from {
        transform: rotate(0deg);
      }
      to {
        transform: rotate(360deg);
      }
    }
    
    .parent {
      background-color: #4ca3df;
      padding: 8px;
      color: #fff;
      width: 100%;
    }
    input[type="checkbox"] {
      outline: 1px solid #fff;
      border-radius: 12px;
      accent-color: #1ea046;
    }
    <div id="root"></div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>