This article will not introduce each API of react-hooks one by one, but instead try to explore the reasons behind the design from a design perspective. It will mainly be divided into several paragraphs:
- Differences between Function Component and Class Component
- How to reuse similar logic in components
- Brief discussion on high-order components, render props, and mixins
Differences between Function Component and Class Component
What are the differences between React Function Component and React class component? From a functional perspective, we can summarize a few points:
- Function components do not have state (if not using hooks)
- Function components do not have
this
(this is important) - Class components can declare instance methods
1. Function components do not have state (if not using hooks)
In the past, when writing Function Components, we could only manipulate components by passing in props
. There is nothing wrong with this, and it is undoubtedly clear.
This way of writing is also easy to test, but function components cannot completely replace class components. The reason is that class components can use various lifecycle methods to have more fine-grained control over the components and provide more flexibility in the implementation of internal components. The mechanism of state also allows for more complex logic to be implemented within the component.
2. Function components do not have this
Function Components do not have this
because they are functions, or more precisely, the object to which this
refers is not the component itself. This provides several benefits. First, it simplifies the syntax, eliminating the need for verbose syntax like this.state.xxx
and the hassle of bind(this)
when passing event handlers.
Although the official documentation clearly states not to directly manipulate state and to use the setState
method to update the state, in actual development, there are still some engineers who do not read the documentation and use incorrect methods to manipulate state
. For example, writing code like this:
this.state.verified = true;
This actually does not trigger the React update mechanism and can easily lead to misunderstandings by other engineers. If there is code like this everywhere, refactoring or rewriting becomes much more difficult. Function Components can avoid this problem directly from the syntax. Although it is not afraid of finding a way to write unmaintainable code...
It is important to note that although class components can be converted to function components if they do not use lifecycle methods, there are still differences between the two, with the biggest difference being the control of this
.
Since both props and state are immutable, when using class components, React internally manipulates this
to refer to the component. In other words, this
is mutable.
In most cases, this is not a problem. However, when dealing with non-immediate execution code such as setTimeout
or setInterval
, special attention needs to be paid:
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 within these two seconds?
Did you notice it? Because this
can change in class components, if userId
changes during these 2 seconds, the class component will use the props at the time this method is called, while the function component will use the props at the time the button is clicked.
When using this.props
, the context has already changed, making it impossible to get the correct result. You can declare a variable to store this.props
, but it still doesn't avoid the fundamental problem. What if the implementation inside setTimeout
has other methods that call props?
On the other hand, because function components do not have this
, we can safely use setTimeout
or setInterval
.
However, since it is a class, you can implement instance methods to expose some methods for external operations.
This may not necessarily be a good thing (most of the time, it is even a bad thing). If you want to modify or rename this method later, you have to consider whether this method is directly called in other places or within the component, which will make modifying or refactoring the component more difficult.
In addition, it is difficult to share code with similar logic in class components.
Suppose we want to display content based on the user's login status. What is the usual design if we want each component that needs to display based on the isLoggedIn
status to directly access this state?
The first method may consider putting the logic and implementation of isLoggedIn
into the context and use Consumer
to wrap the components whenever accessing the context.
This will make the entire implementation cumbersome and introduce some code unrelated to the implementation of the component (like adding a 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>
}
}
For simplicity and reusability, popular solutions at that time included High Order components, render props, and the older mixin.
Mixin
Let's talk about Mixin first. The reason Mixin was deprecated is simple: it easily affects the internal implementation of components and is difficult to modify. Components may rely on methods defined in mixins, making it difficult to modify them later, or a mixin may depend on other mixins. For more details, you can refer to this article.
Of course, this does not mean that mixin is a bad pattern, but it may not be suitable for React.
High-order component
A high-order component is defined as a function that takes a React Component as a parameter and returns a wrapped component.
The most classic use case is probably the connect
function in Redux:
const MyProfile = ({ profile }) => {
};
export default connect(state => ({
profile: state.profile,
}))(MyProfile);
Through this approach, you can define parameters in the function to make the implementation more elegant without modifying the internal implementation of the component. The only dependency is the reception of props
, but it can be easily modified to the props required by the component through mapStateToProps
.
The implementation of this function usually looks 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 approach still requires some understanding of the underlying implementation. For example:
With this kind of wrapping, you need to know that, oh, withWindowSize
will add the windowSize
property by itself.
It seems elegant, but it still requires some effort in the function and needs to define another component inside the function, as well as modify displayName
, and so on.
It also becomes quite cumbersome when used in conjunction with other functions. Although you can use compose
to simplify this function, it is still somewhat inconvenient to use.
withWindowSize(withScroll(withRouter(connect(...
This is already a pretty good solution, but it also creates a significant learning curve for beginners.
Render Props
Render props expose parameters wrapped in functions to children, allowing us to clearly know which parameters are passed in and choose whether to use them.
React's context Consumer uses render props to access context.
const ListContainer = ({ list }) => (
<InfiniteList>
{this.props.children(list)}
</InfiniteList>
)
Render props also have some drawbacks. You still need to look inside the component to see what parameters are passed in, and you need to consider whether there is corresponding processing if the other party is not passing a function but a regular component.
These solutions are all good, but they seem to have some limitations. Therefore, when the concept and implementation of react-hooks were introduced, they immediately received enthusiastic responses from the React community.
The concept of Hooks is to remove the original limitations of function components. You can use state, declare side effects, refs, context, and so on in function components. Everything that could only be used in class components can now be replaced with function components.
This allows developers to organize code in a more lightweight and concise manner, and because hooks are just functions, they can also encapsulate logic. For more details, you can refer to Dan's article Making sense of react hooks.
Here is a simple example using useState
and useEffect
as a 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;
}
In the component, we can directly call useWindowSize
to get the current window.innerWidth
.
const Layout = () => {
const windowSize = useWindowSize();
return ...
};
In addition to knowing the source of windowSize
more easily, the implementation is also quite intuitive, unlike high-order components or render props, where additional considerations need to be made.
Hooks are not entirely perfect either. For example, to ensure the correct rendering result, the order of hook calls must be consistent, which introduces the new eslint-plugin-react-hooks to ensure the result.
Hooks still cannot replace class components in some aspects, such as methods like componentDidCatch
or getSnapshotBeforeUpdate
. When using render props, if the parent is wrapped by DOM or other structures, hooks cannot be used to solve the problem.
Also, function components still cannot expose methods for external use. They can only be controlled through passed parameters, which is not always convenient.
Conclusion
React has a large community, which allows the core team to focus on developing these features. It is also because of various experimental solutions in the React community, such as react mixin and high-order components, that they eventually came up with the Hooks solution.
Although the solutions seem endless, the principles behind them are similar. They are derivatives created to solve specific problems.