DEV Community

Cover image for React Hooks: Part 2 and Recipe App
Hulya
Hulya

Posted on • Updated on

React Hooks: Part 2 and Recipe App

Originally I have submitted this tutorial to Facebook Community Challenge 2020, you can access and read it from this link.

If you’ve written React class components before, you should be familiar with lifecycle methods like componentDidMount, componentDidUpdate, and componentWillUnmount. The useEffect Hook is all three of these lifecycle methods combined. It's used for side effects (all things which happen outside of React) like network requests, managing subscriptions, DOM manipulation, setting up event listeners, timeouts, intervals, or local storage, etc.

useEffect functions run after every rerender by default.
It doesn't matter what caused the render like changing the state, or maybe new props, the effect will be triggered after rendering.

Setting the title of the page will also be a side effect.
useEffect takes a callback function, we can tell useEffect when the code we want to be executed with the second argument. This will control the effect.

For the second argument, we can use useEffect in three different ways:

1. useEffect without a Dependency Array

// runs after every rerender
useEffect(() => {
  console.log('I run after every render and at initial render');
});
Enter fullscreen mode Exit fullscreen mode

This renders every time our app renders and at initial render. But we don't want to render each time, this can cause an infinite loop and we should avoid this.

We need to optimize our components. We can pass a list of dependencies. The dependency will trigger an effect on the change of the dependencies.

Let's see it in a simple example.

// src/components/UseEffect.js

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

const UseEffect = ()  => {
  const [count, setCount] = useState(0);
  const [isOn, setIsOn] = useState(false;)

// useEffect to set our document title to isOn's default state
  useEffect(() => {
    document.title = isOn;
    console.log('first render');
  });

const handleClick = () => {
  setIsOn(!isOn);
  setCount(count + 1)
}
  return (
    <div>
      <h1>{isOn ? "ON" : "OFF"}</h1>
      <h1>I was clicked {count} times</h1>
      <button onClick={handleClick} className="btn">Click me</button>
    </div>
  );
}

export default UseEffect;
Enter fullscreen mode Exit fullscreen mode

In our example, we have two states: count and isOn. We are rendering these with our button and h1 tags. When the button gets clicked, we are setting the isOn state to the opposite of its state.

For the purpose of this example, we are setting useEffect hook and changing our document title to our isOn's default value(false).

With our console.log, we can see that we rerender our component with our initial render and whenever we click the button. Because we don't have any array dependency.

2. useEffect with an Empty Dependency Array

// runs at initial render
useEffect(() => {
  console.log('I only run once');
}, []);
Enter fullscreen mode Exit fullscreen mode

This only runs once when the component is mounted or loaded.

It looks exactly like the behavior of componentDidMount in React classes. But we shouldn't compare with React class components.

3. useEffect with a Non-empty Dependency Array

// runs after every rerender if data has changed since last render
useEffect(() => {
  console.log('I run whenever some piece of data has changed)');
}, [id, value]);
Enter fullscreen mode Exit fullscreen mode

If the variable is inside this array, we will trigger this effect only when the value of this variable changes, and not on each rerender. Any state or props we list in this array will cause useEffect to re-run when they change.

We can put our variables inside the dependency array from our component like any variables that we want for; for example, state variables, local variables, or props.
They adjust the array of dependencies.

// src/components/UseEffect.js

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

const UseEffect = () => {
    const [ count, setCount ] = useState(0);
    const [ isOn, setIsOn ] = useState(false);

    useEffect(() => {
      document.title = isOn;
      // only difference from our previous example
      setCount(count + 1);
    });

    const handleClick = () => {
      setIsOn(!isOn);
    };

    return (
      <div>
        <h1>{isOn ? 'ON' : 'OFF'}</h1>
        <h1>I was clicked {count} times</h1>
        <button onClick={handleClick} className="btn">Click me</button>
      </div>
    );
}

export default UseEffect;
Enter fullscreen mode Exit fullscreen mode

We have just changed one line of code from the previous example and changed useEffect a little, we will not increase our count with the button click. However, we will trigger our effect whenever the useEffect triggers. Let's see what will happen.

loop.gif

source.gif

We are in an infinite loop; but why? React rerenders our component when the state changes. We are updating our state in our useEffect function, and it's creating an infinite loop.

I think no one wants to stuck in a loop; so, we need to find a way to get out of the loop and only run our function whenever our isOn state changes. For that, we will add our dependency array and pass our isOn state.

