Search code examples
javascriptreactjsfetchstate

React child components state is undefined but can see state using console.log


I have a parent component that gets data from an API end point using fetch. This data displays like it should. The parent component passes an element of an array of objects to the child component. In the child component, when I do a console log I can see the state when it's undefined and when the state is set. The issue that I am having is when I try to access a key of the state (i.e. ticket.title) I get an error saying that ticket is undefined. Any help with would be great.

TicketList

import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import TicketDetails from "./TicketDetails"

export default function TicketList() {
    const [tickets, updateTickets] = useState([])
    const [ticketIndex, updateticketIndex] = useState("0")
    useEffect(() => {
        async function fetchTickets() {
            const response = await fetch("/api/v1/tickets")
            const json = await response.json()
            updateTickets(json.data)
        }
        fetchTickets()
    }, [])
    return (
        <Wrapper>
            < div >
                <TableTitle>
                    <h3>Tickets</h3>
                    <button type="submit">Create A Ticket</button>
                </TableTitle>
                {
                    tickets.map((ticket, index) => (
                        <ListInfo key={ticket._id} onClick={() => updateticketIndex(index)}>
                            <Left>
                                <p>{ticket.project}</p>
                                <p>{ticket.title}</p>
                                <p>{ticket.description}</p>
                            </Left>
                            <Right>
                                <p>{ticket.ticketType}</p>
                                <p>{ticket.ticketStatus}</p>
                                <p>{ticket.ticketPriority}</p>
                            </Right>
                        </ListInfo>
                    ))
                }
            </div>
            <TicketDetails key={tickets._id} data={tickets[ticketIndex]} />

        </Wrapper>
    );
}

const Wrapper = styled.div` 
display: flex;
background: white;
grid-area: ticketarea;
height: calc(100vh - 4.25rem);
`

const ListInfo = styled.div`
display: flex;
justify-content: space-between;
width: 100%;
padding: .5rem .75rem;
border-bottom: solid 1px #ccc;
`;

const Left = styled.div`
display: flex;
flex: 2;
flex-direction: column;
p {
padding: .25rem;
}
`;

const Right = styled.div`
display: flex;
flex: 1;
flex-direction: column;
align-items: end;
width: 500px;
p {
padding: .25rem;
}
`;

const TableTitle = styled.div`
display: flex;
justify-content: space-between;
padding: 1rem 1rem;
border-bottom: solid 1px #ccc;
button {
      padding: .5rem;
}
`;

TicketDetails

import React, { useEffect, useState } from 'react'
// import TicketInfo from './TicketInfo'
import TicketNotes from "./TicketNotes"
import styled from "styled-components"

export default function TicketDetail(data) {
  const [ticket, setTicket] = useState(data)
  useEffect(() => {
    setTicket(data)
  }, [data])
  console.log(ticket.data)

  return (
    <Main>
      <TicketInfo key={ticket._id}>
        <h2>{ticket.title}</h2>
        <Info>
          <div>
            <InfoItem>
              <p>Project</p>
              <p>{ticket.project}</p>
            </InfoItem>
            <InfoItem>
              <p>Assigned Dev</p>
              <p>{ticket.assignedDev}</p>
            </InfoItem>
            <InfoItem>
              <p>Created By</p>
              <p>{ticket.submitter}</p>
            </InfoItem>

          </div>
          <div>
            <InfoItem>
              <p>Type</p>
              <p>{ticket.ticketType}</p>
            </InfoItem>
            <InfoItem>
              <p>Status</p>
              <p>{ticket.ticketStatus}</p>
            </InfoItem>
            <InfoItem>
              <p>Priority</p>
              <p>{ticket.ticketPriority}</p>
            </InfoItem>
          </div>
        </Info>
        <Description>{ticket.description}</Description>
      </TicketInfo>
      <TicketNotes />
      <TicketComment>
        <textarea name="" id="" cols="30" rows="10" />
        <button type="submit">Submit</button>
      </TicketComment>
    </Main>
  )
}

const TicketInfo = styled.div` 
margin: .5rem;
h2{
padding: 0.5rem 0;
}
`;

const Description = styled.p` 
padding-top: .5rem;
`;

const Info = styled.div`
display: flex;
justify-content: space-between;
border-bottom: solid 1px #ddd;
`;

const InfoItem = styled.section`
margin: .5rem 0;
 p:nth-child(1) {
   text-transform: uppercase;
   color: #ABB1B6;
   font-weight: 500;
   padding-bottom: .25rem;
 }
`;


const Main = styled.div`
background: white;

`


const TicketComment = styled.div`
  display: flex;
  flex-direction: column;
  width: 40rem;
  margin: 0 auto ;

input[type=text] {
  
  height: 5rem;
  border: solid 1px black;
}

textarea {
  border: solid 1px black;
}

button {
margin-top: .5rem;
padding: .5rem;
width: 6rem;

}
`;

Solution

  • There are a few issues here, let's tackle them in order.

    Tickets are undefined

    When TicketList is mounted, it fetches tickets. When it renders, it immediately renders TicketDetail. The tickets fetch request won't have finished so tickets is undefined. This is why TicketDetail errors out. The solution is to prevent rendering TicketDetail until the tickets are available. You have a few options.

    A bare bones approach is to just prevent rendering until the data is available:

    { !!tickets.length && <TicketDetails key={tickets._id} data={tickets[ticketIndex]} />
    

    This uses how logical operators work in JS. In JS falsey && expression returns falsey, and true && expression returns expression. In this case, we turn ticket.length into a boolean. If it is 0 (i.e. not loaded, therefore false), we return false, which React simply discards. If it is greater than 0 (i.e. loaded, therefore true), we render the component.

    This doesn't really result in a positive UX though. Ideally this is solved by showing some kind of Loading spinner or somesuch:

    {
      !!tickets.length 
        ? <TicketDetails . . . /> 
        : <LoadingSpinner />
    }
    

    Child data access

    In TicketDetail it seems like you meant to destructure data. Currently you are taking the entire prop object and setting it to ticket. Fixing this should resolve the other half of the issue.

    Paradigms

    You didn't specifically ask for this, but I’d like to back up and ask why you are putting this prop into state? Typically this only done when performing some kind of ephemeral edit, such as pre-populating a form for editing. In your case it looks like you just want to render the ticket details. This is an anti-pattern, putting it into state just adds more code, it doesn’t help you in any way. The convention in React is to just render props directly, state isn't needed.