I was playing some react component under NextJS framework, and accidentally found that the components under pages render twice. See this simple example: (/pages/test.js
)
function Test() {
console.log("I am rendering...");
return (
<p>Hi</p>
);
}
export default Test;
I loaded the page: localhost:3000/test and saw “I am rendering …” printed twice in the console. It really confused me. I tried to figure out which React Lifecycle causes the issue, so I rewrote it into the Class way:
export default class Test extends React.Component {
componentDidMount() {
console.log("did mount");
}
componentDidUpdate() {
console.log("did update");
}
render() {
console.log("I am rendering...");
return (
<p>Hi</p>
);
}
}
Then I was back to the browser, and saw those lines printed:
I am rendering...
did mount
I am rendering...
did update
Based on React Docs:
componentDidMount()
is invoked immediately after a component is mounted (inserted into the tree). - Right after first rendercomponentDidUpdate()
is invoked immediately after updating occurs. This method is not called for the initial render. - Right after other renders
So the problem is obviously in componentDidUpdate()
, how come it got triggered in the initial render? Could this be the issue in the NextJS framework in my case?
Next.js has a file-system based router built on the concept of pages.
When a file is added to the
pages
directory it's automatically available as a route.
The pages route starts from app.js. I have a custom App component, _app.js to override the default one, in which I set up UserContext (global state) in the componentDidMount(). After some research, I got the answer. I believe I learned this before, but it's so easy to overlook, so I decided to write it down, hope I would not forget it again.
When the parent re-renders, it's children also re-render even no state/props changed, unless it's a pure component.
I could fix this issue in my Class component by changing it to PureComponent, like this:
export default class Test extends React.PureComponent {
componentDidMount() {
console.log("did mount");
}
componentDidUpdate() {
console.log("did update");
}
render() {
console.log("I am rendering...");
return (
<p>Hi</p>
);
}
}
How about my functional component. Sure, there is a High Order Function called React.memo
could make a functional component like PureComponent, and it is like this:
function Test() {
console.log("I am rendering...");
return (
<p>Hi</p>
);
}
export default React.memo(Test);
After changing it to PureComponent and with React.memo
, the component only renders once.
There is actually another reason which could cause the component rendering twice. Although it's kind of false positive, it's worth mentioning. It happens when we use React.StrictMode
, especially, in the Create React App (CRA.) In src/index.js generated by CRA, React.StrictMode
has been used, like this:
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
Regarding ReactJS docs:
StrictMode
is a tool for highlighting potential problems in an application. LikeFragment
,StrictMode
does not render any visible UI. It activates additional checks and warnings for its descendants.
Due to additional checks, React may invoke render phase lifecycles more than once. ReactJS docs have a very clear explanation about this.
The good thing is that the StrictMode is only run in development mode; it does not impact the production build.
Happy learning!