I\'ve been having this problem where my code in the componentDidMount()
method wasn\'t firing properly when refreshing the current page (and subsequently, the c
I see that this question has over a thousand views, so I'll explain how I solved this problem:
To solve this particular problem, the most sensible way is to create an upper level component that loads your subscription or database, so that you load the required data before passing it to your child component, which would completely remove the need to use componentWillMount()
. Also, you can do the computations in the upper level component and just pass them down as props to use in your receiving component
For example:
class UpperLevelComponent extends React.Component {
render() {
if(this.props.isReady) {
return(<ChildComponent {...props}/>)
}
}
}
export default createContainer(() => {
const data = Meteor.subscribe("myData");
const isReady = data.ready();
return {
isReady,
data: MyData.find.fetch()
}
})
In the example above, I use Meteor's reactive container to get my MongoDB data and wait for it to completely finish subscribing before I render the child component, passing it any props I want. If you load all your data in the higher level component, you won't have to rely on the componentWillMount()
method to trigger after every refresh. The data will be ready in the upper level component, so you can use it however you want in the child component.
When the page refreshes react doesn't have the chance to unmount the components as normal. Use the window.onbeforeunload event to set a handler for refresh (read the comments in the code):
class Demo extends React.Component {
constructor(props, context) {
super(props, context);
this.componentCleanup = this.componentCleanup.bind(this);
}
componentCleanup() { // this will hold the cleanup code
// whatever you want to do when the component is unmounted or page refreshes
}
componentWillMount(){
if(dataReady.get(true)){
let logos = this.props.questions[0].data.logos.length > 0 ? this.props.questions[0].data.logos.filter((item) => {
if(item.logo === true && item.location !== ""){
return item;
}
}) : [];
this.setState({ logos });
}
}
componentDidMount(){
window.addEventListener('beforeunload', this.componentCleanup);
}
componentWillUnmount() {
this.componentCleanup();
window.removeEventListener('beforeunload', this.componentCleanup); // remove the event handler for normal unmounting
}
}
useWindowUnloadEffect Hook
I've extracted the code to a reusable hook based on useEffect:
// The hook
const { useEffect, useRef, useState } = React
const useWindowUnloadEffect = (handler, callOnCleanup) => {
const cb = useRef()
cb.current = handler
useEffect(() => {
const handler = () => cb.current()
window.addEventListener('beforeunload', handler)
return () => {
if(callOnCleanup) handler()
window.removeEventListener('beforeunload', handler)
}
}, [cb])
}
// Usage example
const Child = () => {
useWindowUnloadEffect(() => console.log('unloaded'), true)
return <div>example</div>
}
const Demo = () => {
const [show, changeShow] = useState(true)
return (
<div>
<button onClick={() => changeShow(!show)}>{show ? 'hide' : 'show'}</button>
{show ? <Child /> : null}
</div>
)
}
ReactDOM.render(
<Demo />,
root
)
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="root"></div>
I also run into this problem and realised that I needed to make sure that at least 2 components will always gracefully unmount. So I finally did a High Order Component that ensures the wrapped component is always unmounted
import React, {Component} from 'react'
// this high order component will ensure that the Wrapped Component
// will always be unmounted, even if React does not have the time to
// call componentWillUnmount function
export default function withGracefulUnmount(WrappedComponent) {
return class extends Component {
constructor(props){
super(props);
this.state = { mounted: false };
this.componentGracefulUnmount = this.componentGracefulUnmount.bind(this)
}
componentGracefulUnmount(){
this.setState({mounted: false});
window.removeEventListener('beforeunload', this.componentGracefulUnmount);
}
componentWillMount(){
this.setState({mounted: true})
}
componentDidMount(){
// make sure the componentWillUnmount of the wrapped instance is executed even if React
// does not have the time to unmount properly. we achieve that by
// * hooking on beforeunload for normal page browsing
// * hooking on turbolinks:before-render for turbolinks page browsing
window.addEventListener('beforeunload', this.componentGracefulUnmount);
}
componentWillUnmount(){
this.componentGracefulUnmount()
}
render(){
let { mounted } = this.state;
if (mounted) {
return <WrappedComponent {...this.props} />
} else {
return null // force the unmount
}
}
}
}
Note: If like me, you are using turbolinks and rails, you might wanna hook on both beforeunload
and turbolinks:before-render
events.