Lesson 2

Journey through Component Lifecycles with useEffect Hook in React

Topic Overview and Actualization

Welcome to this lesson, where we delve deeply into the useEffect hook in React. Our goal is to grasp the concept of component lifecycle in functional components and use the useEffect hook to achieve this. A lifecycle in this context is a component's birth, growth, and retirement. We'll dissect each phase and apply useEffect. We'll conclude with a real-world example to solidify these concepts. Let's get started!

Understanding Component Lifecycle in Functional Components

The component lifecycle is a sequence of events that range from initialization through to destruction in a React application, much like a human lifecycle.

  1. Mounting: This is the point at which a component's life commences, much like the start of life for a newborn baby.

  2. Updating: Throughout a component's lifetime, it may evolve, thanks to changes in state and props, just as we grow and adapt.

  3. Unmounting: When a component is no longer needed, it's removed — this is akin to the end of life.

Functional components don’t have lifecycle methods like class components. However, useEffect can simulate these behaviors.

Using useEffect to Handle Different Lifecycle Phases

useEffect is excellent for simulating lifecycle methods:

  1. Mounting with useEffect: By using useEffect and specifying an empty array [] as the dependency, the effect runs once after the initial render.

  2. Updating with useEffect: All the data the effect needs to 'watch' should be specified in the dependency array.

  3. Unmounting with useEffect: A cleanup function returned from the effect helps prevent memory leaks during this phase.

Here's an example showing you how to apply everything:

1import React, { useState, useEffect } from 'react'; 2 3function MyComponent() { 4 const [count, setCount] = useState(0); 5 6 useEffect(() => { 7 // Mounting and updating: 8 document.title = `You clicked ${count} times`; 9 10 // Unmounting: 11 return function cleanup() { 12 document.title = `React App`; // Original title restored 13 }; 14 15 }, [count]); // When count updates, re-run the effect 16 17 return ( 18 <div> 19 <p>You clicked {count} times</p> 20 <button onClick={() => setCount(count + 1)}> 21 Click me 22 </button> 23 </div> 24 ); 25} 26 27export default MyComponent;

In this case, every time you click the button, the count state will be updated along with the document title. When you navigate away from the component, the cleanup function restores the original title.

Deep Dive: Cleanup Function and Handling Memory Leaks

Now that we've gotten a hold of the useEffect's basic functionality, it's time to explore its power to prevent memory leaks. Imagine your code sets up a subscription in a component. But aligning with the natural lifecycle, our component might be removed from the DOM at some point. If our component is gone but our subscription is still active, we have a problem -- this is a memory leak!

Memory leaks subtly eat up your system resources and can cause your application to slow down or crash. Hence, it's important to clean up your task if the component unmounts during the effect's lifecycle.

The cleanup part of useEffect comes into play here. Upon returning a function from a useEffect hook, React interprets this as a cleanup function and invokes it before the component is removed from the UI to prevent memory leaks.

Consider this example:

1import React, { useState, useEffect } from "react"; 2 3function MyComponent() { 4 const [size, setSize] = useState(window.innerWidth); 5 6 // Effect to update state 7 useEffect(() => { 8 const handleResize = () => setSize(window.innerWidth); 9 window.addEventListener('resize', handleResize); // Set up subscription 10 11 // Cleanup function to remove subscription 12 return () => { 13 window.removeEventListener('resize', handleResize); 14 }; 15 16 }, []); // Empty array ensures the effect runs only once 17 18 return ( 19 <div> 20 <p>Window Size: {size} px</p> 21 </div> 22 ); 23} 24 25export default MyComponent;

In this code, our effect sets up a window resize event listener, which triggers every time the window's width changes. But what if the MyComponent suddenly gets removed from the DOM? Our listener would still be active and may cause memory leaks. To prevent this, our effect returns a cleanup function, which removes the event listener when MyComponent is unmounted.

So never forget, when performing tasks such as setting up a subscription in an effect, always provide a cleanup function to release resources if/when the component unmounts!

Calling Async Functions in `useEffect`

As we are using React, which is a JavaScript library, there will be instances when we need to perform asynchronous operations. However, we might wonder how to perform asynchronous tasks using the useEffect hook. Let's take a deeper look.

An async function is a function declared with the async keyword and allows us to use the await keyword to pause the execution of the function until a particular promise is resolved.

UseEffect doesn't support async functions as its parameter directly. This is because useEffect expects its parameter function to return a cleanup function or nothing at all. However, async functions always return a promise, which leads to a warning or error.

Here's an example that demonstrates this point:

1import React, { useState, useEffect } from 'react'; 2 3function MyComponent() { 4 const [state, setState] = useState("Initial State"); 5 6 // A mock async function 7 const asyncFunc = async () => { 8 return Promise.resolve("Updated state"); 9 }; 10 11 useEffect(async () => { 12 let result = await asyncFunc(); 13 setState(result); 14 }, []); 15 16 return <div>{state}</div>; 17} 18 19export default MyComponent;

In the above example, the async function asyncFunc returns a promise, but this causes issues when used directly inside useEffect.

The correct approach is to define an async function inside the useEffect hook and then call it:

1import React, { useState, useEffect } from 'react'; 2 3function MyComponent() { 4 const [state, setState] = useState("Initial State"); 5 6 // Mock async function 7 const asyncFunc = async () => { 8 return Promise.resolve("Updated state"); 9 }; 10 11 useEffect(() => { 12 const callAsyncFunc = async () => { 13 let val = await asyncFunc(); 14 setState(val); 15 } 16 17 callAsyncFunc(); 18 19 }, []); 20 21 return <div>{state}</div>; 22} 23 24export default MyComponent;

In the updated code, callAsyncFunc is defined inside useEffect, and then we immediately invoke it. This respects the rules of useEffect while allowing us to handle promises or asynchronous operations correctly.

Great work! You've made significant progress in mastering useEffect, especially with asynchronous operations. As a rule of thumb, always work with async functions within the scope of the useEffect parameter function – never pass an async function directly to useEffect.

Lesson Summary and Practice

Great work! We have covered the process of understanding lifecycle phases and managing them with useEffect.

Now, the handling of phases in the life of a React component using useEffect should be clearer. Please remember that understanding this concept is critical when creating React apps with complicated data flows and side effects.

Are you ready for practice exercises? They will reinforce your learning and give you a chance to practice the use of the useEffect hook. Keep going — you're doing fantastic work!

Enjoy this lesson? Now it's time to practice with Cosmo!

Practice is how you turn knowledge into actual skills.