AvnishYadav
WorkProjectsBlogsNewsletterSupportAbout
Work With Me

Avnish Yadav

Engineer. Automate. Build. Scale.

© 2026 Avnish Yadav. All rights reserved.

The Automation Update

AI agents, automation, and micro-SaaS. Weekly.

Explore

  • Home
  • Projects
  • Blogs
  • Newsletter Archive
  • About
  • Contact
  • Support

Legal

  • Privacy Policy

Connect

LinkedInGitHubInstagramYouTube
React Architecture: Mastering State and Props in Class vs. Functional Components
2026-02-21

React Architecture: Mastering State and Props in Class vs. Functional Components

7 min readDevelopmentReactEngineeringJavaScriptWeb DevelopmentReactFrontendHooks

Compare React's legacy class components with modern functional hooks by building a state-managed counter app in both paradigms.

The Shift in Mental Models

If you started using React after 2019, you might view Class components as artifacts of a bygone era. If you started before then, you remember the boilerplate heavy lifting required just to increment a number. As an automation engineer, I look at code through the lens of efficiency and maintainability. The transition from Class components to Functional components wasn't just a syntax update; it was a fundamental shift in the mental model of how we handle UI state.

Understanding both paradigms is not optional for a senior developer. You will encounter legacy codebases, and you will need to refactor them. Moreover, understanding how React managed state in classes gives you a profound appreciation for the closure-based architecture of Hooks.

In this build, we are going to strip away the complex business logic and focus purely on the mechanics. We will build a Smart Counter Application. We will build it twice: once using the Object-Oriented approach (Classes) and once using the Functional approach (Hooks). We will dissect how Props (data input) and State (internal memory) behave in both.


Part 1: The Props Paradigm

Props (properties) are the mechanism for passing data down the component tree. Regardless of the component type, props are read-only (immutable). The component receives them, but cannot change them. However, how we access them differs.

Class Components: this.props

In a class component, props are accessed via the instance of the class using the this keyword. This relies heavily on the context of the object instance.

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

The subtle danger here lies in the lifecycle. If this.props changes over time, the render method captures the current value. However, inside asynchronous callbacks, this.props might have changed by the time the callback executes, leading to race conditions.

Functional Components: Argument Destructuring

Functional components represent a purer form of UI-as-a-function. Props are simply the first argument passed to the function.

const Welcome = ({ name }) => {  return <h1>Hello, {name}</h1>;};

Here, the closure captures the render-time value. This makes functional components inherently more predictable in asynchronous flows. There is no this context to worry about.


Part 2: The State Architecture

This is where the divergence is most apparent. State allows a component to change its output over time in response to user actions.

The Class Approach: One Object to Rule Them All

In classes, state is a single object. You initialize it in the constructor, and you update it using this.setState(). Crucially, setState performs a shallow merge.

// Initializingthis.state = {  count: 0,  status: 'active'};// Updatingthis.setState({ count: this.state.count + 1 });// 'status' remains 'active' automatically

The Functional Approach: Independent State Slices

With the useState hook, we don't typically use a single object (though we can). We slice state into independent variables.

const [count, setCount] = useState(0);const [status, setStatus] = useState('active');// UpdatingsetCount(prev => prev + 1);

Builder Note: The useState updater function does not automatically merge objects. If you use an object in useState, you must manually spread the previous state: setState(prev => ({ ...prev, count: prev.count + 1 })).


The Build: Smart Counter (Class Version)

Let's build a counter that accepts a startValue via props and manages the count via state.

