Phase 4: Clock Component

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:

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:

Step 2: Devise a Plan

We'll refactor both components, starting with the simpler ClockToggle component:

For ClockToggle:

  1. Convert the class declaration to a function declaration
  2. Use props destructuring in the function parameters
  3. Return the JSX directly from the function

For Clock:

  1. Convert the class declaration to a function declaration
  2. Replace this.state.time with the useState hook
  3. Use useEffect to combine componentDidMount and componentWillUnmount:
    • 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
  4. Extract the tick function into the useEffect callback
  5. Update references to this.state.time in the render logic
  6. Remove the render method 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:

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:

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:

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:

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:

  1. Before the component unmounts: This is similar to componentWillUnmount in class components
  2. 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:

  1. Initial render: The component function runs and the initial JSX is rendered
  2. Effect execution: After the render is committed to the screen, the useEffect callback runs
    • The interval is set up
    • The cleanup function is stored for later use
  3. State updates: Every second, the interval callback runs, calling setTime
  4. Re-renders: Each state update triggers a re-render, but the effect doesn't run again (due to the empty dependency array)
  5. 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

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:

Best Practices

  1. Always clean up: Make it a habit to include cleanup for any setup operation
  2. Bundle related setup and cleanup: Keep them in the same effect for clarity
  3. Be specific with dependencies: Only include the values that should trigger a re-run of the effect
  4. Handle edge cases: Consider what happens if dependencies change rapidly or if async operations complete out of order
  5. Separate concerns: Use multiple useEffect hooks 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