1

Currently I've got a react component that looks like this:

const GeraCard = (cards, cart = false) => {
    return cards.map((v, i) => {
      return (
        <div key={i} className={styles.card}>
          <div onClick={() => urlRender(v.url)} className={styles.cardContent}>
            <div>
              <span className={styles.cardTitulo}>{v.Nome}</span>
            </div>
            <div>
              <span className={styles.cardData}>{v.Data}</span>
              <span className={styles.cardAtivos}>{v.Ativos} ativo(s)</span>
            </div>
            {cart ? <div>R$ {FormatCapital(v.Capital)}</div> : null}
          </div>
          <span className={styles.trash}>
            <FontAwesomeIcon
              icon={faTrash}
              color={"#3c3c3c77"}
              onClick={(e) => {
                e.persist()
                TrashHandler(v.Nome, e)
              }}
            />
          </span>
        </div>
      );
    });
  };

Based on the cards array, it renders something like this:

Rendered Component

Whenever I click the trash button, I make a request to my backend, edit the list on my database and rerender the component based on the now updated "cards". The problem is that this takes sometime to happen, so i wanted a way to remove it from the dom instantly while my backend does it's job.

somehting like

{show ? renderCompoennt : null}

I've tried using vanilla javascript to grab the parent from the trash can, which would be the card i want to remove, but the results are unpredictable and it's quite slow as well.

My latest try was this:

const GeraCard = (cards, cart = false) => {
    return cards.map((v, i) => {
      const [show, setShow] = useState(true);
      return (
        <div key={i}>
          {show ?
            <div className={styles.card}>
              <div onClick={() => urlRender(v.url)} className={styles.cardContent}>
                <div>
                  <span className={styles.cardTitulo}>{v.Nome}</span>
                </div>
                <div>
                  <span className={styles.cardData}>{v.Data}</span>
                  <span className={styles.cardAtivos}>{v.Ativos} ativo(s)</span>
                </div>
                {cart ? <div>R$ {FormatCapital(v.Capital)}</div> : null}
              </div>
              <span className={styles.trash}>
                <FontAwesomeIcon
                  icon={faTrash}
                  color={"#3c3c3c77"}
                  onClick={(e) => {
                    setShow(false);
                    e.persist()
                    TrashHandler(v.Nome, e)
                  }}
                />
              </span>
            </div> :
            null
          }
        </div>
      );
    });
  };

but react won't let me do this. Even tho its fast, everytime one item gets deleted, react complains that "less hooks were rendered" and crashes the app.

1
  • 1
    You can't use useState inside a map, I'd suggest extracting the content in map as a component Commented May 11, 2021 at 19:23

2 Answers 2

2

You are attempting to do some Optimistic UI, in which you assume that your action will succeed, and reflect the expected/assumed state instantly, before the request to the backend completes. This would be in lieu of showing some progress/busy indicator, like a spinner, until the action completes with the server.

The first problem and immediate problem in your code-- it violates the rules of hooks, which state that hooks may only be used at the top-level (never inside loops, conditionals, etc).

The second problem is that you are leveraging vanilla JS to manipulate the DOM directly; this generally an antipattern in MV* frameworks, and very much so here. Instead, I would suggest doing managing it in your data model; something like this:

  1. Rewrite your .map handler to return null if the card has a deleted property.
  2. When the user clicks the trash button, do two things:
    1. Make the request to the backend to delete it
    2. Use a setState to add a deleted: true property to the clicked card

Now you will get a rerender that will omit the deleted card, and also make the request to the backend, all inside the React data model. Make sure that you handle complexity for:

  1. How to handle the response
  2. How to handle an error if the deletion fails at the backend
  3. How to manage if a user quickly clicks many cards for deletion before any of the requests can complete.
Sign up to request clarification or add additional context in comments.

Comments

1

The problem is that in the first render you have {cards.length} calls to hook "useState" within GeraCard, but after deletion of one card, you will have {cards.length-1} calls to hook "useState". As the React docs state:

Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function. By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls.

You should extract the content of map callback into separate a component.

const GeraCards = (cards, cart = false) => {
    return cards.map((v, i) =>
        <GeraCard card={v} index={i} cart={cart} />
    );
};

const GeraCard = ({ card, index, cart }) => {
    const [show, setShow] = useState(true);
    const v = card;
    return (
        <div key={index}>
            {show ?
                <div className={styles.card}>
                    <div onClick={() => urlRender(v.url)} className={styles.cardContent}>
                        <div>
                            <span className={styles.cardTitulo}>{v.Nome}</span>
                        </div>
                        <div>
                            <span className={styles.cardData}>{v.Data}</span>
                            <span className={styles.cardAtivos}>{v.Ativos} ativo(s)</span>
                        </div>
                        {cart ? <div>R$ {FormatCapital(v.Capital)}</div> : null}
                    </div>
                    <span className={styles.trash}>
                        <FontAwesomeIcon
                            icon={faTrash}
                            color={"#3c3c3c77"}
                            onClick={(e) => {
                                setShow(false);
                                e.persist()
                                TrashHandler(v.Nome, e)
                            }}
                        />
                    </span>
                </div> :
                null
            }
        </div>
    );
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.