Functional and Class Components

Understanding the building blocks of React applications

Components: The Heart of React

Components are the fundamental building blocks of React applications. They encapsulate a piece of the UI, along with its functionality and state, into reusable, self-contained units.

graph TD A[React Application] --> B[Components] B --> C[Functional Components] B --> D[Class Components] C --> E[Hooks for State & Lifecycle] D --> F[Built-in Lifecycle Methods] B --> G[Props] B --> H[State] B --> I[Composition]

Analogy: Components as LEGO Blocks

Think of React components like LEGO blocks:

  • Reusable: The same component can be used in different parts of your application, just like the same LEGO piece can be used in different models.
  • Self-Contained: Each component manages its own rendering logic, just like each LEGO piece has its own shape and purpose.
  • Composable: Complex UIs are built by combining simpler components, just like complex LEGO models are built from simpler blocks.
  • Hierarchical: Components can be nested inside other components, forming a tree structure, just like LEGO blocks can be stacked and nested.

Two Types of Components

React provides two primary ways to define components: Functional Components and Class Components. Both serve the same purpose but with different syntax and capabilities.

Functional Component • Simple JavaScript function • Accepts props as parameter • Returns JSX • Uses Hooks for state & effects Class Component • ES6 class extending React.Component • Accesses props via this.props • Has render() method returning JSX • Built-in lifecycle methods

Functional Components

Functional components are JavaScript functions that accept props as an argument and return React elements (JSX). They're also known as "stateless components" (though with Hooks, they can now have state).

Basic Structure

// Basic functional component
function Greeting(props) {
  return <h1>Hello, {props.name}!</h1>;
}

// Using arrow function syntax
const Greeting = (props) => {
  return <h1>Hello, {props.name}!</h1>;
};

// Shortened arrow function with implicit return
const Greeting = (props) => <h1>Hello, {props.name}!</h1>;

With Destructured Props

// Destructuring props in the parameter
function ProfileCard({ name, title, avatar }) {
  return (
    <div className="profile-card">
      <img src={avatar} alt={name} />
      <h2>{name}</h2>
      <p>{title}</p>
    </div>
  );
}

With Default Props

// Providing default values for props
function Button({ text = "Click me", type = "primary", onClick }) {
  return (
    <button 
      className={`btn btn-${type}`} 
      onClick={onClick}
    >
      {text}
    </button>
  );
}

With State (Using Hooks)

import React, { useState } from 'react';

function Counter() {
  // useState returns a state value and a function to update it
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

When to Use Functional Components

Functional components are now recommended as the default choice for most cases in React. They offer:

  • Simplicity: Cleaner, more concise syntax
  • Performance: Potentially better performance (though differences are minimal)
  • Hooks: Full access to React's hooks system
  • Future-Proof: Aligned with React team's recommended patterns

Class Components

Class components are ES6 classes that extend React.Component. They have more built-in features like lifecycle methods and were the standard before Hooks were introduced.

Basic Structure

import React, { Component } from 'react';

class Greeting extends Component {
  render() {
    return <h1>Hello, {this.props.name}!</h1>;
  }
}

With State

import React, { Component } from 'react';

class Counter extends Component {
  constructor(props) {
    super(props);
    // Initialize state in constructor
    this.state = {
      count: 0
    };
    
    // Bind the event handler
    this.handleClick = this.handleClick.bind(this);
  }
  
  handleClick() {
    // Update state using setState
    this.setState(prevState => ({
      count: prevState.count + 1
    }));
  }
  
  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={this.handleClick}>
          Click me
        </button>
      </div>
    );
  }
}

With Lifecycle Methods

import React, { Component } from 'react';

class UserProfile extends Component {
  constructor(props) {
    super(props);
    this.state = {
      user: null,
      loading: true,
      error: null
    };
  }
  
  componentDidMount() {
    // Fetch data when component mounts
    fetch(`https://api.example.com/users/${this.props.userId}`)
      .then(response => response.json())
      .then(data => {
        this.setState({ 
          user: data, 
          loading: false 
        });
      })
      .catch(error => {
        this.setState({ 
          error: error.message, 
          loading: false 
        });
      });
  }
  
  componentDidUpdate(prevProps) {
    // Refetch if userId prop changes
    if (prevProps.userId !== this.props.userId) {
      this.setState({ loading: true });
      fetch(`https://api.example.com/users/${this.props.userId}`)
        .then(response => response.json())
        .then(data => {
          this.setState({ 
            user: data, 
            loading: false 
          });
        })
        .catch(error => {
          this.setState({ 
            error: error.message, 
            loading: false 
          });
        });
    }
  }
  
