0

I have a pretty simple component that displays a list of contacts via an API call.

allContacts.jsx

import React, { useEffect, useState } from 'react';

import PhoneLayout from './layouts/phoneLayout';
import AddContact from './addContact';
import ShowContact from './showContact';

export default function AllContacts() {
  let [contacts, setContacts] = useState([]);
  let [showContact, setShowContact] = useState(false);

  useEffect(() => {
    fetch(`http://localhost:8000/all-contacts`)
      .then((response) => response.json())
      .then((data) => setContacts(data))
      .catch((err) => console.log(err));
  }, []);

  return (
    <PhoneLayout>
      <div className="relative">
        <AddContact />
        <div className="flex flex-col">
          {contacts.map((contact) => (
            <button onClick={() => setShowContact(true)}>
              {showContact ? (
                <ShowContact contactId={contact.id} open={true} />
              ) : (
                <div className="border-b border-gray-200 p-4 py-2 text-left capitalize text-gray-700 hover:bg-gray-200">
                  <span className="font-bold">{contact.first_name}</span>{' '}
                  {contact.last_name}
                </div>
              )}
            </button>
          ))}
        </div>
      </div>
    </PhoneLayout>
  );
}

I am displaying another component and passing some props with it.

<ShowContact contactId={contact.id} open={true} />

The trouble is that the API call in ShowContact seems to be calling it for every entry I have in my API db. On click I get the following in the console:

showContact.jsx:17 {contactId: 4, open: true}
showContact.jsx:17 {contactId: 5, open: true}
showContact.jsx:17 {contactId: 6, open: true}
showContact.jsx:17 {contactId: 7, open: true}
showContact.jsx:17 {contactId: 8, open: true}
showContact.jsx:17 {contactId: 9, open: true}
showContact.jsx:17 {contactId: 10, open: true}
showContact.jsx:17 {contactId: 11, open: true}
showContact.jsx:17 {contactId: 12, open: true}
showContact.jsx:17 {contactId: 13, open: true}
showContact.jsx:17 {contactId: 14, open: true}
showContact.jsx:17 {contactId: 15, open: true}
showContact.jsx:17 {contactId: 16, open: true}
showContact.jsx:17 {contactId: 17, open: true}

This is the ShowContact components

// showContact.jsx

