Home > Back-end >  Handle rendering array of editable components
Handle rendering array of editable components

Time:11-09

I have a contentEditable component:

EditableComponent.js

const EditableComponent = (props) => {
    return <p contentEditable>{props.children}</p>;
};

In the ParentComponent I can add EditableComponents to an array (someArr) with useState, and then I pass someArr and setSomeArray via props to another component (AllEditable) to render it:

ParentComponent.js

import EditableComponent from "./components";
import AllEditable from "./components";

const ParentComponent = () => {
   
    const [someArr, setSomeArr] = useState([]);

    const handleNewEditable = () => {
        setContentArr((prevContentArr) => {
            return [...prevContentArr, <EditableComponent />];
        });
    };

    return (
         <div className="wrapper">
             <AllEditable someArr={someArr} setSomeArr={setSomeArr} />
             <div>
                 <button onClick={handleNewEditable}>Add</button>
             </div>
         </div>
    );
};

Each rendered component (EditableComponent) have a span with the content 'X' that should delete the target onClick:

AllEditable.js

const AllEditable= (props) => {
    const deleteContentHandler = (index) => {
        props.setSomeArr((prevState) => {
            return prevState.filter((_, idx) => idx !== index);
        });
    };

    return (
        <div>
            {props.someArr.map((content, idx) => {
                return (
                    <div key={`content-${idx}`}>
                        <span onClick={() => {deleteContentHandler(idx);}}>
                            X
                        </span>
                        <div>{content}</div>
                    </div>
                );
            })}
        </div>
    );
};

The problem:

It doesn't matter which component I'm trying to delete, it removes the last component (even in the Components section in the developer tools) and I'm pretty sure that the logic behind deleting (filter) works well.

I tried deleting the contentEditable attribute, and added some unique random text in each component and then it worked as expected!.

Things I tried

  • Creating a new array without the removed target
  • Nesting the components in someArr in objects with key: index, example: {idx: 0, content: <EditableComponent />}
  • Added a function - onDoubleClick for each EditableComponent to toggle the attribute contentEditable, true or false.
  • Replaced the element in EditableComponent to <textarea></textarea> instead of <p contentEditable></p>

CodePudding user response:

Your problem is all your EditableComponent components have the same key (because you haven't declared key on them). React cannot identify which EditableComponent you want to delete. You can check this document.

I'd suggest you add a key attribute like below

<EditableComponent key={prevContentArr.length - 1}/>

For safer index reservation, you should use map instead filter

const AllEditable= (props) => {
    const deleteContentHandler = (index) => {
        props.setSomeArr((prevState) => {
            return prevState.map((x, idx) => idx !== index ? x : null);
        });
    };

    return (
        <div>
            {props.someArr.map((content, idx) => {
                return content ? (
                  <div key={`content-${idx}`}>
                      <span onClick={() => {deleteContentHandler(idx);}}>
                          X
                      </span>
                      <div>{content}</div>
                  </div>
              ) : null;
            })}
        </div>
    );
};

const { useState } = React

const EditableComponent = (props) => {
    return <p contentEditable>{props.children}</p>;
};

const AllEditable= (props) => {
const deleteContentHandler = (index) => {
    props.setSomeArr((prevState) => {
        return prevState.map((x, idx) => idx !== index ? x : null);
    });
};

return (
    <div>
        {props.someArr.map((content, idx) => {
            return content ? (
              <div key={`content-${idx}`}>
                  <span onClick={() => {deleteContentHandler(idx);}}>
                      X
                  </span>
                  <div>{content}</div>
              </div>
          ) : null;
        })}
    </div>
);
};

const ParentComponent = () => {
   
    const [someArr, setSomeArr] = useState([]);

    const handleNewEditable = () => {
        setSomeArr((prevContentArr) => {
            return [...prevContentArr, <EditableComponent key={prevContentArr.length - 1}/>];
        });
    };

    return (
         <div className="wrapper">
             <AllEditable someArr={someArr} setSomeArr={setSomeArr} />
             <div>
                 <button onClick={handleNewEditable}>Add</button>
             </div>
         </div>
    );
};

ReactDOM.render(
  <ParentComponent/>,
  document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Side note that keys with index values are not the best because your array keeps changing that would make key changes as well. You can use some unique key generator to handle that situation instead.

CodePudding user response:

You shluld change the way you set keys, setting the element key to: "content-index" is confusing for react, because once you remove an item all the indexes will change, and therefore your keys.

So you need to find a way to have static keys for your elements. That way everytime they render, the key will be the same.

CodePudding user response:

The logic is working correctly and in fact it is deleting the correct elements, however since you are using the index to identify elements, you will never see this since it will always appear that only the last one is removed (when the array updates, the indices will update too).

So if you had 0,1,2 and removed 1 now you have an array with indices 0,1

To test this you can place a reference to the index when rendering the content editable div, based on testing you can see that the correct element is in fact being removed:

import "./styles.css";
import React, { useState } from "react";

const EditableComponent = (props) => {
  return (
    <p contentEditable>
      {props.children}
      {props.idtfy}
    </p>
  );
};

const AllEditable = (props) => {
  const deleteContentHandler = (index) => {
    props.setSomeArr((prevState) => {
      return prevState.filter((_, idx) => idx !== index);
    });
  };

  return (
    <div>
      {props.someArr.map((content, idx) => {
        return (
          <div key={`content-${idx}`}>
            <span>{idx}</span>
            <span
              onClick={() => {
                deleteContentHandler(idx);
              }}
            >
              X
            </span>
            <div>{content}</div>
          </div>
        );
      })}
    </div>
  );
};

const ParentComponent = () => {
  const [someArr, setSomeArr] = useState([]);

  const handleNewEditable = () => {
    setSomeArr((prevContentArr) => {
      return [
        ...prevContentArr,
        <EditableComponent idtfy={prevContentArr.length   1} />
      ];
    });
  };

  return (
    <div className="wrapper">
      <AllEditable someArr={someArr} setSomeArr={setSomeArr} />
      <div>
        <button onClick={handleNewEditable}>Add</button>
      </div>
    </div>
  );
};

  • Related