  componentWillUnmount() {
    // Clean up code here (e.g., cancel subscriptions)
    console.log('Component is unmounting');
  }
  
  render() {
    const { user, loading, error } = this.state;
    
    if (loading) return <div>Loading...</div>;
    if (error) return <div>Error: {error}</div>;
    if (!user) return <div>No user found</div>;
    
    return (
      <div className="user-profile">
        <h2>{user.name}</h2>
        <p>Email: {user.email}</p>
      </div>
    );
  }
}

When to Use Class Components

While functional components are now preferred, class components are still relevant in certain scenarios:

  • Legacy Code: Understanding class components is essential for maintaining older React codebases
  • Error Boundaries: Currently, only class components can use componentDidCatch for error boundaries
  • Complex Lifecycle Logic: Some advanced patterns might be more explicit with lifecycle methods
  • Familiar to OOP Developers: Might be more intuitive for developers with strong OOP backgrounds

Comparing Functional and Class Components

Feature Functional Components Class Components
Syntax Simple JavaScript function ES6 class extending React.Component
Props Received as function arguments Accessed via this.props
State useState Hook this.state and this.setState()
Lifecycle useEffect Hook Dedicated lifecycle methods
Context useContext Hook static contextType or Context.Consumer
Performance Slightly better in most cases Slightly more overhead
Code Size Usually more concise Usually more verbose
Error Boundaries Not supported Supported with componentDidCatch

Converting Between Component Types

Class Component

class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

Equivalent Functional Component

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

Class Component with State

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }
  
  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Increment
        </button>
      </div>
    );
  }
}

Equivalent Functional Component with useState

function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  );
}

Component Lifecycle and Hooks Equivalence

Understanding how class component lifecycle methods map to functional component hooks can ease the transition between the two paradigms.

graph TD subgraph "Class Component Lifecycle" A[constructor] --> B[render] B --> C[componentDidMount] C --> D[componentDidUpdate] D --> E[componentWillUnmount] end subgraph "Functional Component Hooks" F[useState initial value] --> G[JSX return] G --> H["useEffect(() => {}, [])"] H --> I["useEffect(() => {}, [dependency])"] I --> J["useEffect(() => { return cleanup })"] end A -.-> F B -.-> G C -.-> H D -.-> I E -.-> J

Lifecycle Methods and their Hook Equivalents

Class Lifecycle Method Hooks Equivalent
constructor(props) useState(initialState)
componentDidMount() useEffect(() => {}, [])
componentDidUpdate(prevProps, prevState) useEffect(() => {}, [dependencies])
componentWillUnmount() useEffect(() => { return () => {} }, [])
shouldComponentUpdate(nextProps, nextState) React.memo, useMemo, useCallback
static getDerivedStateFromProps(props, state) Update state during render using updated props
getSnapshotBeforeUpdate(prevProps, prevState) No direct equivalent
componentDidCatch(error, info) Currently no hook equivalent

Practical Example: Converting a Class Component to a Functional Component

Let's take a more complex class component and convert it to a functional component with hooks:

Class Component Version

import React, { Component } from 'react';

class WeatherWidget extends Component {
  constructor(props) {
    super(props);
    this.state = {
      temperature: null,
      conditions: null,
      loading: true,
      error: null,
      city: props.city || 'New York'
    };
    
    this.updateCity = this.updateCity.bind(this);
  }
  
  componentDidMount() {
    this.fetchWeatherData();
  }
  
  componentDidUpdate(prevProps, prevState) {
    if (prevState.city !== this.state.city) {
      this.fetchWeatherData();
    }
  }
  
  fetchWeatherData() {
    this.setState({ loading: true });
    
    fetch(`https://api.weather.example.com/current?city=${this.state.city}`)
      .then(response => {
        if (!response.ok) {
          throw new Error('Weather data not available');
        }
        return response.json();
      })
      .then(data => {
        this.setState({
          temperature: data.temperature,
          conditions: data.conditions,
          loading: false,
          error: null
        });
      })
      .catch(error => {
        this.setState({
          loading: false,
          error: error.message
        });
      });
  }
  
  updateCity(newCity) {
    this.setState({ city: newCity });
  }
  
