Skip to main content

The Component LifeCycles

Prerequisites#

  • React
  • Components
  • State and props

Learning Objectives#

After this lesson you will be able to:

  • Explain the three 3️⃣ phases of the Component lifecycle
  • Run code within each of the three 3️⃣ phases
  • Use useEffect to implement the three 3️⃣ main lifecycle methods

Framing#

So far we've learned that React Components can be used to break down the UI into smaller and smaller bit of reusable functionality. The Component model even encourages going as low level as rendering a single button.

Part of the Component reusability design is that it allows them to contain as much logic as needed to adapt to their environment and implementation. That flexibility might require the Component to perform some action when it's first mounted, then updated and perhaps, even when it's unmounted.

Take for instance AirBnB. When you perform a search, the site returns a set of data points that match the query and must update state in some way in order to render those items as Components.

The Components themselves may need to keep track of if, and when, it's been added as a favorite ❤️ or adjust it's own layout based on mobile or tablet layout.


⏰ Activity - 5min#

Let's take a look at the following versions of the Mars website that I built in React.

Let's do the following together:

  • Open both sites in separate tabs and with DevTools open as well.
  • In DevTools use the Toggle Device Toolbar to view the site in Responsive mode
  • Enable Tablet view for both web sites
  • Select and examine the following element:
<div id="nav_container">...</div>

The Instructor will now direct your attention to those elements the difference between the two apps.

Note: The implementation of these two sites were created for the following medium article I wrote on 3-ways-to-implement-responsive-design-in-your-react-app


The 3 Main Phases Of The Component Lifecycle#

As the complexity of the Component grows we can make use of more and Hooks to meet the demands of our business logic. Certain functionality might need to be performed only once when Component loads or with each re-render.

Below are the 3 phases that a Component goes through during it's lifetime.

  • Mounting
  • Updating
  • Unmounting

Mounting and Unmounting occur only once during the lifecycle of the Component with Updating occurring as often as the Component is re-rendered.

Each phase can call a specific lifecycle method that runs as the last function call in that cycle.

Here is a visual diagram of the phases and their corresponding lifecycle methods.

Although up to this point we've only worked with Functional Components, in lue of Classes, however under the hood React is indeed initializing and calling them as if they were written as classes. The diagram represents the Components referenced as classes.

Mounting: called when a component is created and inserted into the DOM.

  • constructor - initializes the component and sets state via useState
  • render() - returns the UI
  • DOM and Refs - done using useRef
  • componentDidMount() - performed using useEffect(() => {}, [])

Updating: usually triggered by changes in props or state.

  • setState() - updates state which triggers the re-render
  • render() - returns the UI
  • DOM and Refs - done using useRef
  • componentDidUpdate() - performed using useEffect(() => {}) (No [] included)
  • componentDidUpdate() performed using useEffect(() => {}, [someValueToMonitor])

Unmounting: called when a component is being removed from the DOM.

  • componentDidUnmount() - useEffect(() => {}) (No [] included)

The useEffect Hook#

Now it's time to add yet another Hook to our collection, the useEffect Hook. This Hook lets us perform side effects in functional components.

A side effect is any application state change that is observable outside the called function other than its return value.

Several examples of side effects are:

  • Logging to the console
  • Making an API call
  • Calling setInterval/setTimeout

As we pointed out during the overview of the React lifecycle, useEffect and be run in one of 3 ways:

  • useEffect(() => {}, []) - empty [] means this will only run once when the Component mounts
  • useEffect(() => {}) - no [] means this will run on every render/re-render
  • useEffect(() => {}, [someValueToMonitor]) - run on mount and then only if the value has changed

⏰ Activity - 1min#

Starter Code#

Here is the starter code we will be working with: useEffect CodeSandbox Starter

https://codesandbox.io/s/useeffect-starter-9cpw0?file=/src/Counter.js

👍 - Click on the thumbs up when you have forked the starter code.


Mounting#

Since every Component requires an initial mounting it makes sense that we start with the mounting phase.

Let's take a look at the Solution we are looking to implement. As we can see the app does the following:

  • timer starts counting when the Component mounts
  • timer increments every second
  • clicking on the pause button pauses the timer
  • clicking on start re-initiates the counting sequence.

It's safe to say that our current knowledge of React will fall short to implement that functionality and that we will need to make use of the useEffect Hook to implement the logic.

But let's first push the envelope and see how far we can go without useEffect before we hit our first wall.

Base Functionality#

As of right now our implementation only contains the console logs needed to confirm that the buttons work.

Counter.jsx
const Counter = () => {  const [counter, setCounter] = useState(0);  let interval = null;
  const startTimer = () => {    console.log("startTimer");  };
  const pauseTimer = () => {    console.log("stopTimer");  };
  return (    <>      <div>Counter: {counter}</div>      <button onClick={startTimer}>Start Timer</button>      <button onClick={pauseTimer}>Pause Timer</button>    </>  );};

Increment The Counter On Mount#

Let's first see if we could at least call startTimer when the component is loaded. That's easy to do as we only need to call the function. For now let's put it just above the return statement.