import React, { Component } from 'react';class CounterClass extends Component {  constructor(props) {    super(props);    // 1. Initialization    this.state = {      count: props.startValue || 0,      lastAction: 'none'    };    // 2. Binding methods (The "this" headache)    this.increment = this.increment.bind(this);    this.decrement = this.decrement.bind(this);  }  increment() {    // 3. Merging state    this.setState((prevState) => ({      count: prevState.count + 1,      lastAction: 'increment'    }));  }  decrement() {    this.setState((prevState) => ({      count: prevState.count - 1,      lastAction: 'decrement'    }));  }  render() {    return (      <div className="p-6 border rounded-lg shadow-sm">        <h3>Class Counter</h3>        <p>Start Value (Prop): {this.props.startValue}</p>        <div className="text-4xl font-bold my-4">          {this.state.count}        </div>        <p className="text-gray-500 mb-4">Last Action: {this.state.lastAction}</p>        <div className="flex gap-2">          <button onClick={this.decrement} className="px-4 py-2 bg-red-500 text-white rounded">-</button>          <button onClick={this.increment} className="px-4 py-2 bg-blue-500 text-white rounded">+</button>        </div>      </div>    );  }}export default CounterClass;

Analysis of the Class Build

  1. Boilerplate: Look at the constructor. We have to call super(props) just to access this.props.
  2. Binding: If we didn't bind this.increment in the constructor (or use class field arrow functions), this would be undefined when the button is clicked. This was the source of confusion for thousands of React developers for years.
  3. Verbosity: The logic is spread across the constructor, the methods, and the render function.

The Build: Smart Counter (Functional Version)

Now, let's refactor this into a modern functional component using Hooks.

import React, { useState } from 'react';const CounterFunctional = ({ startValue = 0 }) => {  // 1. Isolated State Slices  const [count, setCount] = useState(startValue);  const [lastAction, setLastAction] = useState('none');  const increment = () => {    setCount(prev => prev + 1);    setLastAction('increment');  };  const decrement = () => {    setCount(prev => prev - 1);    setLastAction('decrement');  };  return (    <div className="p-6 border rounded-lg shadow-sm">      <h3>Functional Counter</h3>      <p>Start Value (Prop): {startValue}</p>      <div className="text-4xl font-bold my-4">        {count}      </div>      <p className="text-gray-500 mb-4">Last Action: {lastAction}</p>      <div className="flex gap-2">        <button onClick={decrement} className="px-4 py-2 bg-red-500 text-white rounded">-</button>        <button onClick={increment} className="px-4 py-2 bg-blue-500 text-white rounded">+</button>      </div>    </div>  );};export default CounterFunctional;

Analysis of the Functional Build

  1. No Constructor: State initialization happens inline. Code is read top-to-bottom.
  2. No Binding: Because increment is a function defined inside the component scope, it inherently has access to the component's scope. No this binding required.
  3. Destructuring Props: We pull startValue directly from the function arguments, setting a default value cleanly.
  4. Separation of Concerns: We separated count and lastAction. This allows the React runtime to optimize updates better than a single large object, although we could group them if they were tightly coupled.

Deep Dive: The "Gotchas"

While the functional version looks cleaner, there are engineering nuances you must respect.

1. Stale Closures

In Class components, this.state.count always points to the latest instance. In Functional components, functions capture values at the time they were created. If you use setTimeout inside the functional counter, it might print an old value of count unless you use a Ref or proper dependency arrays in useEffect.

2. The Merge vs. Replace

As mentioned earlier, this.setState merges. useState replaces. If you are migrating a complex form from a Class to a Function, this is where bugs happen. You must manually spread your object: setState({ ...state, field: value }).

3. Initialization Performance

In the Class constructor, state initialization runs once. In the Functional component, the line useState(expensiveComputation()) runs on every render (though React ignores the result on subsequent renders). If initializing your state is computationally expensive, use lazy initialization:

// Only runs onceconst [value, setValue] = useState(() => expensiveComputation());

Avnish’s Verdict

I build automation systems where complexity creates friction. Class components introduce "incidental complexity"—complexity related to the mechanism of the language (classes, `this` binding) rather than the problem I'm solving.

Functional components with Hooks strip that away. They allow us to compose logic. If I wanted to share the counter logic between two components, in a Class world, I'd need Higher Order Components (HOCs) or Render Props—both messy patterns. In a Functional world, I simply extract the logic into a custom hook: useCounter.

However, respect the Class component. It forces you to think about lifecycle methods (Mounting, Updating, Unmounting) in a very explicit way. If you are struggling to understand useEffect, go write a Class component using componentDidUpdate. The clarity you gain there will make you a better Hooks developer.

Start building with Functions, but know your history.

Share

Comments

Loading comments...

Add a comment

By posting a comment, you’ll be subscribed to the newsletter. You can unsubscribe anytime.

0/2000