From Class Components to Hooks
This article will not introduce every React Hooks API one by one; instead, it tries to explore the reasoning behind their design from an architectural perspective. It will mainly be divided into several sections:
- Differences between Function Components and Class Components
- How to reuse similar logic across components
- A brief overview of Higher-Order Components, Render Props, and Mixins
Function Components vs. Class Components
What is the difference between a React Function Component and a React Class Component? From a functional perspective, we can summarize a few points:
- Function components have no state (if not using hooks)
- Function components have no
this(this is very important) - Class components can declare instance methods
1. Function components have no state (if not using hooks)
In the past, when writing Function Components, the only way to control the component was by passing in props. There was nothing wrong with this—undoubtedly so.
This approach was also very easy to test. However, function components could not completely replace class components. The reason was that class components could use various lifecycle methods to exert finer control over the component, providing more flexibility in internal implementation. The state mechanism also offered more granular operations for implementing complex internal logic.
2. Function components have no this
Because a Function Component is just a function, it does not have this—or rather, the object that this points to is not the component itself. This provides several benefits. First is syntactic conciseness: you no longer need to write verbose code like this.state.xxx, nor do you need to worry about calling bind(this) depending on how event handlers are passed.
Although the official documentation makes it very clear never to mutate state directly and to always use setState to update state, in actual development, there were still engineers who skipped the docs and used incorrect methods to manipulate state. For example, writing code like this:
this.state.verified = true;
This doesn’t actually trigger React’s update mechanism and easily causes misunderstandings among other engineers. If such code is scattered everywhere, it doubles the difficulty of refactoring or rewriting. Function Components avoid this problem directly at the syntax level. Though, if someone wants to write unmaintainable code, they will always find a way…
It is particularly worth noting that even if a class component without lifecycle methods can be directly converted into a function component, there is still a difference between the two. The biggest difference lies in how this is handled.
Since props and state are immutable, when using class components, React’s internal implementation mutates this to point to the current component instance. In other words, this is mutable.
In most cases, this isn’t an issue. However, once you deal with non-instantaneous code like setTimeout or setInterval, you need to be especially careful:
function Profile({ userId }) {
setTimeout(() => {
fetchUserProfile(userId).then(alert);
}, 2000);
return ...
}
class Profile extends React.Component {
componentDidUpdate() {
setTimeout(() => {
getUserProfile(this.props.userId);
}, 2000);
}
getProfile() {
fetchUserProfile(this.props.userId);
}
render() {
return ...
}
}
What happens if userId changes during those two seconds?
Did you catch it? Because this is mutable in a class component, if userId changes during these 2 seconds, the class component reads the props from when the callback actually runs, whereas the function component captures the props from when the button was clicked.
When reading this.props, the context has already changed, making it impossible to get the intended result. You could declare a variable to capture this.props beforehand, but that still doesn’t solve the fundamental problem—what if the callback inside setTimeout calls another method that reads this.props?
In contrast, because function components don’t have this, we can safely use setTimeout or setInterval.
However, since a class component is a class, you can implement instance methods and expose them for external consumers to call.
This isn’t necessarily a good thing (in fact, it’s often a bad thing). If you want to modify or rename this method later, you have to consider whether other components or places call this method directly, which makes component modifications or refactoring much more troublesome.
Unless you can guarantee the general utility of this method during the initial architectural planning, it can easily lead to much greater refactoring costs down the road.
Additionally, sharing similar logic across class components is quite difficult.
Suppose we want to conditionally render content based on a user’s login state. If we want every component that needs to render based on isLoggedIn to directly access this state, how would we typically design it?
One approach might be to put the isLoggedIn logic and implementation into a context, and wrap the component with a Consumer every time we want to access it.
This makes the implementation verbose and introduces boilerplate unrelated to the component’s core logic (such as adding the Consumer wrapper).
It would look something like this:
const {Provider, Consumer} = createContext(null);
export default class UserContext extends React.Component {
state = {
isLoggedIn: false,
};
componentDidMount() {
fetchUser()
.then(res => this.setState({
isLoggedIn: true
}))
}
render() {
<Provider value={this.state}>
{this.props.children}
</Provider>
}
}
export Consumer;
App.js
const App = () => <UserContext>
<Profile />
</UserContext>
Profile.js
import { Consumer } from 'UserContext';
class Profile extends React.Component {
render() {
<Consumer>
{({ isLoggedIn }) => isLoggedIn ? showProfile() : null}
</Consumer>
}
}
To maintain simplicity and reusability, popular solutions at the time included Higher-Order Components and Render Props, as well as the older Mixins.
Mixins
Let’s start with Mixins. Mixins were deprecated for a simple reason: they were too prone to affecting a component’s internal implementation and were hard to modify. Components might depend on methods defined inside a mixin, making future changes exceedingly difficult, or a mixin might depend on other mixins. For details, refer to this article.
Of course, this doesn’t mean mixin is an inherently bad pattern; it was simply not well-suited for React.
Higher-Order Components
A Higher-Order Component (HOC) is a function that takes a React Component as an argument and returns an enhanced component.
The most classic use case is probably connect in Redux:
const MyProfile = ({ profile }) => {
};
export default connect(state => ({
profile: state.profile,
}))(MyProfile);
Through this approach, you can define parameters inside the function, making the implementation more elegant without modifying the component’s internal code. The only dependency is receiving props, and with mapStateToProps, you can easily reshape the props into what the component needs.
This function returns a React Component, typically implemented like this:
const withWindowSize = (WrappedComponent) => class WindowComponent extends React.Component {
state = {
screenSize: window.innerWidth,
}
setScreenSize = () => this.setState({ screenSize: window.innerWidth });
componentDidMount() {
window.addEventListener('resize', this.setScreenSize);
}
componentWillUnmount() {
window.removeEventListener('resize', this.setScreenSize);
}
render() {
return <WrappedComponent {...this.props} windowSize={this.state.screenSize} />
}
}
However, with this pattern, you still more or less need to understand the underlying implementation. For example:
With this kind of wrapper, you still need to know behind the scenes: “Oh, withWindowSize automatically injects the windowSize prop for me.”
While it seems elegant, it still requires extra effort inside the function—defining an extra component, modifying displayName, and so on.
Moreover, when combining it with other functions, it quickly becomes verbose. While you can simplify this with compose, it remains somewhat inconvenient in practice:
withWindowSize(withScroll(withRouter(connect(...
Though it was already a pretty good solution, it introduced a significant learning curve for beginners.
Render Props
Render props work by exposing arguments to children via a function, allowing us to clearly see which arguments are passed in and choose whether or not to use them.
React’s Context Consumer is consumed using the render props pattern:
const ListContainer = ({ list }) => (
<InfiniteList>
{this.props.children(list)}
</InfiniteList>
)
Render props also have some downsides: you still have to inspect the component to see what arguments are passed in, and you must consider whether edge cases—like passing a regular component instead of a function—are properly handled.
All these solutions were decent, but they left something to be desired. That is why when the concept and implementation of react-hooks were introduced, they immediately generated massive enthusiasm across the React community.
The concept behind Hooks is to remove the previous limitations of function components. You can use state, declare side effects, refs, context, and more inside a function component. Everything that used to be exclusive to class components can now be replaced with function components.
This allows developers to organize code in a lighter, cleaner manner. And because a hook is just a function, you can encapsulate custom logic yourself. For more details, check out Dan Abramov’s Making sense of react hooks.
Here is a minimal example using useState and useEffect for reference:
function useWindowSize() {
const [windowSize, setWindowSize] = useState(window.innerWidth);
useEffect(() => {
const setSize = () => {
setWindowSize(window.innerWidth);
};
window.addEventListener('resize', setSize);
return () => window.removeListener('resize', setSize);
}, []);
return windowSize;
}
Inside the component, we can directly call useWindowSize to obtain the current window.innerWidth:
const Layout = () => {
const windowSize = useWindowSize();
return ...
};
Beyond making the source of windowSize much clearer, the implementation is also very intuitive, without requiring the extra considerations needed for higher-order components or render props.
Hooks aren’t entirely without caveats, though. For instance, hooks must be called in a consistent order to ensure correct renders. To enforce this, eslint-plugin-react-hooks had to be introduced.
There are still a few areas where Hooks cannot fully replace class components yet, such as componentDidCatch or getSnapshotBeforeUpdate. Furthermore, when using render props where the parent is wrapped in an external DOM structure or similar, hooks cannot always solve it directly.
Additionally, function components still cannot expose instance methods to the outside—control is limited to passing parameters, which is sometimes not as convenient.
Conclusion
It is precisely because React has such a large community that the core team can focus on developing these features. Perhaps it was only through experiencing various exploratory solutions like mixins and higher-order components that we were able to arrive at Hooks.
While solutions seem to emerge endlessly, the underlying principles are similar: they are all products evolved to solve specific problems.
Related Posts
- Recreating My Room with Three.js Using React Three Fiber, I brought my real room into the browser—turning physical objects into an interactive table of contents, and using spatial memory to tell the story of my life and work over the past few years.
- Things to Keep in Mind When Using Images in Frontend Development Expanding on Jake Archibald's article, this post organizes how modern responsive images should be written: why width/height are still necessary, when to use CSS aspect-ratio, how to choose between AVIF and WebP, and using picture/source/srcset for art direction on mobile devices.
- CSS field-sizing — Auto-resize Form Elements with a Single Line of CSS Previously, auto-resizing a textarea required listening to scrollHeight in JavaScript. With CSS field-sizing: content, a single line replaces it all, supporting textarea, input, and select. This article covers the pain points of older approaches and how to use field-sizing.
- Make Your Link Underlines Look Better: text-underline-offset By default, underlines sit very close to the text. Some designers dislike this look, and personally, I don't think it looks great either.