Counter.jsx
startTimer();
return (  // ...rest of code)

So it seems calling startTimer() when the Component loads does indeed execute the function.

Now let's add the logic startTimer that will increment the counter just once when it's mounted.

Counter.jsx
const startTimer = () => {  console.log("startTimer");  setCounter(counter + 1);};

React doesn't like that very much and errors out as it takes us to brink of an infinite loop.

1 of 2 errors on the page

Error

Too many re-renders. React limits the number of renders to prevent an infinite loop

Note: Make sure to delete calling startTimer().

❓ - Why the error?

Answer

Calling startTimer() updates state which triggers the Component to update, which then calls the function again triggering another update..and so on..

ComponentDidMount - Run Once On Mount#

So we need a means of calling startTimer() when the Component is initially mounted and not on any subsequent re-renders. For that use case we can use:

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

Since useEffect is a hook it needs to be imported.

Counter.jsx
import React, { useState, useEffect } from 'react';

useEffect takes in a callback function as it's first param and an empty [] as its second. The empty [] is how we tell useEffect to run only on the initial mount.

Counter.jsx
useEffect(() => {  console.log("useEffect");  startTimer();}, []);

Ok. So now we have a means of calling it a single time. That's enough to to get us started and now we can add the additional logic setInterval logic to increment every second.

This also bring us to the following best practice:

⭐ - Use the callback function version of useState if you need to reference the previous version of state.

Since setCounter is being called in the callback function of setInterval this is a perfect use case for using the callback version of setCounter

Counter.jsx
const startTimer = () => {  console.log("startTimer");  interval = setInterval(() => {    console.log("setInterval");    setCounter((prevState) => prevState + 1);  }, 2000);};
const pauseTimer = () => {  console.log("stopTimer");  clearInterval(interval);};

This seems to do the trick and were back on track with the Counter app. Now let's work out the logic to pause the timer.

Pausing The Timer#

With 2 of the app requirements in working order let's see if we can pause the timer. Clicking on Pause Timer does indeed execute the function, as seen in the console logs, but does nothing to stop the timer.

If we also click on Start Timer a few times in a row we will see that it increments several times and begins to jump ahead.

ComponentWillUnmount#

One thing that hasn't been mentioned yet is that every time a re-render happens, we schedule a new effect replacing the previous effect. Although in our use case useEffect isn't being called there is still a need to clear the interval just before the re-render. This falls ito the category of ComponetWillUnmount

React must clean up the previous effect before applying the next effect. In our case we need to remove the previous instance of the setTimer and create a new instance.

For this we need to refactor useEffect to do the following:

  • im must clear the previous setInterval before the new instance of useEffect is called

To do this we add a final return statement that is passed a callback function.

Counter.jsx
useEffect(() => {  console.log("useEffect");  startTimer();  return () => clearInterval(interval);}, []);

ComponentDidUpdate#

This doesn't seem to do the trick. Thats because we need to run useEffect on every re-render or when the ComponentDidUpdate. In order to do that we remove the [].

Counter.jsx
useEffect(() => {  console.log("useEffect");  startTimer();  return () => clearInterval(interval);});

ClearInterval Once More#

Even with this implementation were still faced with the same problem that multiple clicks to Start Timer will cause the timer count much faster.

In order to resolve this we need to add one more clearInterval to startTimer to clear out any timer that was initiated before the next interval value.

Counter.jsx
const startTimer = () => {  clearInterval(interval);  //...rest of code};

Here is good summary for implementing useEffect.

Once (similar to componentDidMount)#

Counter.jsx
useEffect(() => {  // put 'run once' code here}, []); // pass empty array

On State Change (similar to componentDidUpdate)#

const YourComponent = () => {  const [state, setState] = useState();
  useEffect(() => {    // code to run when state changes  }, [state]); // include all state vars to watch}

On Props Change (similar to componentDidUpdate)#

const YourComponent = ({ someProp }) => {  useEffect(() => {    // code to run when someProp changes  }, [someProp]); // include all monitored props}

On Unmount (similar to componentWillMount)#

useEffect(() => {  // return the cleanup function  return () => {    // put unmount code here  }});

After every render (similar to componentDidUpdate)#

useEffect(() => {  // put 'every update' code here }); // no second argument

Bonus - Conditional Rendering A Component#

A much better UI for our Counter would be to show only 1 button at a time. We essentially would toggle between the buttons with only one of them being visible at any one time.

Enabling this functionality would also require a bit of refactoring of our code which is beyond the scope of this bonus so we will only focus on how to conditionality render the buttons.

More State#

First let's add an additional state value.

const [toggle, setToggle] = useState(false);

Ternary Operator#

Now we can add the conditional logic needed to toggle between the buttons. We will do this using a ternary operator.

Counter.jsx
return (  <>    <div>Counter: {counter}</div>    {toggle ? (      <button onClick={startTimer}>Start Timer</button>    ) : (      <button onClick={pauseTimer}>Pause Timer</button>    )}  </>);

Complete Refactor#

Here is the code that includes the complete refactor.

Solution
Counter.jsx
const Counter = () => {  const [counter, setCounter] = useState(0);  const [toggle, setToggle] = useState(false);
  useEffect(() => {    let interval;    if (!toggle) {      interval = setInterval(() => {        setCounter((prevState) => prevState + 1);      }, 2000);    }    return () => clearInterval(interval);  }, [toggle]);
  const handleToggle = () => {    setToggle(!toggle);  };
  return (    <>      <div>Counter: {counter}</div>      {toggle ? (        <button onClick={handleToggle}>Start Timer</button>      ) : (        <button onClick={handleToggle}>Pause Timer</button>      )}    </>  );};

References#