Reactjs - Higher Order Component unmount not working correctly - reactjs

I created a HOC to listen for clicks outside its wrapped component, so that the wrapped component can listen and react as needed.
The HOC looks like this :
const addOutsideClickListener = (WrappedComponent) => {
class wrapperComponent extends React.Component {
componentDidMount() {
console.log('*** component did MOUNT called ***');
document.addEventListener('click', this._handleClickOutside.bind(this), true);
}
componentWillUnmount() {
console.log('*** component will UNMOUNT called ***');
document.removeEventListener('click', this._handleClickOutside.bind(this), true);
}
_handleClickOutside(e) {
const domNode = ReactDOM.findDOMNode(this);
if ((!domNode || !domNode.contains(e.target))) {
this.wrapped.handleClickOutside();
}
}
render() {
return (
<WrappedComponent
ref={(wrapped) => { this.wrapped = wrapped }}
{...this.props}
/>
);
}
}
return wrapperComponent;
}
It works fine... most of the time.
When the wrapped component is unmounted, the method "componentWillUnmount" gets called, but "removeEventListener" continues to listen for events, and so I get error messages
"Uncaught Error: findDOMNode was called on an unmounted component."
Any ideas what could be causing this ?

The reason why your removeEventListener is not working is because you are trying to remove a newly created function. The same applies for addEventListener. These are two completely difference functions and have no reference to each other whatsoever.
You have to bind your method _handleClickOutside so React knows there exactly one version of it. You do that by binding it in the constructor with
constructor() {
super();
this._handleClickOutside.bind(this);
}
or auto bind it with ES6 arrow methodology
_handleClickOutside = (e) => { ... }
Now you pass the binded method to your eventlisteners with
document.addEventListener('click', this._handleClickOutside, true);
and respectively the remove listener.

Add this to constructor:
this._handleClickOutside = this._handleClickOutside.bind(this)
and then do this:
componentDidMount() {
document.addEventListener('click', this._handleClickOutside, true);
}
componentWillUnmount() {
document.removeEventListener('click', this._handleClickOutside, true);
}

Related

React / Redux Components not re-rendering on state change