  render() {
    const { temperature, conditions, loading, error, city } = this.state;
    
    if (loading) {
      return <div className="weather-widget loading">Loading weather data...</div>;
    }
    
    if (error) {
      return <div className="weather-widget error">Error: {error}</div>;
    }
    
    return (
      <div className="weather-widget">
        <h2>Weather in {city}</h2>
        <div className="temperature">{temperature}°F</div>
        <div className="conditions">{conditions}</div>
        
        <div className="city-selector">
          <label>Change city:</label>
          <select 
            value={city} 
            onChange={(e) => this.updateCity(e.target.value)}
          >
            <option value="New York">New York</option>
            <option value="London">London</option>
            <option value="Tokyo">Tokyo</option>
            <option value="Sydney">Sydney</option>
          </select>
        </div>
      </div>
    );
  }
}

Functional Component Version

import React, { useState, useEffect } from 'react';

function WeatherWidget({ city: initialCity = 'New York' }) {
  // State with useState hooks
  const [temperature, setTemperature] = useState(null);
  const [conditions, setConditions] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [city, setCity] = useState(initialCity);
  
  // useEffect for data fetching (combines componentDidMount & componentDidUpdate)
  useEffect(() => {
    async function fetchWeatherData() {
      setLoading(true);
      
      try {
        const response = await fetch(`https://api.weather.example.com/current?city=${city}`);
        if (!response.ok) {
          throw new Error('Weather data not available');
        }
        
        const data = await response.json();
        setTemperature(data.temperature);
        setConditions(data.conditions);
        setError(null);
      } catch (error) {
        setError(error.message);
      } finally {
        setLoading(false);
      }
    }
    
    fetchWeatherData();
  }, [city]); // Dependency array - effect runs when city changes
  
  // Event handler function
  const updateCity = (newCity) => {
    setCity(newCity);
  };
  
  // Conditional rendering
  if (loading) {
    return <div className="weather-widget loading">Loading weather data...</div>;
  }
  
  if (error) {
    return <div className="weather-widget error">Error: {error}</div>;
  }
  
  return (
    <div className="weather-widget">
      <h2>Weather in {city}</h2>
      <div className="temperature">{temperature}°F</div>
      <div className="conditions">{conditions}</div>
      
      <div className="city-selector">
        <label>Change city:</label>
        <select 
          value={city} 
          onChange={(e) => updateCity(e.target.value)}
        >
          <option value="New York">New York</option>
          <option value="London">London</option>
          <option value="Tokyo">Tokyo</option>
          <option value="Sydney">Sydney</option>
        </select>
      </div>
    </div>
  );
}

Key Points in the Conversion

Real-World Component Patterns

Controlled Components

// Functional Controlled Component
function SearchInput({ value, onChange }) {
  return (
    <input
      type="text"
      value={value}
      onChange={onChange}
      placeholder="Search..."
      className="search-input"
    />
  );
}

// Using the controlled component
function SearchBar() {
  const [query, setQuery] = useState('');
  
  const handleQueryChange = (e) => {
    setQuery(e.target.value);
  };
  
  return (
    <div className="search-bar">
      <SearchInput 
        value={query} 
        onChange={handleQueryChange} 
      />
      <button>Search</button>
    </div>
  );
}

Data Fetching Component

// Reusable data fetching component
function DataFetcher({ url, children, loadingComponent, errorComponent }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    async function fetchData() {
      try {
        setLoading(true);
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error(`HTTP error! Status: ${response.status}`);
        }
        const result = await response.json();
        setData(result);
        setError(null);
      } catch (err) {
        setError(err.message);
        setData(null);
      } finally {
        setLoading(false);
      }
    }
    
    fetchData();
  }, [url]);
  
  if (loading) {
    return loadingComponent || <div>Loading...</div>;
  }
  
  if (error) {
    return errorComponent || <div>Error: {error}</div>;
  }
  
  return children(data);
}

// Using the data fetcher component
function UserList() {
  return (
    <DataFetcher
      url="https://api.example.com/users"
      loadingComponent={<div className="spinner">Loading users...</div>}
      errorComponent={<div className="error-box">Failed to load users</div>}
    >
      {(users) => (
        <ul className="user-list">
          {users.map(user => (
            <li key={user.id}>{user.name}</li>
          ))}
        </ul>
      )}
    </DataFetcher>
  );
}

Higher-Order Component (HOC) Pattern

