The Component LifeCycles
#
Prerequisites- React
- Components
- State and props
#
Learning ObjectivesAfter 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
#
FramingSo 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 - 5minLet'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 LifecycleAs 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)
useEffect
Hook#
The 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 mountsuseEffect(() => {})
- no [] means this will run on every render/re-renderuseEffect(() => {}, [someValueToMonitor])
- run on mount and then only if the value has changed
#
⏰ Activity - 1min#
Starter CodeHere 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.
#
MountingSince 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 FunctionalityAs of right now our implementation only contains the console logs needed to confirm that the buttons work.
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 MountLet'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.
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.
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 MountSo 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.
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.
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
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 TimerWith 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.
#
ComponentWillUnmountOne 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 ofuseEffect
is called
To do this we add a final return
statement that is passed a callback function.
useEffect(() => { console.log("useEffect"); startTimer(); return () => clearInterval(interval);}, []);
#
ComponentDidUpdateThis 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 []
.
useEffect(() => { console.log("useEffect"); startTimer(); return () => clearInterval(interval);});
#
ClearInterval Once MoreEven 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.
const startTimer = () => { clearInterval(interval); //...rest of code};
Here is good summary for implementing useEffect
.
#
Once (similar to componentDidMount)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 ComponentA 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 StateFirst let's add an additional state value.
const [toggle, setToggle] = useState(false);
#
Ternary OperatorNow we can add the conditional logic needed to toggle between the buttons. We will do this using a ternary operator
.
return ( <> <div>Counter: {counter}</div> {toggle ? ( <button onClick={startTimer}>Start Timer</button> ) : ( <button onClick={pauseTimer}>Pause Timer</button> )} </>);
#
Complete RefactorHere is the code that includes the complete refactor.
Solution
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> )} </> );};