Understanding the Problem
In this phase, we'll refactor both the Clock and ClockToggle components from class components to function components. This phase introduces an important new challenge: handling both setup and cleanup in the same component lifecycle. Specifically:
- The
ClockTogglecomponent is a simple button that toggles the visibility of theClockcomponent - The
Clockcomponent usescomponentDidMountto set up an interval that updates the time every second - The
Clockcomponent usescomponentWillUnmountto clear the interval when the component is removed from the DOM
This pattern of setting up a resource (the interval) and then cleaning it up is very common in React applications. In function components, both the setup and cleanup can be handled by a single useEffect hook.
George Polya's Problem-Solving Method
Step 1: Understand the Problem
Let's examine the original Clock and ClockToggle class components to understand their functionality:
// Clock.jsx
import React from 'react';
export class ClockToggle extends React.Component {
render () {
return (
<button
type="button"
className="clock-toggle"
onClick={this.props.toggleClock}
>
Toggle Clock
</button>
)
}
}
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {
time: new Date(),
};
}
componentDidMount() {
this.interval = setInterval(this.tick, 1000);
}
componentWillUnmount() {
console.log("Clearing Clock interval!")
clearInterval(this.interval);
}
tick = () => {
this.setState({ time: new Date() });
}
render() {
let hours = this.state.time.getHours();
let minutes = this.state.time.getMinutes();
let seconds = this.state.time.getSeconds();
hours = (hours < 10) ? `0${hours}` : hours;
minutes = (minutes < 10) ? `0${minutes}` : minutes;
seconds = (seconds < 10) ? `0${seconds}` : seconds;
const timezone = this.state.time
.toTimeString() // Form: "14:39:07 GMT-0600 (PDT)"
.replace(/[^A-Z]/g, "") // Strip out all but capitals
.slice(3); // Eliminate initial GMT
return (
<section className="clock-section">
<h1>Clock</h1>
<div className='clock'>
<p className="time">
<span>
Time:
</span>
<span>
{`${hours}:${minutes}:${seconds} ${timezone}`}
</span>
</p>
<p className="date">
<span>
Date:
</span>
<span>
{this.state.time.toDateString()}
</span>
</p>
</div>
</section>
);
}
}
export default Clock;
Analyzing this code, we can see that:
- ClockToggle Component:
- Renders a button with an
onClickhandler (toggleClock), which is received as a prop - This component is relatively simple and straightforward
- Renders a button with an
- Clock Component:
- Has one state variable:
time(initialized to the current date/time) - Uses
componentDidMountto set up an interval that callstickevery second - Uses
componentWillUnmountto clear the interval when the component is unmounted - The
tickmethod updates thetimestate with the current date/time - The
rendermethod formats and displays the current time and date
- Has one state variable:
Step 2: Devise a Plan
We'll refactor both components, starting with the simpler ClockToggle component:
For ClockToggle:
- Convert the class declaration to a function declaration
- Use props destructuring in the function parameters
- Return the JSX directly from the function
For Clock:
- Convert the class declaration to a function declaration
- Replace
this.state.timewith theuseStatehook - Use
useEffectto combinecomponentDidMountandcomponentWillUnmount:- Set up the interval inside the effect function
- Return a cleanup function that clears the interval
- Use an empty dependency array to ensure the effect runs only once
- Extract the
tickfunction into theuseEffectcallback - Update references to
this.state.timein the render logic - Remove the
rendermethod and return JSX directly
Step 3: Carry Out the Plan
1. Refactoring ClockToggle
Let's start with the simpler ClockToggle component:
// From
export class ClockToggle extends React.Component {
render () {
return (
<button
type="button"
className="clock-toggle"
onClick={this.props.toggleClock}
>
Toggle Clock
</button>
)
}
}
// To
export function ClockToggle({ toggleClock }) {
return (
<button
type="button"
className="clock-toggle"
onClick={toggleClock}
>
Toggle Clock
</button>
);
}
This conversion is quite straightforward:
- We changed the class declaration to a function declaration
- We used props destructuring to directly access the
toggleClockprop - We removed the
rendermethod and returned JSX directly - We changed
this.props.toggleClockto justtoggleClock
2. Refactoring Clock: State Management
Now let's convert the Clock component, starting with its state management:
// From
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {
time: new Date(),
};
}
// ...
}
// To
function Clock() {
const [time, setTime] = useState(new Date());
// ...
}
Don't forget to import the necessary hooks:
import React, { useState, useEffect } from 'react';
3. Refactoring Clock: Interval and Cleanup
The most challenging part of this refactoring is handling the interval setup (componentDidMount) and cleanup (componentWillUnmount) with useEffect:
// From
componentDidMount() {
this.interval = setInterval(this.tick, 1000);
}
componentWillUnmount() {
console.log("Clearing Clock interval!")
clearInterval(this.interval);
}
tick = () => {
this.setState({ time: new Date() });
}
// To
useEffect(() => {
// Setup interval (similar to componentDidMount)
const interval = setInterval(() => {
setTime(new Date());
}, 1000);
// Return cleanup function (similar to componentWillUnmount)
return () => {
console.log("Clearing Clock interval!");
clearInterval(interval);
};
}, []); // Empty dependency array means run once after mount
Let's break down this transformation:
- We created a
useEffecthook that runs once after the component mounts (due to the empty dependency array[]) - Inside the effect, we set up the interval using
setInterval - Instead of a separate
tickmethod, we directly update the state withsetTime(new Date())inside the interval callback - We return a cleanup function that clears the interval, which React will call when the component unmounts
- The
intervalis now a local variable inside the effect, not an instance property
4. Refactoring Clock: Render Logic
Finally, we'll update the render logic to use our time state variable directly:
// Format time values
let hours = time.getHours();
let minutes = time.getMinutes();
let seconds = time.getSeconds();
hours = (hours < 10) ? `0${hours}` : hours;
minutes = (minutes < 10) ? `0${minutes}` : minutes;
seconds = (seconds < 10) ? `0${seconds}` : seconds;
const timezone = time
.toTimeString() // Form: "14:39:07 GMT-0600 (PDT)"
.replace(/[^A-Z]/g, "") // Strip out all but capitals
.slice(3); // Eliminate initial GMT
// Return what was previously in the render method
return (
<section className="clock-section">
<h1>Clock</h1>
<div className='clock'>
<p className="time">
<span>
Time:
</span>
<span>
{`${hours}:${minutes}:${seconds} ${timezone}`}
</span>
</p>
<p className="date">
<span>
Date:
</span>
<span>
{time.toDateString()}
</span>
</p>
</div>
</section>
);
The main changes here are:
- We changed
this.state.timeto justtime - We removed the
rendermethod and returned JSX directly
5. Putting It All Together
Here's the complete refactored file with both components:
// Clock.jsx (Refactored)
import React, { useState, useEffect } from 'react';
// Convert ClockToggle to function component
export function ClockToggle({ toggleClock }) {
return (
<button
type="button"
className="clock-toggle"
onClick={toggleClock}
>
Toggle Clock
</button>
);
}
// Convert Clock to function component
function Clock() {
// Replace this.state with useState
const [time, setTime] = useState(new Date());
// Replace componentDidMount and componentWillUnmount with useEffect
useEffect(() => {
// Setup interval (like componentDidMount)
const interval = setInterval(() => {
setTime(new Date());
}, 1000);
// Return cleanup function (like componentWillUnmount)
return () => {
console.log("Clearing Clock interval!");
clearInterval(interval);
};
}, []); // Empty dependency array means this runs once on mount
// Format time values
let hours = time.getHours();
let minutes = time.getMinutes();
let seconds = time.getSeconds();
hours = (hours < 10) ? `0${hours}` : hours;
minutes = (minutes < 10) ? `0${minutes}` : minutes;
seconds = (seconds < 10) ? `0${seconds}` : seconds;
const timezone = time
.toTimeString() // Form: "14:39:07 GMT-0600 (PDT)"
.replace(/[^A-Z]/g, "") // Strip out all but capitals
.slice(3); // Eliminate initial GMT
// Return what was previously in the render method
return (
<section className="clock-section">
<h1>Clock</h1>
<div className='clock'>
<p className="time">
<span>
Time:
</span>
<span>
{`${hours}:${minutes}:${seconds} ${timezone}`}
</span>
</p>
<p className="date">
<span>
Date:
</span>
<span>
{time.toDateString()}
</span>
</p>
</div>
</section>
);
}
export default Clock;
Step 4: Look Back and Review
Let's verify that our refactored components maintain the same functionality as the originals:
- ClockToggle Component:
- Still renders a button with the "Toggle Clock" text
- Still calls the
toggleClockprop when clicked
- Clock Component:
- Still initializes the
timestate to the current date/time - Still sets up an interval that updates the time every second
- Still cleans up the interval when the component unmounts
- Still formats and displays the time and date correctly
- Still initializes the
Let's run the tests to confirm that everything works as expected:
npm test
The tests for the Clock and ClockToggle components should pass, confirming that our refactoring was successful.
Understanding useEffect Cleanup
The Importance of Cleanup
One of the most important aspects of this refactoring is the cleanup function returned from useEffect. In React, failing to clean up things like intervals, event listeners, or subscriptions can lead to memory leaks and unexpected behavior. Let's understand how cleanup works in both class and function components.
In Class Components:
Class components handle cleanup in the componentWillUnmount lifecycle method, which is called just before the component is removed from the DOM:
class Example extends React.Component {
componentDidMount() {
// Setup stuff (intervals, subscriptions, etc.)
}
componentWillUnmount() {
// Clean up stuff
}
}
In Function Components:
Function components handle cleanup by returning a function from the useEffect hook:
function Example() {
useEffect(() => {
// Setup stuff
// Return a cleanup function
return () => {
// Clean up stuff
};
}, [])
}
When Does Cleanup Run?
The cleanup function returned from useEffect runs in two scenarios:
- Before the component unmounts: This is similar to
componentWillUnmountin class components - Before the effect runs again: If the effect depends on values that change, the cleanup function runs before the effect re-runs with new values
In our Clock component, we're only concerned with the first scenario because we're using an empty dependency array, which means the effect only runs once when the component mounts.
Visualizing the Component Lifecycle
To better understand how the cleanup function works in the context of the component lifecycle, consider this sequence of events:
- Initial render: The component function runs and the initial JSX is rendered
- Effect execution: After the render is committed to the screen, the
useEffectcallback runs- The interval is set up
- The cleanup function is stored for later use
- State updates: Every second, the interval callback runs, calling
setTime - Re-renders: Each state update triggers a re-render, but the effect doesn't run again (due to the empty dependency array)
- Unmounting: When the component is about to be removed from the DOM, React calls the cleanup function
- The interval is cleared
- No more state updates will occur
This process ensures that resources are properly managed throughout the component's lifecycle.
What Can Be Cleaned Up?
The cleanup pattern we've seen with the interval is applicable to many other resources that need to be released when a component is no longer needed:
Common Cleanup Scenarios
- Timers and intervals: Clear them with
clearTimeoutorclearInterval - Event listeners: Remove them with
removeEventListener - Subscriptions: Unsubscribe from observables, WebSockets, etc.
- Async operations: Use a flag to prevent setting state after unmount
- WebRTC connections: Close media streams and connections
- Third-party libraries: Invoke their cleanup methods when provided
Event Listener Example
Here's an example of cleaning up an event listener:
function WindowResizeListener() {
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
useEffect(() => {
// Setup function for the event listener
const handleResize = () => {
setWindowWidth(window.innerWidth);
};
// Add the event listener
window.addEventListener('resize', handleResize);
// Return cleanup function
return () => {
// Remove the event listener
window.removeEventListener('resize', handleResize);
};
}, []); // Empty dependency array
return <div>Window width: {windowWidth}px</div>;
}
Subscription Example
Here's an example of cleaning up a subscription:
function DataSubscriber() {
const [data, setData] = useState(null);
useEffect(() => {
// Create a subscription
const subscription = dataSource.subscribe({
next: (newData) => setData(newData),
error: (error) => console.error(error)
});
// Return cleanup function
return () => {
// Unsubscribe to prevent memory leaks
subscription.unsubscribe();
};
}, []); // Empty dependency array
// Render with the data
return <div>{data ? `Data: ${data}` : 'Loading...'}</div>;
}
Preventing Race Conditions in Async Operations
For async operations that may complete after the component has unmounted, you need to prevent setting state:
function AsyncDataFetcher() {
const [data, setData] = useState(null);
useEffect(() => {
// Flag to track if the component is still mounted
let isMounted = true;
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/data');
const result = await response.json();
// Only update state if the component is still mounted
if (isMounted) {
setData(result);
}
} catch (error) {
if (isMounted) {
console.error('Error fetching data:', error);
}
}
};
fetchData();
// Return cleanup function
return () => {
// Set the flag to false when the component unmounts
isMounted = false;
};
}, []); // Empty dependency array
return <div>{data ? `Data: ${JSON.stringify(data)}` : 'Loading...'}</div>;
}
Real-World Applications
When to Use This Pattern
The pattern of setting up and cleaning up resources is fundamental to building robust React applications. It's used in many common scenarios:
- Live updates: Tickers, counters, clocks, and other components that update at regular intervals
- Event-based interactions: Components that listen for global events like keyboard shortcuts, window resizing, or device orientation changes
- Real-time data: Chat applications, live dashboards, or any component that subscribes to a data stream
- Location tracking: Maps and geolocation features that need to watch position changes
- Media playback: Audio or video players that need to manage media resources
- Animation libraries: Integrations with libraries that need to be initialized and shut down properly
Best Practices
- Always clean up: Make it a habit to include cleanup for any setup operation
- Bundle related setup and cleanup: Keep them in the same effect for clarity
- Be specific with dependencies: Only include the values that should trigger a re-run of the effect
- Handle edge cases: Consider what happens if dependencies change rapidly or if async operations complete out of order
- Separate concerns: Use multiple
useEffecthooks for unrelated functionality
Comparing Class and Function Component Cleanup
Class Component Approach
class Timer extends React.Component {
constructor(props) {
super(props);
this.state = { seconds: 0 };
}
componentDidMount() {
this.interval = setInterval(() => {
this.setState(prevState => ({
seconds: prevState.seconds + 1
}));
}, 1000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
return <div>Seconds: {this.state.seconds}</div>;
}
}
Function Component Approach
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(prevSeconds => prevSeconds + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
return <div>Seconds: {seconds}</div>;
}
Key Differences
| Aspect | Class Components | Function Components |
|---|---|---|
| Setup location | componentDidMount |
useEffect callback |
| Cleanup location | componentWillUnmount |
Function returned from useEffect |
| Resource reference | Stored as instance property (this.interval) |
Stored in function closure (interval) |
| Conditional setup/cleanup | Requires explicit conditions in methods | Can use multiple effects with different dependencies |
| Code organization | Setup and cleanup in separate methods | Setup and cleanup kept together in one effect |
Advantages of the Function Component Approach
- Colocation: Setup and cleanup are kept together, making their relationship clear
- No this binding: Eliminates a common source of bugs in React
- Flexibility: Can have multiple effects with different dependencies
- Isolation: Resources are scoped to the effect that creates them
- Reusability: Effect logic can be extracted into custom hooks