I think this question has been answer several time but I can't find my specific case.
https://codesandbox.io/s/jjy9l3003
So basically I have an App component that trigger an action that change a state call "isSmall" to true if the screen is resized and less than 500px (and false if it is higher)
class App extends React.Component {
...
resizeHandeler(e) {
const { window, dispatch } = this.props;
if (window.innerWidth < 500 && !this.state.isSmall) {
dispatch(isSmallAction(true));
this.setState({ isSmall: true });
} else if (window.innerWidth >= 500 && this.state.isSmall) {
dispatch(isSmallAction(false));
console.log(isSmallAction(false));
this.setState({ isSmall: false })
}
};
componentDidMount() {
const { window } = this.props;
window.addEventListener('resize', this.resizeHandeler.bind(this));
}
...
I have an other component called HeaderContainer who is a child of App and connected to the Store and the state "isSmall", I want this component to rerender when the "isSmall" change state... but it is not
class Header extends React.Component {
constructor(props) {
super(props);
this.isSmall = props.isSmall;
this.isHome = props.isHome;
}
...
render() {
return (
<div>
{
this.isSmall
?
(<div>Is small</div>)
:
(<div>is BIG</div>)
}
</div>
);
}
...
even if I can see through the console that redux is actually updating the store the Header component is not re-rendering.
Can someone point out what I am missing ?
Am I misunderstanding the "connect()" redux-react function ?
Looking at your code on the link you posted your component is connected to the redux store via connect
const mapStateToProps = (state, ownProps) => {
return {
isHome: ownProps.isHome,
isSmall: state.get('isSmall')
}
}
export const HeaderContainer = connect(mapStateToProps)(Header);
That means that the props you are accessing in your mapStateToProps function (isHome and isSmall) are taken from the redux store and passed as props into your components.
To have React re-render your component you have to use 'this.props' inside the render function (as render is called every time a prop change):
render() {
return (
<div>
{
this.props.isSmall
?
(<div>Is small</div>)
:
(<div>is BIG</div>)
}
</div>
);
}
You are doing it well in the constructor but the constructor is only called once before the component is mounted. You should have a look at react lifecycle methods: https://reactjs.org/docs/react-component.html#constructor
You could remove entirely the constructor in your Header.js file.
You should also avoid using public class properties (e.g. this.isSmall = props.isSmall; ) in react when possible and make use of the React local state when your component needs it: https://reactjs.org/docs/state-and-lifecycle.html#adding-local-state-to-a-class
A component is only mounted once and then only being updated by getting passed new props. You constructor is therefore only being called once before mount. That means that the instance properties you set there will never change during the lifetime of your mounted component. You have to directly Access this.props in your render() function to make updating work. You can remove the constructor as he doesn't do anything useful in this case.

How to react to a prop change without render it using mobx

I am building a React + Mobx app and I have a situation where I have a component that does not render anything because it is all based on a 3rd party map API, but I need to react to some store property changes:
componentDidMount() {
mapApi.doSomething(this.props.store.value)
}
componentWillReact() {
mapApi.doSomething(this.props.store.value)
}
render () {
//workaround to make componentWillReact trigger
console.log(this.props.store.value)
return null
}
Is there an elegant way to implement this?
If I understood you correctly, you want to react to something that not used in render function. You can do something like this:
#observer
class ListView extends React.Component {
constructor(props) {
super(props)
}
#observable filterType = 'all'
handleFilterTypeChange(filterType) {
fetch('http://endpoint/according/to/filtertype')
.then()
.then()
}
// I want the method that autoruns whenever the #observable filterType value changes
hookThatDetectsWhenObservableValueChanges = reaction(
() => this.filterType,
filterType => {
this.handleFilterTypeChange(filterType);
}
)
}
This solution is mentioned here https://github.com/mobxjs/mobx-react/issues/122#issuecomment-246358153
You could use shouldComponentUpdate
shouldComponentUpdate() {
return false; // would never rerender.
}

React Isomorphic Rendering - handle window resize event

I would like to set the state of a component based on the current size of the browser window. The server-side rendering has been used (React+Redux). I was thinking about using the Redux store as a glue - just to update the store on resize.
Is there any other/better solution that doesn't involve Redux.
Thanks.
class FocalImage extends Component {
// won't work - the backend rendering is used
// componentDidMount() {
// window.addEventListener(...);
//}
//componentWillUnmount() {
// window.removeEventListener('resize' ....);
//}
onresize(e) {
//
}
render() {
const {src, className, nativeWidth, nativeHeight} = this.props;
return (
<div className={cn(className, s.focalImage)}>
<div className={s.imageWrapper}>
<img src={src} className={_compare_ratios_ ? s.tall : s.wide}/>
</div>
</div>
);
}
}
I have a resize helper component that I can pass a function to, which looks like this:
class ResizeHelper extends React.Component {
static propTypes = {
onWindowResize: PropTypes.func,
};
constructor() {
super();
this.handleResize = this.handleResize.bind(this);
}
componentDidMount() {
if (this.props.onWindowResize) {
window.addEventListener('resize', this.handleResize);
}
}
componentWillUnmount() {
if (this.props.onWindowResize) {
window.removeEventListener('resize', this.handleResize);
}
}
handleResize(event) {
if ('function' === typeof this.props.onWindowResize) {
// we want this to fire immediately the first time but wait to fire again
// that way when you hit a break it happens fast and only lags if you hit another break immediately
if (!this.resizeTimer) {
this.props.onWindowResize(event);
this.resizeTimer = setTimeout(() => {
this.resizeTimer = false;
}, 250); // this debounce rate could be passed as a prop
}
}
}
render() {
return (<div />);
}
}
Then any component that needs to do something on resize can use it like this:
<ResizeHelper onWindowResize={this.handleResize} />
You also may need to call the passed function once on componentDidMount to set up the UI. Since componentDidMount and componentWillUnmount never get called on the server this works perfectly in my isomorphic App.
My solution is to handle resize event on the top-most level and pass it down to my top-most component, you can see full code here, but the gist is:
let prevBrowserWidth
//re-renders only if container size changed, good place to debounce
let renderApp = function() {
const browserWidth = window.document.body.offsetWidth
//saves re-render if nothing changed
if (browserWidth === prevBrowserWidth) {
return
}
prevBrowserWidth = browserWidth
render(<App browserWidth={browserWidth} />, document.getElementById('root'))
}
//subscribing to resize event
window.addEventListener('resize', renderApp)
It obviously works without Redux (while I still use Redux) and I figured it would be as easy to do same with Redux. The advantage of this solution, compared to one with a component is that your react components stay completely agnostic of this and work with browser width as with any other props passed down. So it's a localized place to handle a side-effect. The disadvantage is that it only gives you a property and not event itself, so you can't really rely on it to trigger something that is outside of render function.
Besides that you can workaround you server-side rendering issue by using something like:
import ExecutionEnvironment from 'exenv'
//...
componentWillMount() {
if (ExecutionEnvironment.canUseDOM) {
window.addEventListener(...);
}
}

componentWillUnmount() not being called when refreshing the current page

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 component). However, it works perfectly fine just navigating and routing through my website by clicking links. Refresh the current page? Not a chance.
I found out that the problem is that componentWillUnmount() doesn't trigger when I refresh the page and triggers fine clicking links and navigating my website/app.
The triggering of the componentWillUnmount() is crucial for my app, since the data that I load and process in the componentDidMount() method is very important in displaying information to users.
I need the componentWillUnmount() to be called when refreshing the page because in my componentWillMount() function (which needs to re-render after every refresh) I do some simple filtering and store that variable in a state value, which needs to be present in the logos state variable in order for the rest of the component to work. This does not change or receive new values at any time during the component's life cycle.
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: logos});
}
};
Cliffs:
I do DB filtering in componentWillMount()method
Need it to be present in the component after refresh
But I have a problem where the componentWillUnmount() doesn't trigger when the page is refreshed
Need help
Please
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)
}
}, [callOnCleanup])
}
// 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.
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.

