If you have any questions or feedback, pleasefill out this form
This post is translated by ChatGPT and originally written in Mandarin, so there may be some inaccuracies or mistakes.
This article does not aim to introduce each react-hook API individually, but rather attempts to explore the design rationale from a design perspective. It will be divided into several sections:
- Differences between Function Components and Class Components
- How to reuse similar logic within components
- A brief discussion on higher-order components, render props, and mixins
Differences between Function Components and Class Components
What distinguishes React Function Components from React Class Components? From a functional standpoint, we can summarize a few key points:
- Function components do not have state (unless hooks are used)
- Function components do not have
this(this point is crucial) - Class components can declare instance methods
1. Function components do not have state (unless hooks are used)
In the past, when writing Function Components, we could only manipulate components using props, which is not a bad approach—this is beyond doubt.
This method is also easy to test, but function components cannot completely replace class components. The reason is that class components can utilize various lifecycle methods to have finer control over components, providing more flexibility in internal implementations. The state mechanism allows for more intricate operations to implement more complex logic within components.
2. Function components do not have this
Since Function Components are functions, they do not have this, or rather, the object that this points to is not the component itself. This offers several benefits, one being syntactic simplicity—there's no need to write verbose syntax like this.state.xxx, nor do we have to worry about binding this when passing event handlers.
Although the official documentation clearly states that you should not directly manipulate state and should always use the setState method to update state, in actual development, some engineers, who do not refer to the documentation, still use incorrect methods to manipulate state. For example, they might write something like this:
this.state.verified = true;This will not trigger React's update mechanism, which can easily lead to misunderstandings among other engineers. If such code is prevalent, it doubles the difficulty of refactoring or rewriting. Function Components can avoid this issue syntactically. Although it's not hard to write unmaintainable code, the opportunity for doing so is minimized.
It’s important to note that while class components can be converted to function components if lifecycle methods are not used, there are still differences, the most significant being the control of this.
Since both props and state are immutable, when using class components, React's internal implementation helps control the this context, meaning this is mutable.
In most cases, this is not problematic. However, caution is needed when dealing with non-instantaneous code like setTimeout or setInterval:
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 these two seconds?
Did you notice? Because this varies in class components, if userId changes during this 2-second period, the class component will use the props at the time the method was called, while the function component will use the props at the time of the button click.
When using this.props, the context has already changed, resulting in an inability to obtain the correct result. You could declare a variable to store this.props, but that doesn't solve the fundamental issue. What if other methods calling props are also implemented inside the setTimeout?
Conversely, since function components do not have this, we can safely use setTimeout or setInterval.
However, since class components allow for the implementation of instance methods, you can expose some methods for external operations.
This is not necessarily a good thing (most of the time, it can even be a bad thing). If you later want to modify or rename this method, you must consider whether it is directly called elsewhere or within the component, making modifications or refactoring more cumbersome.
Unless you can guarantee the method's generality during the initial structural planning, it can easily lead to greater effort during subsequent refactoring.
Additionally, it is difficult to share similar logic code in class components.
Suppose we want to determine the displayed content based on the user's login status. If we hope that each component needing to display based on the isLoggedIn state can directly access this state, how would we typically design it?
One option might be to place the logic and implementation of isLoggedIn into context, using Consumer to wrap the component whenever we want to access the context.
This makes the entire implementation lengthy and introduces additional code unrelated to the component's implementation (like adding Consumer).
It might look 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.
Mixin
First, regarding Mixins, the reason they have fallen out of favor is straightforward: they can easily affect the internal implementation of a component and are difficult to modify. A component might rely on methods defined in a mixin, making future modifications quite challenging, or a single mixin might depend on other mixins. For more details, refer to this article.
Of course, this doesn’t mean that mixins are inherently bad patterns; they just might not be the best fit in React.
Higher-Order Component
A higher-order component is defined as a function that takes a React Component as an argument and returns a wrapped component.
The classic use case is probably the connect function in Redux:
const MyProfile = ({ profile }) => {
};
export default connect(state => ({
profile: state.profile,
}))(MyProfile);Through this method, you can define parameters within the function, allowing for a more elegant implementation without modifying the internal workings of the component. The only dependency lies in receiving props, but through mapStateToProps, you can easily adjust props to suit the needs of the component.
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, this method still requires some understanding of the underlying implementation. For instance, you need to realize that withWindowSize adds the windowSize property for you.
It may seem elegant, but it inevitably requires some effort within the function, and you also need to define an additional component in the function and modify the displayName, among other things.
Additionally, when trying to combine it with other functions, it can become quite verbose. Although you can simplify this function using compose, it still presents some inconvenience.
withWindowSize(withScroll(withRouter(connect(...Even though this is a relatively good solution, it creates a considerable hurdle for beginners learning the concepts.
Render Props
Render Props involve wrapping parameters as function arguments exposed to children, allowing us to clearly see what parameters are passed in and freely decide whether to use them.
The context Consumer in React is accessed using the render props approach.
const ListContainer = ({ list }) => (
<InfiniteList>
{this.props.children(list)}
</InfiniteList>
)Render props also have some drawbacks; you still need to check the component to see what parameters are passed in, and you have to consider whether appropriate handling is in place if the counterpart does not pass a function but rather a regular component.
These various solutions are quite good, but they still have their shortcomings. Thus, the concept and implementation of react-hooks sparked a vigorous response within the React community upon its release.
The essence of Hooks is to eliminate the original limitations of function components, allowing you to use state, declare side effects, refs, context, and more—all things that were previously only usable within class components can now be employed in function components.
This enables developers to organize code in a more lightweight and concise manner. Moreover, since hooks are just functions, you can encapsulate logic yourself. For more details, refer to Dan's article, Making sense of react hooks.
Here’s a simple reference using useState and useEffect:
function useWindowSize() {
const [windowSize, setWindowSize] = useState(window.innerWidth);
useEffect(() => {
const setSize = () => {
setWindowSize(window.innerWidth);
};
window.addEventListener('resize', setSize);
return () => window.removeEventListener('resize', setSize);
}, []);
return windowSize;
}Within a component, we can directly call useWindowSize to obtain the current window.innerWidth.
const Layout = () => {
const windowSize = useWindowSize();
return ...
};Not only does this make it easier to know the source of windowSize, but it is also quite intuitive to implement, unlike higher-order components or render props, where additional considerations are necessary.
However, Hooks are not without their downsides. For instance, they require hooks to be called in the same order to achieve the correct render results, introducing a new eslint-plugin-react-hooks to ensure proper results.
Currently, Hooks still cannot replace class components entirely, such as methods like componentDidCatch or getSnapshotBeforeUpdate, and when using render props, if the parent is wrapped in a DOM-like structure, hooks cannot resolve the issue.
Additionally, function components still cannot expose methods for external use; they can only control through passing parameters, which is sometimes inconvenient.
Conclusion
The vast React community enables the core team to focus on developing these features. The journey from React mixins and higher-order components to Hooks may very well have been shaped by various experimental solutions.
Although the solutions may seem endless, the underlying principles are quite similar—each is a product developed to solve a specific problem.
If you found this article helpful, please consider buying me a coffee ☕ It'll make my ordinary day shine ✨
☕Buy me a coffee