Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at CFUNITED 2008 (Washington, D.C.) with: Peter Bell
Ben Nadel at CFUNITED 2008 (Washington, D.C.) with: Peter Bell@peterbell )

Image Load And Error Events Bubble Up In ReactJS

By Ben Nadel on

In JavaScript, images are not that hard to use, generally speaking. But, hooking into the image node life cycle can sometimes be a bit of a pain. Not only do the "load" and "error" events not bubble-up in the DOM (Document Object Model) tree but, some older browsers [can] fire the "load" event before the onLoad event handler is even bound. Because of these eccentricities, I was particularly impressed to see that the ReactJS synthetic event system not only supports "load" and "error" events but, that it also allows them to bubble up in the component hierarchy.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

Behind the scenes, ReactJS uses event delegation for all event handlers. This makes the event bindings extremely dynamic and allows the seamless binding of callbacks to new elements as they are added to the virtual DOM. But, event delegation relies heavily on the bubbling-up of events within the DOM. As such, it's surprising to see that the "load" and "error" events are available in the ReactJS event system.

If you dig into the ReactJS source code, you will see that the native IMG component requires the "LocalEventTrapMixin". This mixing hooks into the mounting and unmounting life cycle events of the ReactJS image component and adds local event listeners to the IMG element as its added to the physical DOM. These local event bindings tap into the "load" and "error" events; and, when they are fired, the event bindings pipe the events into React's synthetic event system. In doing so, it allows you to treat image "load" and image "error" events in the same exact way that you would treat any other event on the virtual DOM. And that's very exciting.

To see this in action, I've created a small ReactJS demo that conditionally includes an image on the page. The demo wires-up two "load" event bindings: one on the conditionally-included image and another on the parent container. This will show us that "load" event fires on the image and bubbles up in the component tree.

  • <!doctype html>
  • <html>
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Image Load And Error Events Bubble Up In ReactJS
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></link>
  • </head>
  • <body>
  •  
  • <h1>
  • Image Load And Error Events Bubble Up In ReactJS
  • </h1>
  •  
  • <div id="content">
  • <!-- This content will be replaced with the React rendering. -->
  • </div>
  •  
  •  
  • <!-- Load scripts. -->
  • <script src="../../vendor/reactjs/react-0.13.3.min.js"></script>
  • <script src="../../vendor/reactjs/JSXTransformer-0.13.3.js"></script>
  • <script type="text/jsx">
  •  
  • // I manage the Demo widget.
  • var Demo = React.createClass({
  •  
  • // I return the initial state and initialize any instance variables.
  • getInitialState: function() {
  •  
  • return({
  • isShowingImage: true
  • });
  •  
  • },
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I handle the load event on the image.
  • handleImageLoad: function( event ) {
  •  
  • console.log( "Image load event fired -->", event.target.tagName );
  •  
  • },
  •  
  •  
  • // I handle the load event WHEN / IF it bubbles up to the DIV element.
  • handleImageLoadBubbled: function( event ) {
  •  
  • console.log( "Image load event bubbled up to Div -->", event.target.tagName );
  •  
  • },
  •  
  •  
  • // I render the view using the current state and properties collections.
  • render: function() {
  •  
  • var image = (
  • <img
  • src="./flex.jpg"
  • onLoad={ this.handleImageLoad }
  • width="400"
  • />
  • );
  •  
  • return(
  • <div onLoad={ this.handleImageLoadBubbled }>
  • <p>
  • <a onClick={ this.toggleImage }>Toggle Image</a>
  • </p>
  •  
  • { this.state.isShowingImage && image }
  • </div>
  • );
  •  
  • },
  •  
  •  
  • // I toggle the inclusion of the image element.
  • toggleImage: function() {
  •  
  • this.setState({
  • isShowingImage: ! this.state.isShowingImage
  • });
  •  
  • }
  •  
  • });
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // Render the root Demo and mount it inside the given element.
  • React.render( <Demo />, document.getElementById( "content" ) );
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, each of the event bindings logs a message that includes the tag-name of the target element. If I toggle the image rendering a few times, we get the following output:


 
 
 

 
 The image load event bubbles-up in ReactJS. 
 
 
 

Awesome pants (and awesome muscles)!

Now, as a sort of "control" in this experiment, I wanted to build the same demo in an AngularJS context. Since AngularJS uses the native event system, not a synthetic event system, we should be able to see the image load event on the Image node but, there shouldn't be any bubbling-up of the "load" event to the parent container.

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Image Load And Error Events Bubble Up In ReactJS
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></link>
  • </head>
  • <body ng-controller="AppController">
  •  
  • <h1>
  • Image Load And Error Events Bubble Up In ReactJS
  • </h1>
  •  
  • <h2>
  • AngularJS "Control" Example
  • </h2>
  •  
  • <div bn-load="imageLoadBubbled( $event )">
  •  
  • <p>
  • <a ng-click="toggleImage()">Toggle Image</a>
  • </p>
  •  
  • <img
  • ng-if="isShowingImage"
  • ng-src="./flex.jpg"
  • bn-load="imageLoaded( $event )"
  • width="400"
  • />
  •  
  • </div>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.4.5.min.js"></script>
  • <script type="text/javascript">
  •  
  • // Create an application module for our demo.
  • angular.module( "Demo", [] );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I control the root of the application.
  • angular.module( "Demo" ).controller(
  • "AppController",
  • function AppController( $scope ) {
  •  
  • $scope.isShowingImage = true;
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I handle the load event WHEN / IF it bubbles up to the DIV element.
  • $scope.imageLoadBubbled = function( event ) {
  •  
  • console.log( "Image load event bubbled up to Div -->", event.target.tagName );
  •  
  • };
  •  
  •  
  • // I handle the load event on the image.
  • $scope.imageLoaded = function( event ) {
  •  
  • console.log( "Image load event fired -->", event.target.tagName );
  •  
  • };
  •  
  •  
  • // I toggle the inclusion of the image element.
  • $scope.toggleImage = function() {
  •  
  • $scope.isShowingImage = ! $scope.isShowingImage;
  •  
  • };
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I bind to the load event on the given element and invoke the given callback.
  • angular.module( "Demo" ).directive(
  • "bnLoad",
  • function bnLoadDirective() {
  •  
  • // Return the directive configuration object.
  • return({
  • link: link,
  • restrict: "A"
  • });
  •  
  •  
  • // I bind the JavaScript events to the view-model.
  • function link( scope, element, attributes ) {
  •  
  • element.on( "load", handleLoad );
  •  
  • function handleLoad( event ) {
  •  
  • scope.$eval(
  • attributes.bnLoad,
  • {
  • $event: event
  • }
  • );
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, it's the same basic idea. But, when we run this code, and I toggle the image a few times, we get the following output:


 
 
 

 
 The image load event bubbles-up in ReactJS, but not in AngularJS.  
 
 
 

As you can see, we were able to log the "load" event on the callback bound to the Image element. But, our load callback on the Div container never fired. This is because the "load" event doesn't "bubble" natively on the DOM.

I have to admit, I am fairly impressed with React's event system. It takes some getting used-to as it is definitely a large mental-shift; but, it really does make working with events quite nice. Just the fact that the image load event bubbles-up is a huge win as you can start to think about image loading in terms of event delegation within your ReactJS components.




Reader Comments

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.