in react can't get new value after immediately setState

I am working with Reactjs. in which I am setting error if not properly entered.
Sample code is:
handleChange: function() {
if(shares) {
this.setState({
error_shares:''
})
}
if(this.state.error_shares === ''){
console.log('entered!!')
}
}
Here value of error_state doesn't reflect changed value in next if
Yes, that is the desired behavior.
The reason why the value has not been updated is because your component has not been re-rendered yet.
If you need such a feature, you should use the componentDidUpdate callback.
See the lifecycle methods here.
EXPLANATION:
Here is how React works:
Each component is a function of external props + internal state
If a change in either of the two is triggered, the component is queued to be re-rendered (it is asynchronous so that the client application is not blocked).
See the above link for exact sequence of operations but here is a little proof of concept.
import React from 'react';
class Example extends React.Component {
constructor(props, context) {
super(props, context);
// initial state
this.state = {
clicked: false
};
}
componentDidUpdate(prevProps, prevState) {
console.log(this.state.clicked); // this will show "true"
}
render() {
return (
<button
onClick={() => {
this.setState({clicked: true});
}}
>
</button>
);
}
}
setState is asynchronous in nature.
The second (optional) parameter is a callback function that will be
executed once setState is completed and the component is re-rendered.
so you can do something like this.
handleChange: function () {
if ( shares ) {
this.setState( {
error_shares: ''
}, function () {
if ( this.state.error_shares === '' ) {
console.log( 'entered!!' )
}
} )
}
}
source: https://facebook.github.io/react/docs/component-api.html#setstate
Yes, indeed.
setState() does not immediately mutate this.state but creates a pending state transition. Accessing this.state after calling this method can potentially return the existing value.
http://facebook.github.io/react/docs/component-api.html
No. You can't get update value in console just after setState. This is because as soon as setState is called, view is re-rendered. Hence to get updated value, you can check it inside render() .
render(){
if(this.state.error_shares === ''){
//your code.
}
}

Resources