javascriptreactjsarraysreact-hooksjsx

Is there some useEffect and useState behavior that explains why only one of these instances work?


I'm using useEffect to fetch data. This data is passed to a state, then used as a prop for a component to render/populate accordingly. Here is the code that works:

const [projects, setProjects] = useState([]);

useEffect(() => {
  const q = query(collection(db, "projects"), orderBy("update", "desc"));
  const unsubscribe = onSnapshot(q, (QuerySnapshot) => {
    const fetched = [];
    QuerySnapshot.forEach((doc) => {
      fetched.push({ ...doc.data(), id: doc.id });
    });
    const sortedProjects = fetched.sort((a, b) => a.update - b.update);
    setProjects(sortedProjects);
  });
  return () => unsubscribe;
}, []);

The code above correctly fetches the data and then passes it to a component that then uses map to display a list of the projects. In terms of streamlining, I wanted to see if I could do the same with the resume data. Here is the code for that:

const [edu, setEducation] = useState([]);

useEffect(() => {
  const q = query(
    collection(db, "resume/resume/education"),
    orderBy("startDate")
  );
  const unsubscribe = onSnapshot(q, (QuerySnapshot) => {
    const fetched = [];
    QuerySnapshot.forEach((doc) => {
      fetched.push({ ...doc.data(), id: doc.id });
    });
    const sortedEdu = fetched.sort(
      (a, b) => a.startDate.nanoseconds - b.startDate.nanoseconds
    );
    setEducation(sortedEdu);
  });
  return () => unsubscribe;
}, []);

This one, for some reason, does not work. I checked that data is being retrieved (it is), and the useEffect and useState appear to be working as they should. I even added a log in to the component, and indeed, the data shows up from that end, but I'm still getting a map error, saying the array is undefined, and preventing the react from rendering. I also know that these components work when I try and input data directly. What could be happening to cause this?

I've literally copy/pasted then tweaked values, and still get the same problem. For clarity's sake, here is the component in question:

export const ResumeItemLister = ({ items, sectionTitle }) => {
  return (
    <div>
      <h2 className="text-xl text-left">{sectionTitle}</h2>
      <hr />
      <table>
        {items.map(({ title, location, date, bullets }) => (
          <tr className="pt-10">
            <div className="grid grid-cols-3">
              <td className="text-left">{date}</td>
              <td className="col-span-2">
                <div className="text-left">
                  {title ? (
                    <p>
                      <bold className="font-bold">{title}</bold>, {location}
                    </p>
                  ) : (
                    <p>{location}</p>
                  )}
                  <ul>
                    {bullets.map((text) => (
                      <li className="list-disc list-inside"> {text}</li>
                    ))}
                  </ul>
                </div>
                <br />
              </td>
            </div>
          </tr>
        ))}
      </table>
    </div>
  );
};

This works, as long as I explicitly set the items in the parent component. Using the fetched state however, I get:

Uncaught TypeError: Cannot read properties of undefined (reading 'map')

I understand this means items is undefined, but as mentioned, that isn't the case. Any help is deeply appreciated


Solution

  • You've a couple mapped arrays in ResumeItemLister. First is the main items prop, which it seems you've verified is a defined array, and the second is some bullets property of each mapped items item element.

    Generally you will want to ensure your code protects against "accidental" accesses into potentially undefined references.

    Examples:

    I'd likely suggest only rendering the unordered list if item.bullets exists and has list items to render though:

    {!!bullets?.length && (
      <ul>
        {bullets.map((text) => (
          <li className="list-disc list-inside">{text}</li>
        ))}
      </ul>
    )}
    

    Full code:

    export const ResumeItemLister = ({ items, sectionTitle }) => {
      return (
        <div>
          <h2 className="text-xl text-left">{sectionTitle}</h2>
          <hr />
          <table>
            {items.map(({ title, location, date, bullets }) => (
              <tr className="pt-10">
                <div className="grid grid-cols-3">
                  <td className="text-left">{date}</td>
                  <td className="col-span-2">
                    <div className="text-left">
                      {title ? (
                        <p>
                          <bold className="font-bold">{title}</bold>, {location}
                        </p>
                      ) : (
                        <p>{location}</p>
                      )}
                      {!!bullets?.length && (
                        <ul>
                          {bullets.map((text) => (
                            <li className="list-disc list-inside">{text}</li>
                          ))}
                        </ul>
                      )}
                    </div>
                    <br />
                  </td>
                </div>
              </tr>
            ))}
          </table>
        </div>
      );
    };