Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

Notes

  • This code handles any JS runtime error during rendering React components. Without this handling, once an error occurs, whole component tree is damaged and can't be used at all. With this handling, nothing will be rendered in production environment (error span in dev env.) + in production the error is logged to Sentry (if you are not using it just delete related code)
  • This is basicaly a workaround for proposed feature in React core - described in Issue: https://github.com/facebook/react/issues/2461
  • Works for all variants of Component creation - React.createClass, extending React.Component and also stateless functional components.
  • To get this work, just put this snippet into your entry js file. Then it will work in whole application.
  • Also supporting React Hot Reload!
  • If you find this useful, please retweet https://twitter.com/Aldredcz/status/744650159942995968 :)

Ideas

  • specify custom error renderer (global / per component, e.g. by implementing method renderOnError() in a component)

Snippet code

import React from 'react';

const statelessComponentsMap = new Map(); // original -> monkeypatched stateless functional components cache
let errorPlaceholder = <noscript/>;

if (__DEV__) {
	errorPlaceholder = (
		<span
			style={{
				background: 'red',
				color: 'white'
			}}
		>
			Render error!
		</span>
	);
}

function logError(Component, error) {
	const errorMsg = `Error while rendering component. Check render() method of component '${Component.displayName || Component.name || '[unidentified]'}'.`;

	console.error(errorMsg, 'Error details:', error); // eslint-disable-line

	if (typeof Raven !== 'undefined' && typeof Raven.captureException === 'function') {
		Raven.captureException(new Error(errorMsg), {
			extra: {
				errorStack: error.stack
			}
		});
	}
}

function monkeypatchRender(prototype) {
	if (prototype && prototype.render && !prototype.render.__handlingErrors) {
		const originalRender = prototype.render;

		prototype.render = function monkeypatchedRender() {
			try {
				return originalRender.call(this);
			} catch (error) {
				logError(prototype.constructor, error);

				return errorPlaceholder;
			}
		};

		prototype.render.__handlingErrors = true; // flag render method so it's not wrapped multiple times
	}
}

const originalCreateElement = React.createElement;
React.createElement = (Component, ...rest) => {
	if (typeof Component === 'function') {

		if (Component.prototype && typeof Component.prototype.render === 'function') {
			monkeypatchRender(Component.prototype);
		}

		// stateless functional component
		if (!Component.prototype || !Component.prototype.render) {
			const originalStatelessComponent = Component;
			if (statelessComponentsMap.has(originalStatelessComponent)) { // load from cache
				Component = statelessComponentsMap.get(originalStatelessComponent);
			} else {
				Component = (...args) => {
					try {
						return originalStatelessComponent(...args);
					} catch (error) {
						logError(originalStatelessComponent, error);

						return errorPlaceholder;
					}
				};
				
				Object.assign(Component, originalStatelessComponent); // copy all properties like propTypes, defaultProps etc.
				statelessComponentsMap.set(originalStatelessComponent, Component); // save to cache, so we don't generate new monkeypatched functions every time.
			}
		}
	}

	return originalCreateElement.call(React, Component, ...rest);
};


// allowing hot reload
const originalForceUpdate = React.Component.prototype.forceUpdate;
React.Component.prototype.forceUpdate = function monkeypatchedForceUpdate() {
	monkeypatchRender(this);
	originalForceUpdate.call(this);
};
@vinnymac
Copy link

vinnymac commented Jun 19, 2016

Have you seen skiano's react-safe-render? It takes care of wrapping more than just the call to render. Interesting idea to make development different from production here, I can see that coming in handy.

@Aldredcz
Copy link
Author

Aldredcz commented Jun 19, 2016

@vinnymac Didn't see it, I guess it works nicely for createClass approach,..

But doesn't handle other variants at all. Plus this gist also cover Hot Reload, so it may speed up development significantly (without error handling, error means F5 usually)

@david-gregorian
Copy link

david-gregorian commented Jul 8, 2016

Works like a charm! Thanks 👍

@kelchm
Copy link

kelchm commented Jul 14, 2016

@Aldredcz, have you encountered any issues with the stateless functional component portion of this? Everything else seems to work fine, but I'm running into issues with this particular component. No exception occurs, but the component does not render as expected.

const validate = ({ email, password }) => {
  const errors = {};
  if (!email || email.length === 0) errors.email = 'Please provide a valid email.';
  if (!password || password.length === 0) errors.password = 'Please provide a valid password.';
  return errors;
};