// Higher-Order Component example
function withLogger(WrappedComponent) {
  // Return a component
  return function WithLogger(props) {
    useEffect(() => {
      console.log(`Component ${WrappedComponent.name} mounted with props:`, props);
      
      return () => {
        console.log(`Component ${WrappedComponent.name} will unmount`);
      };
    }, []);
    
    console.log(`Rendering ${WrappedComponent.name} with props:`, props);
    
    // Pass through all props to the wrapped component
    return <WrappedComponent {...props} />;
  };
}

// Using the HOC
const Button = ({ text, onClick }) => (
  <button onClick={onClick}>{text}</button>
);

const LoggedButton = withLogger(Button);

// Usage
function App() {
  return (
    <LoggedButton 
      text="Click Me" 
      onClick={() => console.log('Button clicked')} 
    />
  );
}

Component Naming Conventions

Following consistent naming conventions makes your code more readable and maintainable:

File Organization Strategies

There are several approaches to organizing React component files:

  • By Feature/Module: Group related components by feature
  • By Component Type: Separate containers, components, hooks, etc.
  • Atomic Design: Organize by complexity (atoms, molecules, organisms, templates, pages)

Whichever approach you choose, consistency is key!

Error Boundaries with Class Components

Error boundaries are a React pattern that uses class components to catch JavaScript errors in their child component tree and display fallback UI.

import React, { Component } from 'react';

class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null, errorInfo: null };
  }
  
  static getDerivedStateFromError(error) {
    // Update state so the next render will show the fallback UI
    return { hasError: true };
  }
  
  componentDidCatch(error, errorInfo) {
    // Log the error to an error reporting service
    console.error("Error caught by boundary:", error, errorInfo);
    this.setState({
      error: error,
      errorInfo: errorInfo
    });
  }
  
  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return (
        <div className="error-boundary">
          <h2>Something went wrong.</h2>
          {this.props.fallback || <button onClick={() => window.location.reload()}>Refresh Page</button>}
          {process.env.NODE_ENV !== 'production' && (
            <details style={{ whiteSpace: 'pre-wrap' }}>
              {this.state.error && this.state.error.toString()}
              <br />
              {this.state.errorInfo && this.state.errorInfo.componentStack}
            </details>
          )}
        </div>
      );
    }
    
    return this.props.children;
  }
}

// Using the error boundary
function App() {
  return (
    <div className="app">
      <h1>My Application</h1>
      
      <ErrorBoundary fallback={<p>User profile failed to load.</p>}>
        <UserProfile userId="123" />
      </ErrorBoundary>
      
      <ErrorBoundary fallback={<p>Weather widget failed to load.</p>}>
        <WeatherWidget city="New York" />
      </ErrorBoundary>
    </div>
  );
}

Error boundaries are an important example of functionality that still requires class components in React.

Practice Activities

Activity 1: Component Conversion

Objective: Practice converting between functional and class components.

Instructions:

  1. Convert the following class component to a functional component using hooks:
class ToggleButton extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      isOn: false
    };
    this.toggle = this.toggle.bind(this);
  }
  
  toggle() {
    this.setState(prevState => ({
      isOn: !prevState.isOn
    }));
  }
  
  render() {
    return (
      <button 
        className={this.state.isOn ? 'btn-on' : 'btn-off'}
        onClick={this.toggle}
      >
        {this.state.isOn ? 'ON' : 'OFF'}
      </button>
    );
  }
}

Activity 2: Building Components from Scratch

Objective: Practice creating functional components for different use cases.

Instructions:

  1. Create a UserProfile functional component that displays a user's details (name, role, email, avatar)
  2. Create a TabPanel component that takes an array of tabs and displays the selected tab's content
  3. Create a ThemeToggle component that switches between light and dark themes

Implement each component in both functional and class styles, and compare the code.

Activity 3: Error Boundary in Action

Objective: Experience how error boundaries work in practice.

Instructions:

  1. Implement the ErrorBoundary class component from the example
  2. Create a BuggyCounter component that throws an error after a certain count
  3. Wrap the BuggyCounter in your ErrorBoundary
  4. Test how the error boundary catches errors and displays fallback UI
// Example BuggyCounter component
function BuggyCounter() {
  const [count, setCount] = useState(0);
  
  function handleClick() {
    const newCount = count + 1;
    setCount(newCount);
    
    // Simulate an error at count 5
    if (newCount === 5) {
      throw new Error('I crashed on purpose!');
    }
  }
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
    </div>
  );
}

Resources for Further Learning

Official Documentation:

Online Learning Resources:

Community Resources:

Summary

In the next lecture, we'll dive deeper into props and explore data flow between React components.