Lesson Learnt: Isolate Components Behavior in ReactJS

Reading Time: 4 minutes

hangmanA few weeks ago I complained that calling asynchronous operations inside Reacts lifecycle method componentWillMount like AJAX may have collateral effects. In my case, the application hangs and stuck inside an infinite rendering loop. I discovered that my first attempt to solve the problem was by no way the solution, although I thought it was. The real cause was an unfortunate composition of components. The scenario was the following:

The Problem

In one of my components I put an AJAX call inside the componentWillMount method, which I needed to feed a drop-down selector. Once this operation was executed (during the mount phase) I had an innumerable amount of requests, which I couldn't explain. For some unknown reason, the componentWillMount method was called repeatedly and the application hung. I walked upwards the component hierarchy to find some hint about that strange behavior, but I did not find anything. So I moved the AJAX call to the componentDidMount method and additionally I surrounded it with an if-statement to prevent further requests, which worked initially. So, I thought I solved the problem and blamed componentWillMount for this undesired behavior. I was lucky, and told it the customer (proudly). Well, there was this tiny voice in my head that whispered to me: "this ain't the problem, dude". And this tiny voice was damn right!

The other day occurred the same problem in a different part of the application, for which I knew it had worked already. While analyzing the behavior I discovered that every time the loading icon disappeared for the first time the infinite loop started, and then I got the clue. It was the way I treated the (dis)appearance of that icon. And this was the original code:

var Application = React.createClass({

	getInitialState: function () {
		return { isLoading : false};
	},

	componentDidMount : function(){
		$event.addListener('loading-started', this.showLoadingIcon);
		$event.addListener('loading-finished', this.hideLoadingIcon);
	},

	componentWillUnmount: function () {
		$event.removeListener('loading-started');
		$event.removeListener('loading-finished');
	},

	showLoadingIcon : function(){
		if(this.isMounted()) {
			this.setState({isLoading: true});
		}
	},

	hideLoadingIcon : function(){
		if(this.isMounted()) {
			this.setState({isLoading: false});
		}
	},

	render: function () {
            return (
                <div>
                    <Header/>
                    <div className="container">
                        <div className="row">
                            <Notification />
                            // THIS IS THE UNFORTUNATE COMPOSITION - DO NOT DO THIS
	                    {this.state.isLoading ? <LoadingIcon/> : null}
                        </div>
                        <div className="row">
                            <RouteHandler/>
                        </div>all 
                    </div>
                    <Footer/>
                </div>
            )

		}
	});

The Solution

The Application component is the most top React component in my application. As one can see I use a loading state, altered by events emitted by my HTTP requests. The way I make the loading icon appear and disappear is crappy, because each time the icon appeared or disappeared, the subtree from RouteHandler is going to be rerendered, too. That's why the AJAX call caused an infinite loop; and it doesn't matter whether the request is done in componentWillMount, componentDidMount, componentWillUpdate, etc.

The solution is simple, but also shows how it should be done the right way. The icons appearance behavior belongs inside the LoadingIcon component, and not elsewhere. That way ReactJS is going to update the single component in an isolated context. Here is the real solution:

// The LoadingIcon component 
define(function (require) {

    var React = require('react');
    var $event = require('common/event');

    return React.createClass({

        getInitialState: function () {
            return {isLoading: false};
        },
        
        componentDidMount: function () {
            $event.addListener('loading-started', this.showLoadingIcon);
            $event.addListener('loading-finished', this.hideLoadingIcon);
        },

        componentWillUnmount: function () {
            $event.removeListener('loading-started');
            $event.removeListener('loading-finished');
        },

        showLoadingIcon: function () {
            if (this.isMounted()) {
                this.setState({isLoading: true});
            }
        },

        hideLoadingIcon: function () {
            if (this.isMounted()) {
                this.setState({isLoading: false});
            }
        },
        
        render: function () {
            return (
                // Here I use simply the hidden attribute, 
                // but returning an empty div would work either.
                // This way the component can be rendered in an isolated 
                // context, without any unnecessary and sometime dangerous 
                // rendering
                <div className="loading" hidden={!this.state.isLoading}>
                    <span className="ball"></span>
                    <span className="ball-small"></span>
                </div>
            );
        }
    });
});

 

var Application = React.createClass({
    render: function () {
        return (
            <div>
                <Header/>
                <div className="container">
                    <div className="row">
                        <Notification />
                        // self contained and isolated behavior
                        <LoadingIcon/>
                    </div>
                    <div className="row">
                        <RouteHandler/>
                    </div>
                </div>
                <Footer/>
            </div>
        )
    }
});

 

Conclusion

This lesson teached me, that component's behavior (that affects DOM manipulation) should be isolated, at best within its own component implementation. This would not only prevent some subtle errors, but also would improve the rendering performance. In my case, the rendering of the entire content, represented by the RouteHandler component, is not necessary anymore. Nowadays, when I want something in my DOM appear or disappear using React I pay doubled attention and watch out for eventual rendering implications.

Facebooktwittergoogle_plusredditpinterestlinkedin

Leave a Reply

Your email address will not be published. Required fields are marked *