Search code examples
reactjsuse-effect

How do I get useEffect to work as expected?


I am new to react and using spoonacular api to fetch recipe using an end point. Here's an example of the data.

{
vegetarian: true,
vegan: false,
preparationMinutes: 10,
extendedIngredients: [
     {original: "1/8 teaspoon ground cardamom"},
     {original: "2 to 4 black peppercorns, crushed"},
     {original: "1/4 cup sweetened condensed milk or 4 teaspoons sugar"},
     {original: "1/4 cup loose Darjeeling tea leaves or 5 tea bags black tea"},
     {original: "2 cups water"}
],
id: 165381,
title: "Chai",
readyInMinutes: 10,
servings: 4,
}

For some reason, whenever I use 'extendedIngredients' I get an error 'Cannot use properties of undefined' but if I comment the 'extendedIngredients', save and then again uncomment and run the app, it works

The code:

const Single=({id})=>{
    
    const [recipe, setRecipe]=useState({})
    
    useEffect(()=>{axios.get(`https://api.spoonacular.com/recipes/${id}/information?apiKey=${apiKey}`)
        .then(res=>{
            setRecipe(res.data)
            console.log("recipe",recipe)
        })
        .catch(err=>{
            console.log(err)
        })
    },[])
 console.log("recipe",recipe)
    return(
      <div>
        
        <Row>
          <Col>
          <h1>{recipe.title}</h1>
          <p>{recipe.extendedIngredients.length} Ingredients</p>
          <p>{recipe.readyInMinutes} Minutes</p>
          <p>{recipe.servings} Servings</p>
          
          </Col>
          <Col>
          <img src={recipe.image} alt={recipe.title}></img>
          </Col>
        </Row>
        <h2>Ingredients</h2>
        {recipe.extendedIngredients.map((ingredient,i)=><p key={i}>{ingredient.original}</p>)}
        <h2>Directions</h2>
        <div dangerouslySetInnerHTML={{__html: recipe.instructions}}/>

      </div>
    )
 
}

Solution

  • If you take a look at your useEffect, you provide an empty dependencies array as an argument. This will cause the callback (your fetch logic) to fire on first render. Axios will create a promise, which might resolve super quickly but it won't be fast enough for said first render. This means, you need to handle the case when there's been no data returned from your network request. The easiest way to achieve this is to use conditional rendering like below.

    With this setup, nothing will render until your promise resolves and once it does React will render your component as intended. Next step is to use a spinner or some kind of indicator that the data is loading.

    const Single = ({ id }) => {
        
      const [recipe, setRecipe] = useState({})
        
      useEffect(() => {
        axios.get(`https://api.spoonacular.com/recipes/${id}/information?apiKey=${apiKey}`)
        .then(res => {
          setRecipe(res.data)
          console.log("recipe", recipe)
        })
        .catch(err => {
          console.log(err)
        })
      }, [])
      console.log("recipe", recipe)
      return (
        <div>
          {recipe && (
            <>
              <Row>
                <Col>
                  <h1>{recipe.title}</h1>
                  <p>{recipe.extendedIngredients.length} Ingredients</p>
                  <p>{recipe.readyInMinutes} Minutes</p>
                  <p>{recipe.servings} Servings</p>
                </Col>
                <Col>
                  <img src={recipe.image} alt={recipe.title}></img>
                </Col>
              </Row>
              <h2>Ingredients</h2>
              {recipe.extendedIngredients.map((ingredient, i) => (
                <p key={i}>{ingredient.original}</p>
              ))}
              <h2>Directions</h2>
              <div dangerouslySetInnerHTML={{ __html: recipe.instructions }} />
            </>
          )}
        </div>
      )
    }