The array of variables will decide if it should execute the function or not. It looks at the content of the array and compares the previous array, and if any of the value specified in the array changes compared to the previous value of the array, it will execute the effect function. If there is no change, it will not execute.

// src/components/UseEffect.js

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

const UseEffect = () =>  {
    const [ count, setCount ] = useState(0);
    const [ isOn, setIsOn ] = useState(false);

    useEffect(() => {
      document.title = isOn;
      setCount(count + 1);
      // only add this
    }, [isOn]);

    const handleClick = () => {
      setIsOn(!isOn);
    };

    return (
      <div>
        <h1>{isOn ? 'ON' : 'OFF'}</h1>
        <h1>I was clicked {count} times</h1>
        <button onClick={handleClick} className="btn">Click me</button>
      </div>
    );
  }

export default UseEffect;
Enter fullscreen mode Exit fullscreen mode

dep.gif

It seems like working, at least we got rid of the infinite loop, when it updates count it will rerender the component. But if you noticed, we start counting from 1 instead of 0. We render first at initial render, that's why we see 1. This effect behaves as a componentDidMount and componentDidUpdate together. We can solve our problem by adding an if condition.

 if(count === 0 && !isOn) return;
Enter fullscreen mode Exit fullscreen mode

This will only render at the first render, after that when we click the button, setIsOn will be true. Now, our code looks like this.

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

const UseEffect = () =>  {
    const [ count, setCount ] = useState(0);
    const [ isOn, setIsOn ] = useState(false);

    useEffect(() => {
      document.title = isOn;
      // add this to the code
      if(count === 0 && !isOn) return;
      setCount(count + 1);
    }, [isOn]);

    const handleClick = () => {
      setIsOn(!isOn);
    };
    return (
      <div>
        <h1>{isOn ? 'ON' : 'OFF'}</h1>
        <h1>I was clicked {count} times</h1>
        <button onClick={handleClick} className="btn">Click me</button>
      </div>
    );
  }

export default UseEffect;
Enter fullscreen mode Exit fullscreen mode

dep2.gif

Okay, now it starts from 0. If you're checking the console, you may see a warning:

warning.png

We will not add count inside our dependency array because if the count changes, it will trigger a rerender. This will cause an infinite loop. We don't want to do this, that's why we will not edit our useEffect. If you want, you can try it out.

useEffect Cleanup

useEffect comes with a cleanup function that helps unmount the component, we can think of it is like componentWillUnmount lifecycle event. When we need to clear a subscription or clear timeout, we can use cleanup functions. When we run the code, the code first will clean up the old state, then will run the updated state. This can help us to remove unnecessary behavior or prevent memory leaking issues.

useEffect(() => {
  effect;
  return () => {
    cleanup;
  };
}, [input]);
Enter fullscreen mode Exit fullscreen mode
// src/components/Cleanup.js

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

const Cleanup = ()  => {
  const [ count, setCount ] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => {
      setCount((prevCount) => prevCount + 1);
      }, 1000);

      // cleanup function
    return () => clearInterval(intervalId);
  }, []);

  return (
  <div>
    <h1>{count}</h1>
  </div>
  );
}

export default Cleanup;
Enter fullscreen mode Exit fullscreen mode

We have defined a setInterval method inside our useEffect hook, and our interval will run in the background. We are passing a function inside setInterval and it will update our count piece of state every second.
Our useEffect hook is only gonna run one time because we have our empty dependency array.

cleanup.gif

To clean up our hook, we are creating our return function, getting our interval id, and passing inside our clearInterval method.

  • We can use multiple useEffect's in our application.
  • We cannot mark useEffect as an async function.
  • React applies effect in the order they are created.
  • We can make API calls to React in four different ways:
  1. Call fetch/Axios in your component
  2. Make another file and store your API calls.
  3. Create a reusable custom hook.
  4. Use a library like react-query, SWR, etc.

We will use fetch in our application for simplicity. Now, ready to move on with our final demo app? Time to combine everything you have learned with a real-life application. This will be fun!!! 😇

RECIPE APP

It's time to create our demo app!
We will create a Food Recipe app, we will fetch data from an API and we will use both useState and useEffect hooks.

First, create a new file under src > components and name it FoodRecipe.js.
To be able to get a response for search queries, we need an APP ID and an APP KEY.