export default function ShowContact(props) {
  let [isOpen, setIsOpen] = useState(false);
  let [contact, setContact] = useState({});

  // delete contact from api then reloads the page
  function deleteContact() {
    fetch(`http://localhost:8000/delete-contact/${props.contactId}`, {
      method: 'DELETE',
    }).catch((err) => console.log(err));

    setIsOpen(false);
    window.location.reload(true);
  }

  console.log(props);

  // get contact from api by contact id
  useEffect(() => {
    async function fetchContact() {
      await fetch(`http://localhost:8000/get-contact/${props.contactId}`)
        .then((response) => response.json())
        .then((data) => {
          setContact(data);
          console.log(data);
        })
        .catch((err) => console.log(err));
    }

    if (!props.open) {
      fetchContact();
    }
  }, []);

  return ( ...

I have inverted the conditional within the uesEffect block so I can show you all whats happening. I have used this useEffect in this way in the pass, however the component was slightly different. Like so:

export default function MyModal(props) {
  let [isOpen, setIsOpen] = useState(false);
  let completeButtonRef = useRef(null);

  function closeModal() {
    setIsOpen(false);
  }

  function openModal() {
    setIsOpen(true);
  }


  // get contact from api by contact id
  useEffect(() => {
    async function fetchContact() {
      await fetch(`http://localhost:8000/get-contact/${props.contactId}`)
        .then((response) => response.json())
        .then((data) => {
          setContact(data);
          console.log(data);
        })
        .catch((err) => console.log(err));
    }

    if (isOpen) {
      fetchContact();
    }
  }, [isOpen]);

  return (
    <>
      <button
        type="button"
        onClick={openModal}
        ref={completeButtonRef}
        className="rounded-md bg-black bg-opacity-20 px-4 py-2 text-sm font-medium text-white hover:bg-opacity-30 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"
      >
        Edit
      </button>

As you can see here the above component had the button which would set isOpen where as the component I am trying to use does not have this, therefore it seems to be calling the API for every entry. This is why I thought about passing a boolean with a props, but this just seems to fire the useEffect again again.

I have looked through:

mrpbennett
  • 1,527
  • 15
  • 40

1 Answers1

3

The problem is you only have one flag (showContact), but seem to expect to have a flag for each contact. But it's controlling whether all of them are visible:

{contacts.map((contact) => (
  <button onClick={() => setShowContact(true)}>
    {showContact ? ( // <===================================== one flag for all elements
      <ShowContact contactId={contact.id} open={true} />
    ) : (
      <div className="border-b border-gray-200 p-4 py-2 text-left capitalize text-gray-700 hover:bg-gray-200">
        <span className="font-bold">{contact.first_name}</span>{' '}
        {contact.last_name}
      </div>
    )}
  </button>
))}

Instead, if you want only one contact to be visible at a time, use the contact.id, perhaps starting with null for "none":

const [showContact, setShowContact] = useState(null);

Then set the contact id on click, and only show the contact that has a matching ID:

{contacts.map((contact) => (
  <button onClick={() => setShowContact(show => show === contact.id ? null : contact.id)}>
    {showContact === contact.id ? (
      <ShowContact contactId={contact.id} open={true} />
    ) : (
      <div className="border-b border-gray-200 p-4 py-2 text-left capitalize text-gray-700 hover:bg-gray-200">
        <span className="font-bold">{contact.first_name}</span>{' '}
        {contact.last_name}
      </div>
    )}
  </button>
))}

Since the button is visible for the contact you're showing, I had it toggle by setting it null when you click the button on the contact that's already showing.

Here's a simplified example:

const { useState, useEffect } = React;

const getAllContacts = (signal) => {
    return new Promise((resolve, reject) => {
        // Fake ajax
        setTimeout(() => {
            if (signal.aborted) {
                reject(new Error("Operation cancelled"));
            } else {
                resolve([
                    {id: 1, name: "Joe Bloggs"},
                    {id: 2, name: "Valeria Hernandez"},
                    {id: 3, name: "Indira Patel"},
                    {id: 4, name: "Mohammed Abu-Yasein"},
                ]);
            }
        }, 800);
    });
};

// Fake details
const contactDetails = new Map([
    [1, Math.floor(Math.random() * 100)],
    [2, Math.floor(Math.random() * 100)],
    [3, Math.floor(Math.random() * 100)],
    [4, Math.floor(Math.random() * 100)],
]);

const ShowContact = ({contact: {id, name}}) => {
    const [details, setDetails] = useState(null);

    useEffect(() => {
        // This is very simplistic, see the more proper `useEffect` example above
        console.log(`Loading details for contact #{id}...`);
        setTimeout(() => {
            console.log(`Loaded details for contact #{id}`);
            setDetails(`high score: ${contactDetails.get(id)}`);
        }, 250);
    }, [id]);
    return <div>{id}: {name} - {details ? details : <em>loading details...</em>}</div>;
};

const Example = () => {
    const [showContact, setShowContact] = useState(null);
    const [contacts, setContacts] = useState([]);

    useEffect(() => {
        const controller = new AbortController();
        const { signal } = controller;
        getAllContacts(signal)
        .then(contacts => {
            if (signal.aborted) {
                return;
            }
            setContacts(contacts);
        })
        .catch(error => {
            if (!signal.aborted) {
                // ...handle/report error...
            }
        });
        return () => {
            controller.abort(); // Cancel the operation, if pending
        };
    }, []);

    return <div>
        {contacts.map(contact => <div key={contact.id} className="contact">
            <input type="button" onClick={() => setShowContact(id => id === contact.id ? null : contact.id)} value="Show/Hide" />
            {
                showContact === contact.id
                    ? <ShowContact contact={contact} />
                    : <div>Contact #{contact.id}</div>
            }
        </div>)}
    </div>;
};

ReactDOM.render(<Example />, document.getElementById("root"));
.contact {
    padding: 4px;
    border: 1px solid gray;
}
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875