const LoginForm = reduxForm({
  form: 'login',
  fields: ['email', 'password'],
  validate,
})(({
  fields: { email, password },
  handleSubmit,
  submitting,
  invalid,
  error,
}) => (
  <form onSubmit={handleSubmit}>
    {error
      ? <div className="alert alert-danger" role="alert">{error}</div>
      : null}
    <FormField label="Email" placeholder="email@example.com" className="input-lg" {...email} />
    <FormField label="Password" placeholder="Password" type="password" className="input-lg" {...password} />
    <button className="btn btn-primary btn-lg btn-block" disabled={submitting || invalid}>
      <SubmitSpinner submitting={submitting} label={<span><i className="fa fa-sign-in" /> Log In</span>} />
    </button>
  </form>
));

@Aldredcz
Copy link
Author

Aldredcz commented Jul 16, 2016

@kelchm Functional stateless component works just fine for me. What is the result of your code? Nothing is rendered at all?

@Psykar
Copy link

Psykar commented Sep 26, 2016

There are issues if a stateless component has a stateful child, as the monkeypatch replaces the entire stateless component every render, react doesn't recognize the old component as being equal to the new component, and all state on the child components is destroyed.

Additionally you're not assigning any properties that exist on the old stateless component to the new one, so you actually lose all your propTypes and defaultProps (although this is easily fixed at least - Object.assign(Component, originalStatelessComponent)

edit: Having a map of Component -> Wrapped component for stateless components works:

const originalCreateElement = React.createElement
const statelessMap = new Map()
React.createElement = (Component, ...rest) => {
  if (typeof Component === 'function') {

    if (typeof Component.prototype.render === 'function') {
      monkeypatchRender(Component.prototype)
    }

    // stateless functional component
    if (!Component.prototype.render) {
      const originalStatelessComponent = Component
      if (statelessMap.has(originalStatelessComponent)) {
        Component = statelessMap.get(originalStatelessComponent)
      } else {
        Component = (...args) => {
          try {
            return originalStatelessComponent(...args)
          } catch (error) {
            logError(originalStatelessComponent, error)

            return errorPlaceholder
          }
        }
        Object.assign(Component, originalStatelessComponent)
        statelessMap.set(originalStatelessComponent, Component)
      }
    }
  }

  return originalCreateElement.call(React, Component, ...rest)
}

https://gist.github.com/Psykar/d01f6e6e9575926768123ff7af82fd11

@Aldredcz
Copy link
Author

Aldredcz commented Oct 3, 2016

@Psykar thanks for the solution, I haven't used stateful components inside stateless ones to be honest.
I will integrate it into the gist :)

@ivansglazunov
Copy link

ivansglazunov commented Nov 9, 2016

@Aldredcz Thank you so much!

@julien-f
Copy link

julien-f commented Dec 1, 2016

Be careful, typeof Component.prototype.render: arrow functions do not have a prototype.

@julien-f
Copy link

julien-f commented Dec 1, 2016

Here is my own (ES2015) implementation.

Thanks for you work :)

@Aldredcz
Copy link
Author

Aldredcz commented Jan 4, 2017

@hyperh
Copy link

hyperh commented Feb 16, 2017

Hmm not working for me. I still get uncaught errors.

@sontek
Copy link

sontek commented Apr 4, 2017

@julien-f your solution works great for me but it blows up with HMR

@lassombra
Copy link

lassombra commented May 31, 2017

Thanks for providing this. It solved exactly the problem I was dealing with and I was just getting ready to write something like this myself. So having a mostly working one is a godsend.

I did make one change in my version, I added a test for the existence of prototype on the component, as it is true, arrow functions don't have prototype if they are implemented directly in the language (not true for babel). Since I'm using node 6 I have access to native arrow functions, which don't have prototype at least on the server. I also changed one part to ignore components which extend a certain base class, but that is implementation specific.

@Aldredcz
Copy link
Author

Aldredcz commented Jun 1, 2017

@lassombra Thanks for your comment. I thought I've already solved the prototypeless case, but I only did it in one condition.

My bad. Fixed now :)

@elmeister
Copy link

elmeister commented Nov 21, 2018

Any idea why this works only in development and doesn't work in production?
I already removed if (__DEV__) { condition obviously.

@natocTo
Copy link

natocTo commented Aug 11, 2019

If someone still use this. The "allowing hot reload" part should be:

const originalForceUpdate = React.Component.prototype.forceUpdate;
React.Component.prototype.forceUpdate = function monkeypatchedForceUpdate(callback) {
	monkeypatchRender(this);
	originalForceUpdate.call(this, callback);
};

There was no callback argument. We had weird error in React-bootstrap (0.31.5) because of this - missing initial Modal animation but only for some ones. Also React-bootstrap (0.32.5 - latest on Bootstrap 3) not work at all with this ErrorHandler hack, have no clue why.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment