How do React function components differ from React classes?
For some time, the standard explanation was that classes offered more features, such as state management. However, with Hooks, this distinction no longer holds true.
It is possible to hear that one type performs better than the other. Many benchmarks making such claims are flawed, so caution is advised when drawing conclusions. Performance primarily depends on the code’s actions, not whether a function or a class is chosen. Observed performance differences are typically negligible, though optimization strategies may vary slightly, as noted here.
Regardless, it is not recommended to rewrite existing components unless there are other compelling reasons and a willingness to be an early adopter. Hooks are still relatively new, and established best practices are still emerging in tutorials.
So, what are the fundamental differences between React functions and classes? The primary distinction lies in their mental model. This post will examine the most significant difference between them. This difference has existed since function components were introduced in 2015 but is often overlooked:
Function components capture the rendered values.
Let’s explore what this implies.
Note: This post does not offer a value judgment on either classes or functions. It solely describes the distinction between these two programming models in React. For questions regarding broader adoption of functions, refer to the Hooks FAQ.
Consider the following component:
function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return (
<button onClick={handleClick}>Follow</button>
);
}
This component displays a button that simulates a network request using setTimeout, then shows a confirmation alert. For instance, if props.user is ‘Dan’, it will display ‘Followed Dan’ after three seconds. This is a straightforward implementation.
(It is worth noting that using arrow functions or function declarations in the example above yields identical results; function handleClick() would behave the same way.)
How would this be written as a class? A direct translation might appear as follows:
class ProfilePage extends React.Component {
showMessage = () => {
alert('Followed ' + this.props.user);
};
handleClick = () => {
setTimeout(this.showMessage, 3000);
};
render() {
return <button onClick={this.handleClick}>Follow</button>;
}
}
It is common to assume these two code snippets are equivalent. Developers often refactor between these patterns without recognizing their implications:

However, these two code snippets are subtly different. Examine them closely. Can you spot the difference? It can be challenging to identify at first glance.
Spoilers are ahead, so here is a live demo if you prefer to discover it independently. The remainder of this article explains the difference and its significance.
Before proceeding, it is important to emphasize that the described difference is unrelated to React Hooks themselves. The examples provided do not even utilize Hooks!
The discussion centers on the fundamental difference between functions and classes in React. Understanding this distinction is beneficial for anyone planning to use functions more frequently in a React application.
The difference will be illustrated with a common bug found in React applications.
Open this example sandbox, which features a profile selector and the two ProfilePage implementations mentioned earlier, each rendering a Follow button.
Perform the following sequence of actions with both buttons:
- Click one of the Follow buttons.
- Change the selected profile before 3 seconds elapse.
- Read the alert text.
A peculiar difference will be observed:
-
With the ProfilePage function, clicking Follow on Dan’s profile and then navigating to Sophie’s profile would still alert ‘Followed Dan’.
-
With the ProfilePage class, it would alert ‘Followed Sophie’:

In this scenario, the first behavior is the correct one. If a user follows a person and then navigates to another person’s profile, the component should not become confused about who was followed. This class implementation clearly contains a bug.
(It is highly recommended to follow Sophie, though.)
So, why does the class example behave this way?
Let’s examine the showMessage method in the class:
class ProfilePage extends React.Component {
showMessage = () => {
alert('Followed ' + this.props.user);
};
This class method reads from this.props.user. While props in React are immutable and cannot change, ‘this’ itself is, and has always been, mutable.
Indeed, that is the very purpose of ‘this’ in a class. React mutates it over time so that the most current version can be read in render and lifecycle methods.
Therefore, if the component re-renders while a request is pending, this.props will change. The showMessage method then reads the user from the updated, or “too new,” props.
This reveals an interesting insight into the nature of user interfaces. If a UI is conceptually a function of the current application state, event handlers are part of the render result—just like the visual output. Event handlers are intrinsically linked to a specific render with its particular props and state.
However, scheduling a timeout whose callback reads this.props severs that association. The showMessage callback is not “tied” to any specific render, and thus it “loses” the correct props. Reading from ‘this’ broke that connection.
Suppose function components did not exist. How would this problem be resolved?
The goal would be to somehow “repair” the connection between the render with the correct props and the showMessage callback that reads them. Somewhere along the way, the props are lost.
One solution would be to read this.props early during the event and then explicitly pass them into the timeout completion handler:
class ProfilePage extends React.Component {
showMessage = (user) => {
alert('Followed ' + user);
};
handleClick = () => {
const {user} = this.props;
setTimeout(() => this.showMessage(user), 3000);
};
render() {
return <button onClick={this.handleClick}>Follow</button>;
}
}
This approach works. However, it makes the code significantly more verbose and prone to errors over time. What if more than a single prop was needed? What if state also needed to be accessed? If showMessage calls another method, and that method reads this.props.something or this.state.something, the exact same problem would recur. This would necessitate passing this.props and this.state as arguments through every method called from showMessage.
Such an approach negates the ergonomic benefits typically offered by a class. It is also difficult to remember or enforce, which often leads to developers accepting bugs instead.
Similarly, inlining the alert code inside handleClick does not address the broader issue. The aim is to structure the code in a way that allows splitting it into multiple methods while still reading the props and state corresponding to the render associated with that call. This problem is not unique to React; it can be reproduced in any UI library that stores data in a mutable object like ‘this’.
Perhaps, methods could be bound in the constructor?
class ProfilePage extends React.Component {
constructor(props) {
super(props);
this.showMessage = this.showMessage.bind(this);
this.handleClick = this.handleClick.bind(this);
}
showMessage() {
alert('Followed ' + this.props.user);
}
handleClick() {
setTimeout(this.showMessage, 3000);
}
render() {
return <button onClick={this.handleClick}>Follow</button>;
}
}
No, this does not resolve the issue. The problem stems from reading from this.props too late, not from the syntax used! However, the problem would disappear if JavaScript closures were fully utilized.
Closures are often avoided because it is challenging to reason about a value that can be mutated over time. But in React, props and state are immutable (or at least, immutability is strongly recommended). This eliminates a major pitfall of closures.
This implies that if props or state from a particular render are closed over, they can always be relied upon to remain precisely the same:
class ProfilePage extends React.Component {
render() {
// Capture the props!
const props = this.props;
// Note: we are *inside render*.
// These aren't class methods.
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return <button onClick={handleClick}>Follow</button>;
}
}
Props are “captured” at the time of render:
This ensures that any code within it (including showMessage) is guaranteed to access the props for that specific render. React no longer causes unexpected changes.
It is then possible to add as many helper functions inside as desired, and they would all utilize the captured props and state. Closures provide the solution!
The example above is correct but appears unusual. What is the purpose of a class if functions are defined inside render instead of using class methods?
Indeed, the code can be simplified by removing the class “shell”:
function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return (
<button onClick={handleClick}>Follow</button>
);
}
As before, the props are still being captured—React passes them as an argument. Unlike ‘this’, the props object itself is never mutated by React.
This becomes clearer if props are destructured in the function definition:
function ProfilePage({ user }) {
const showMessage = () => {
alert('Followed ' + user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return (
<button onClick={handleClick}>Follow</button>
);
}
When the parent component renders ProfilePage with different props, React will call the ProfilePage function again. However, the event handler that was previously clicked “belonged” to the earlier render with its own ‘user’ value and the showMessage callback that reads it. These remain intact.
This explains why, in the function version of this demo, clicking Follow on Sophie’s profile and then changing the selection to Sunil would alert ‘Followed Sophie’:

This behavior is correct. (Although following Sunil is also encouraged!)
Now the significant difference between functions and classes in React is clear:
Function components capture the rendered values.
With Hooks, the same principle applies to state as well. Consider this example:
function MessageThread() {
const [message, setMessage] = useState('');
const showMessage = () => {
alert('You said: ' + message);
};
const handleSendClick = () => {
setTimeout(showMessage, 3000);
};
const handleMessageChange = (e) => {
setMessage(e.target.value);
};
return (
<>
<input value={message} onChange={handleMessageChange} />
<button onClick={handleSendClick}>Send</button>
</>
);
}
(Here is a live demo.)
While this is not an ideal message app UI, it illustrates the same point: if a specific message is sent, the component should not become confused about which message was actually dispatched. This function component’s message captures the state that “belongs” to the render which returned the click handler invoked by the browser. Thus, the message is set to the value present in the input when the “Send” button was clicked.
So, functions in React capture props and state by default. But what if there is a need to read the latest props or state that do not belong to the current render? What if there is a desire to “read them from the future”?
In classes, this is achieved by reading this.props or this.state because ‘this’ itself is mutable; React mutates it. In function components, a mutable value shared by all component renders can also be used. This is called a “ref”:
function MyComponent() {
const ref = useRef(null);
// You can read or write `ref.current`.
// ...
}
However, managing it falls to the developer.
A ref serves the same purpose as an instance field. It acts as an escape hatch into the mutable imperative world. While “DOM refs” may be familiar, the concept is much more general; it is simply a container for any value.
Visually, this.something resembles something.current. They represent the same concept.
By default, React does not create refs for the latest props or state in function components. In many cases, they are not needed, and assigning them would be wasted effort. However, the value can be tracked manually if desired:
function MessageThread() {
const [message, setMessage] = useState('');
const latestMessage = useRef('');
const showMessage = () => {
alert('You said: ' + latestMessage.current);
};
const handleSendClick = () => {
setTimeout(showMessage, 3000);
};
const handleMessageChange = (e) => {
setMessage(e.target.value);
latestMessage.current = e.target.value;
};
If ‘message’ is read in showMessage, the message present at the time the Send button was pressed will be seen. But when latestMessage.current is read, the latest value is obtained—even if typing continued after the Send button was pressed.
The two demos can be compared to observe the difference. A ref offers a way to “opt out” of rendering consistency and can be useful in certain situations.
Generally, reading or setting refs during rendering should be avoided because they are mutable. The goal is to maintain predictable rendering. However, if the latest value of a particular prop or state is needed, manually updating the ref can be cumbersome. This can be automated using an effect:
function MessageThread() {
const [message, setMessage] = useState('');
// Keep track of the latest value.
const latestMessage = useRef('');
useEffect(() => {
latestMessage.current = message;
});
const showMessage = () => {
alert('You said: ' + latestMessage.current);
};
(Here is a demo.)
The assignment is performed inside an effect so that the ref value changes only after the DOM has been updated. This ensures that the mutation does not disrupt features like Time Slicing and Suspense, which rely on interruptible rendering.
Using a ref in this manner is not frequently necessary. Capturing props or state is typically a better default. However, it can be useful when working with imperative APIs such as intervals and subscriptions. Remember that any value can be tracked this way—a prop, a state variable, the entire props object, or even a function.
This pattern can also be beneficial for optimizations, such as when useCallback identity changes too frequently. Nevertheless, using a reducer is often a superior solution. (This will be a topic for a future blog post!)
In this post, a common problematic pattern in classes was examined, along with how closures help resolve it. However, it might be noticed that optimizing Hooks by specifying a dependency array can lead to bugs with stale closures. Does this imply that closures are the problem? This is not necessarily the case.
As demonstrated, closures actually help fix subtle problems that are difficult to detect. Similarly, they simplify writing code that functions correctly in Concurrent Mode. This is possible because the logic within the component closes over the correct props and state with which it was rendered.
In all observed instances, “stale closures” problems arise from the mistaken assumption that “functions don’t change” or that “props are always the same”. This is incorrect, as this post aims to clarify.
Functions close over their props and state, making their identity equally important. This is not a bug but a feature of function components. Functions should not be excluded from the “dependencies array” for useEffect or useCallback, for example. (The correct fix is usually either useReducer or the useRef solution discussed above—guidance on choosing between them will be documented soon.)
When writing the majority of React code with functions, an adjustment in intuition is required regarding optimizing code and what values can change over time.
As Fredrik stated:
The most effective mental rule discovered so far with Hooks is “code as if any value can change at any time”.
Functions are no exception to this rule. It will take time for this to become common knowledge in React learning materials. It necessitates some adaptation from the class mindset. However, this article aims to provide a fresh perspective.
React functions consistently capture their values—and now the reason is clear.
They are a fundamentally different approach.