How Can I Fetch Data?

  1. Go to edamam.com
  2. Choose Recipe Search API and click on Sign Up
  3. Choose Developer and click on Start Now
  4. Fill out the form.
  5. Go to Dashboard
  6. Click on Applications > View. You should see your Application ID and Application Keys on this page.
  7. Copy your keys and paste them inside the code.
  8. API can give some errors, if you see any CORS errors, add a cors browser extension for the browser you are using. Firefox / Chrome
  9. Still, there is a problem? You need to wait until your API keys are available. Also, for the free version, we can only make 5 requests per minute. You can check out the documentation.
// src/components/FoodRecipe.js
import React, {useEffect} from 'react';

const FoodRecipe = () => {
  // paste your APP_ID
  const APP_ID = '';
  // paste your APP_KEY
  const APP_KEY = '';

// url query is making a search for 'chicken' recipe
  const url = `https://api.edamam.com/search?q=chicken&app_id=${APP_ID}&app_key=${APP_KEY}`;

  // useEffect to make our API request
  useEffect(() => {
    getData();
  }, []);

  // created an async function to be able to fetch our data
  const getData = async (e) => {
    const response = await fetch(url);
    const result = await response.json();
    // console log the results we get from the api
    console.log(result);
  };

  return (
    <div>
      <h1>Food Recipe App </h1>
      <form>
        <input type="text" placeholder="Search for recipes" />
        <button type="submit" className="btn">
          Search
        </button>
      </form>
    </div>
  );
};

export default FoodRecipe;
Enter fullscreen mode Exit fullscreen mode

food-demo.gif

Let's see what we did in our code:

  • Created some JSX elements(form, input, and button properties).
  • We are calling our function to fetch our data.
  • Created a fetch request to get our data, and used useEffect hook to call our function. We are using our empty dependency array because we will only make a request when our app loads.

We got our API response, and we got a lot of information. You can see from the gif. Now, we need to create a state for our recipes, and we will update the recipes with the API data. We will only extract hits and their contents from our response. Let's do it!

// src/components/FoodRecipe.js
import React, {useState, useEffect} from 'react';

const FoodRecipe = () => {
  // state for our API data
  const [recipes, setRecipes] = useState([]);

  const APP_ID = '';
  const APP_KEY = '';

  const url = `https://api.edamam.com/search?q=chicken&app_id=${APP_ID}&app_key=${APP_KEY}`;

  useEffect(() => {
    getData();
  }, []);

  const getData = async () => {
    const response = await fetch(url);
    const result = await response.json();
    console.log(result);
    // set the state for our results and extract the 'hits' data from API response
    setRecipes(result.hits);
  };

  // some ui
};

export default FoodRecipe;
Enter fullscreen mode Exit fullscreen mode

Okay, here we have added our recipes state and updated with setRecipes. From our API call, we see that hits is an array, that's why for the default value we put an empty array.

We need to display our recipes, for that let's create a Recipe component.

Go to src > components, create a new component, and name it Recipe.js. Copy this code, this will allow us to display individual recipes.

Here, I have used some Semantic UI components to display our individual recipes.

// src/components/Recipe.js
import React from 'react';

const Recipe = () => {
    return (
      <div class="ui column grid">
        <div className="column recipe">
          <div className="content">
            <h2>Label</h2>
            <p>Calories: </p>
            <ul>
              <li>Ingredients</li>
            </ul>
            <a href="" target="_blank">
              URL
            </a>
            </div>
          <div className="ui fluid card">
            <img />
          </div>
        </div>
      </div>
    );
};

export default Recipe;
Enter fullscreen mode Exit fullscreen mode

Now, we need to map over our recipes state, and display the results.

// src/components/FoodRecipe.js
// ..............
return (
    <div>
      <h1>Food Recipe App </h1>
      <form>
          <input type="text" placeholder="Search for recipes" />
          <button type="submit" className="btn">
            Search
          </button>
        </form>
        <div className="recipes">
          {/* map over our array and pass our data from API*/}
          {recipes !== [] &&
              recipes.map((recipe) => (
              <Recipe
                key={recipe.recipe.url}
                label={recipe.recipe.label}
                calories={recipe.recipe.calories}
                image={recipe.recipe.image}
                url={recipe.recipe.url}
                ingredients={recipe.recipe.ingredients}
              />
            ))}
        </div>
      </div>
  );
Enter fullscreen mode Exit fullscreen mode

For now, I am getting our Recipe.js without any props, of course.

food.gif

