React Typescript Conversion issue rendering data

Viewed 183

This is my first post!

I'm filtering through a list of products I'm fetching from Contentful using graphQL to find the product that matches what the user clicked. Everything worked before I converted the component to Typescript. I am vaguely familiar with TS and just started using it a few months ago.

When I log the product, I get back the Object with the correct filtered data, however when I go to access a key name "imagesCollection" I get the following TS error message:

Property 'imagesCollection' does not exist on type '{}'.ts(2339)

If I // @ts-ignore the product image renders without issues. Is this a race condition of some kind?

Typescript Error Screenshot

Any help is much appreciated as I have spent a good amount of time trying to solve it myself. Thank you!

The Component:

const Product = (props: any) => {
  // Custom HOOK for fetching Contentful Data
  const slug = props.match.params.slug;
  const { product } = useContentful(query, slug);
  console.log(product);
  const [jacketColor, setJacketColor] = useState(null);
  const [color, setColor] = useState(null);
  useEffect(() => {
    setJacketColor(
      product.imagesCollection && product.imagesCollection.items[1].url
    );
    setColor(
      // @ts-ignore
      product.imagesCollection && product.imagesCollection.items[1].description
    );
  }, [product]);

  const changeColor = (jacketColor: string) => {
    // @ts-ignore
    const filterHero = product.imagesCollection.items
      .filter((heroJacket: { title: string }) =>
        heroJacket.title.includes("hero")
      )
      .filter(
        (item: { description: string }) => item.description === jacketColor
      );

    setJacketColor(filterHero[0].url);
    setColor(filterHero[0].description);
  };

  const thumbnailImages = () => {
    return (
      // @ts-ignore
      product.imagesCollection &&
      // @ts-ignore
      product.imagesCollection.items
        .filter((jacket: queryProps) => jacket.title.includes("thumbnail"))
        .map((jacket: { url: string; description: string }, index: number) => {
          return (
            <img
              className={styles.thumbnailImages}
              key={`products-${index}`}
              src={jacket.url}
              // @ts-ignore
              alt={product.title}
              onClick={() => changeColor(jacket.description)}
            />
          );
        })
    );
  };

  return (
    <>
      <Header />
      {!product ? (
        <Loading />
      ) : (
        <div className={styles.productsContainer}>
          {/*  @ts-ignore */}
          <p className={styles.title}>{product && product.title}</p>
          {documentToReactComponents(
            // @ts-ignore
            product.description && product.description.json,
            RICHTEXT_OPTIONS
          )}
          {/*  @ts-ignore */}
          <p className={styles.price}>{`$${product && product.price}`}</p>
          {
            <img
              className={styles.productImage}
              //  @ts-ignore
              src={jacketColor}
              //  @ts-ignore
              alt={product && product.title}
            />
          }
          <p className={styles.selectColor}>{`Select a colour: ${color}`}</p>
          <div className={styles.thumbnailImagesWrapper}>
            {thumbnailImages()}
          </div>
        </div>
      )}
    </>
  );
};

export default Product;

The custom hook to fetch data:

function useContentful(query: string, slug: string | null) {  
  const [products, setProducts] = useState([]);
  const [product, setProduct] = useState({});

  useEffect(() => {
    fetch(graphqlURL, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${REACT_APP_CDA_TOKEN}`,
      },
      body: JSON.stringify({ query }),
    })
      .then((res) => res.json())
      .then(({ data }) => {     
        if (slug) {
          data.arcteryxCollection.items.filter((item: queryProps) => {
            return item.slug === slug && setProduct(item);
          });           
        } else {
          setProducts(data.arcteryxCollection.items);
        }
      })
      .catch((err) => console.error(err));
  }, [query, slug]);
  
  return { product, products };
}

The graphQL query structure:

export const query = ` 
query {
  arcteryxCollection {
    items {
      title
      slug
      description {
        json
      }
      price
      imagesCollection {
        items {
          url
          title
          description
        }
        }
      }
    }
  }
`;

The TS interface I'm importing:

export interface queryProps {
  title: string;
  slug: string;
  price: number;
  description: {
    json: {
      content: {
        value: string;
      }[];
    };
  };
  imagesCollection: {
    items: {
      url: string;
      title: string;
      description: string;
    }[];
  };
}

Link to the Repo: https://github.com/danlubbers/arcteryx-graphql

2 Answers

It's because you are initializing your product state with an empty object.

const [product, setProduct] = useState({});

This makes TypeScript infer that your state's type is an empty object. Therefore, it doesn't have an imagesCollection on it.

You can either give it a type or initialize it with an object that has the properties you want.

Here is an example adding a type for your products. You can add the additional properties you need to use from the Graph QL response.

type ImageCollectionItem = {
    url: string;
    title: string;
    description: string;
}

type ProductState = {
    imagesCollection?: {
        items: ImageCollectionItem[]
    }
}

//...

const [product, setProduct] = useState<ProductState>({});

This was the solution I found that ended up working for me. In the use-contentful.ts hook I changed the way I filtered the response to store the data as an array instead of an object.

Seen here:

const [product, setProduct] = useState<queryProps[]>([]);

          let filteredProduct = data.arcteryxCollection.items.filter((item: queryProps) => {                        
            return item.slug === slug;
          });          
          setProduct(filteredProduct);               

This allowed me to then access the data in the Products Component and render it this way:

 const { product } = useContentful(query, slug);
 const productObj = product[0];

 const [jacketColor, setJacketColor] = useState("");

 setJacketColor(productObj && productObj.imagesCollection.items[1].url);
Related