Now, we can go to our Recipe component and pass our props to it. We are getting these props from the parent FoodRecipe.js. We will use destructuring to get our props.

// src/components/Recipe.js
import React from 'react';

// destructure label, calories etc
const Recipe = ({label, calories, image, url, ingredients}) => {
  return (
      <div class="ui column grid">
          <div className="column recipe">
            <div className="content">
              <h2>{label}</h2>
              <p>{calories}</p>
              <ul>{ingredients.map((ingredient) => 
                  <li key={ingredient.text}>{ingredient.text}</li>)}
              </ul>
              <a href={url} target="_blank">
                URL
              </a>
            </div>
            <div className="ui fluid card">
              <img src={image} alt={label} />
            </div>
          </div>
        </div>
  );
};

export default Recipe;
Enter fullscreen mode Exit fullscreen mode

chicken.gif

Tadaa!! We got our chickens!

Now, we need to use our search bar, we will search the recipe from our input field. To get the state of our search bar, we will create a new piece of state.

Go to FoodRecipe.js and add a new search state.

// src/components/FoodRecipe.js
// create a state for search query
const [search, setSearch] = useState('');
Enter fullscreen mode Exit fullscreen mode

Set the value for input value search, setSearch will update our input with the onChange event handler.

The input is keeping track of its state with the search state. We can get input's value from event.target.value.
Then we can change our state with setSearch function.

// src/components/FoodRecipe.js
<input
  type="text"
  value={search}
  onChange={(event) => setSearch(event.target.value)}
/>
Enter fullscreen mode Exit fullscreen mode

We need to update our state after we click on Search Button. That's why we need another state. And we can update our url from chicken query to any query. Make a new state, name it query.

// src/components/FoodRecipe.js

const [query, setQuery] = useState('');

// when you send the form, we call onSubmit handler to query the results
const onSubmit = (e) => {
  // prevent browser refresh
  e.preventDefault();
  // setQuery for the finished search recipe
  setQuery(search);
};
Enter fullscreen mode Exit fullscreen mode

Now, we need to pass our query state to our onEffect dependency array. Whenever we click on the Search button, we will call our API and change our state to a new query state.

The query will only run after the form submit. Use it as a dependency inside the array. Our final code now looks like this:

// src/component/FoodRecipe.js
import React, {useState, useEffect} from 'react';
import Recipe from './Recipe';

const FoodRecipe = () => {
  const [recipes, setRecipes] = useState([]);
  const [search, setSearch] = useState('');
  const [query, setQuery] = useState('');

  const APP_ID = '';
  const APP_KEY = '';

  const url = `https://api.edamam.com/search?q=${query}&app_id=${APP_ID}&app_key=${APP_KEY}`;

  useEffect(() => {
    getData();
  }, [query]);

  const getData = async () => {
    const response = await fetch(url);
    const result = await response.json();
    setRecipes(result.hits);
  };

  const onSubmit = (e) => {
    e.preventDefault();
    setQuery(search);
    // empty the input field after making search
    setSearch('');
  };

  return (
    <div>
      <h1>Food Recipe App </h1>
      <form onSubmit={onSubmit}>
        <input
          type="text"
          placeholder="Search for recipes"
          value={search}
          onChange={(e) => setSearch(e.target.value)}
        />
        <button type="submit" className="btn">
          Search
        </button>
      </form>
      <div className="ui two column grid">
        {recipes !== [] &&
          recipes.map((recipe) => (
            <Recipe
              key={recipe.recipe.url}
              label={recipe.recipe.label}
              calories={recipe.recipe.calories}
              image={recipe.recipe.image}
              url={recipe.recipe.url}
              ingredients={recipe.recipe.ingredients}
            />
          ))}
      </div>
    </div>
  );
};

export default FoodRecipe;
Enter fullscreen mode Exit fullscreen mode

Screen Shot 2020-12-28 at 3.14.09 PM.png
Time to enjoy your ice creams! I hope you liked the project.

Wrapping Up

Now, go build something amazing, but don't pressure yourself. You can always go back to the tutorial and check how it is done, also check the official React documentation. Start small, try creating components first, then try to make it bigger and bigger. I hope you enjoyed this tutorial. I'm looking forward to seeing your feedback.

If you run into any issues with your app or you have questions, please reach out to me on Twitter or Github.

Credits:

Giphy

References:

Here are the references I used for this tutorial:

Thanks for your time. Like this post? Consider buying me a coffee to support me writing more.

Top comments (0)