Skip to content

Error while plotting after React update #52

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
jimbru opened this issue Feb 26, 2018 · 32 comments
Closed

Error while plotting after React update #52

jimbru opened this issue Feb 26, 2018 · 32 comments
Assignees

Comments

@jimbru
Copy link

jimbru commented Feb 26, 2018

Under certain circumstances, after a React update, the <Plot ../> component will throw the following error:

factory.js:131 Error while plotting: Error: DOM element provided is null or undefined
    at Object../node_modules/plotly.js/src/lib/get_graph_div.js.module.exports [as getGraphDiv] (get_graph_div.js:32)
    at Object../node_modules/plotly.js/src/plot_api/plot_api.js.Plotly.react (plot_api.js:2222)
    at factory.js:107
    at <anonymous>

I added some logging into factory.js to try and debug (modified factory.js attached below). Here's the output:

screen shot 2018-02-25 at 5 09 02 pm

My hypothesis is that if Reacts attempts to update the component quickly enough, the promise scheduled in willComponentUpdate (https://github.com/plotly/react-plotly.js/blob/master/src/factory.js#L100) may be called after React calls getRef with null and before it gets called again with the newly rendered div.


Chrome 64.0.3282.167 Mac
plotly.js 1.34.0
react-plotly.js 1.6.0

factory.js.txt

@nicolaskruchten
Copy link
Contributor

Thanks for the bug report! I'll try to replicate and add some guards here so that we don't try to render on a null ref :)

@nicolaskruchten nicolaskruchten self-assigned this Feb 27, 2018
@nicolaskruchten
Copy link
Contributor

Is this still an issue with version 2.0.0? I haven't been able to replicate it...

@paulboocock
Copy link

paulboocock commented Apr 12, 2018

This still appears to be an issue. If I conditionally render the Plot, say I remove it from the DOM whilst the API is being called to show a loading screen, then this console error occurs.

image

Chrome 65.0.3325.181 Windows
react 15.6.1
plotly.js 1.35.2
react-plotly.js 2.1.0

@d-weeteling
Copy link

d-weeteling commented Apr 16, 2018

disclaimer1: I'm somewhat of a react newbie

dsiclaimer2: This solution appeared to be incomplete for our project: it lead to some new problems.

Last thursday, I got a project to repair/improve which had exactly this bug: I solved (=circumvented) this as follows: the returned component (parent) in my render fn attaches the <Plot ...> component (child) after the parent fn componentDidMount is called. I did this using reactDOM.findDOMNode and reactDOM.render. relevant code:

componentDidMount() {
  const Plot = plotComponentFactory(window.Plotly);
  const plot = (
    <Plot
      data={this.state.combinedEvents}
      layout={this.getLayout(this.state.wantedAxes)}
      config={{ displayModeBar: false }}
    />
  );
  const that = this;
  setTimeout(() => {
    const container = ReactDOM.findDOMNode(that);
    ReactDOM.render(plot, container);
  }, 50);
}

Versions:

Chromium: v58.0.3029.110 Built on Ubuntu
react: v16.0.0
react-plotly.js: v1.7.0

@nicolaskruchten
Copy link
Contributor

@paulboocock thanks for the confirmation! Would it be possible for you to show me a minimal example that reproduces this, possibly a CodePen or something? I can't reproduce it on my end and I'm not clear on what you mean by "If I conditionally render the Plot, say I remove it from the DOM whilst the API is being called to show a loading screen" ... How are you removing from the DOM, and which API are you referring to?

@steventebrinke
Copy link

This seems to happen when you update the props before the render is ready. For example, in our app we calculate the dimensions once the page has been loaded, but this sometimes causes the Plot to be rendered twice: once with the incorrect dimensions, which fails with the mentioned error, and once with the correct dimensions. Since the second render succeeds, the UI looks correct even though an error is logged in the console.

@seangarita
Copy link

I too am running into this issue. Has anybody figured out a good way to catch it?

@babloo80
Copy link

babloo80 commented Nov 6, 2018

Hello there,
I am seeing this issue as well. And here is the error stack I got...
Error while plotting: Error: DOM element provided is null or undefined at Object.module.exports [as getGraphDiv] (plotly.js:128473) at Object.exports.react (plotly.js:136131) at factory.js:142 function.console.(anonymous function) @ index.js:1452 (anonymous) @ factory.js:155 Promise.catch (async) componentWillUpdate @ factory.js:154 updateClassInstance @ react-dom.development.js:13308 updateClassComponent @ react-dom.development.js:14914 beginWork @ react-dom.development.js:15757 performUnitOfWork @ react-dom.development.js:18761 workLoop @ react-dom.development.js:18802 renderRoot @ react-dom.development.js:18881 performWorkOnRoot @ react-dom.development.js:19788 performWork @ react-dom.development.js:19695 performSyncWork @ react-dom.development.js:19668 interactiveUpdates$1 @ react-dom.development.js:19978 interactiveUpdates @ react-dom.development.js:2328 dispatchInteractiveEvent @ react-dom.development.js:5141

Versions:
"plotly.js": "^1.42.2", "react": "^16.7.0-alpha.0", "react-dom": "^16.7.0-alpha.0", Chrome: Version 70.0.3538.77

@ArcticTee
Copy link

Bump

@nicolaskruchten
Copy link
Contributor

@antoinerg can you take a look at this please?

@brammitch
Copy link

brammitch commented Jan 3, 2019

Edit: After testing, I realized this only works on the initial render. Subsequent renders, when the LoadingSpinner component is rendered in place of the PlotlyChart component, the error will reappear.

I've had this error for awhile now and finally came up with a workaround by using code-splitting with React.lazy and Suspense from React 16.6. Here's an oversimplified example:

Before (with "DOM element provided is null or undefined" error)

import React, {Component} from 'react';
import PlotlyChart from './PlotlyChart';
import LoadingSpinner from './LoadingSpinner';

export class Dashboard extends Component {
  /* ... */
  render() {
    /* ... */
    return (
      <div>
        {this.props.loading ? (
          <LoadingSpinner />
        ) : (
          <PlotlyChart divId="dashboardChart" />
        )}
      </div>
    );
  }
}

Redux is used to track when all the data from our API has finished loading, and then render the PlotlyChart component. However, while the component was waiting for the API to finish, it would still load in the background. Since there would be no valid div to render to, the PlotlyChart component would throw an error.

After (no more errors!)

import React, {Component, Suspense} from 'react';
import LoadingSpinner from './LoadingSpinner';
const PlotlyChart = React.lazy(() => import('./PlotlyChart'));

export class Dashboard extends Component {
  /* ... */
  render() {
    /* ... */
    return (
      <div>
        {this.props.loading ? (
          <LoadingSpinner />
        ) : (
          <Suspense fallback={<div />}>
            <PlotlyChart divId="dashboardChart" />
          </Suspense>
        )}
      </div>
    );
  }
}

By using React.lazy, the PlotlyChart component will not load until is is ready to be displayed. This way it will always have a valid div to render to.

@kristian2x
Copy link

Would be great to see an integrated solution

@nicolaskruchten
Copy link
Contributor

@dmt0 can you grab this one please?

@dmt0
Copy link
Contributor

dmt0 commented Jan 11, 2019

This could probably be fixed easily. We can add a check here: https://github.com/plotly/react-plotly.js/blob/master/src/factory.js#L116 to see whether this.el exists before we react. But I'd need to reproduce the issue first, and I'm having hard time doing this. Does anyone have a minimal example of this?

@HansG89
Copy link

HansG89 commented Jan 23, 2019

Same problem here.
@brammitch could you please provide a complete solution?
tanks

@brammitch
Copy link

brammitch commented Jan 23, 2019

@dmt0 Here's a minimal example that reproduces the error:

Edit 0m7nxo0pzw

The error will occur on initial render, and everytime the # of days is changed.

@vdh
Copy link

vdh commented Jan 25, 2019

https://reactjs.org/docs/refs-and-the-dom.html#callback-refs

React will call the ref callback with the DOM element when the component mounts, and call it with null when it unmounts. Refs are guaranteed to be up-to-date before componentDidMount or componentDidUpdate fires.

It looks like someone didn't properly account for when unmounting will set the ref to null. When you do anything asynchronous inside a component, you always need to check if your component is still mounted when your promises resolve.

@AndrewUldin
Copy link

Bump!

@nicolaskruchten
Copy link
Contributor

If anyone wants to contribute a fix, we'd be happy to accept a pull request :)

@jwmann
Copy link

jwmann commented Feb 18, 2019

@brammitch Unfortunately this doesn't seem to solve this issue for me :(

@brammitch
Copy link

@HansG89 @jwmann This should work better:

Edit react-plotly.js DOM element is null or undefined -- fixed

@jwmann
Copy link

jwmann commented Feb 18, 2019

@brammitch Thanks for the quick response!
From initial view, it seems to be pretty much the same as your above example except you seem to be building your Plotly component manually.

Is that the difference or am I missing something else?

@vdh
Copy link

vdh commented Feb 18, 2019

I haven't had time to write a proper PR and test case around it, but I'm pretty certain this is due to the async happening on this line: https://github.com/plotly/react-plotly.js/blob/master/src/factory.js#L115

In all other circumstances React will guarantee that the ref is handled (and so this.el is set), but due to this being asynchronous activity inside componentWillUpdate, if the component unmounts before the promise resolves, then the element will not be available.

Something like this:

componentWillUnmount() {
      this.unmounting = true;
      // (existing code…)
}

and this:

this.p = this.p
        .then(() => {
          if (!this.el) {
            if (this.unmounting) {
              const error = new Error('Cannot plot while component is unmounting');
              error.reason = 'unmounting';
              throw error;
            } else {
              throw new Error('Cannot plot due to missing element reference');
            }
          }
          return Plotly.react(this.el, {
            data: nextProps.data,
            layout: nextProps.layout,
            config: nextProps.config,
            frames: nextProps.frames,
          });
        })
        .then(() => this.syncEventHandlers(nextProps))
        .then(() => this.syncWindowResize(nextProps))
        .then(() => this.figureCallback(nextProps.onUpdate))
        .catch(err => {
          if (err.reason === 'unmounting') {
            // Cannot plot while unmounting
            return;
          }
          console.error('Error while plotting:', err);
          this.props.onError && this.props.onError(err);
        });

should (in theory) handle and prevent the code from attempting to call Plotly.react on an unmounted component

@vdh
Copy link

vdh commented Feb 18, 2019

Actually, now that I look over it, I thought the one inside componentDidMount might have been safe but as it's also technically async activity it will probably need the same error handler.

@dmt0
Copy link
Contributor

dmt0 commented Feb 18, 2019

Looking at @brammitch's first code example, the error comes out of both componentDidMount and componentWillUpdate.

Regarding @vdh solution above, I'm worried that if we use a flag like this.unmounting to simply throw an Error, some things would not get dereferenced and garbage-collected, so we'd fix the JS error but still have the memory leak, which is part of the issue here. I'm still investigating how to do it right. This looks interesting: https://github.com/hjylewis/trashable, https://github.com/hjylewis/trashable-react

@vdh
Copy link

vdh commented Feb 19, 2019

@dmt0 What exactly are the "some things" you're worried about needing to be dereferenced? I would assume the existing code in componentWillUnmount is already handling the important cleanup.

The only thing the this.unmounting flag and wrapper does is detect if the purge is already happening, to avoid attempting a redundant update render post-purge. So unless there's some special side-effect magic happening in those other chained calls (syncEventHandlers, syncWindowResize, figureCallback?) that still need to happen post-purge, then isn't it beneficial to skip an update to nothing?

Is it important for the onUpdate callback to be triggered after an onPurge callback? I'd assume the event and resize handlers ought to be skipped since they seem only relevant to the DOM and rendering.

@dmt0 dmt0 mentioned this issue Feb 20, 2019
@dmt0
Copy link
Contributor

dmt0 commented Feb 20, 2019

Yep, all looks good, made a PR based on your solution, thanks @vdh :)

@jwmann
Copy link

jwmann commented Feb 25, 2019

When will this merge be published to npm? 😬

@dmt0
Copy link
Contributor

dmt0 commented Feb 25, 2019

Fixed released in v2.3.0. Gonna close this issue. Feel free to let us know if problems remain.

@lavor
Copy link

lavor commented Jun 29, 2020

Hi, I am still getting this errors in some circumstances. I tried to debug my situation and come to finding that library is not checking if a component is mounted in promise handlers in updatePlotly function.

Short story
Hence, whenever the component mounts and in next moment it unmounts (before all promises in updatePlotly resolves), the syncWindowResize will call Plotly.Plots.resize with null (this.el) as an argument.

What happened in my situation in details (I've put some checkpoints in comments in bellow snippet):

updatePlotly(shouldInvokeResizeHandler, figureCallbackFunction, shouldAttachUpdateEvents) {
// updatePlotly: checkpoint 1
      this.p = this.p
        .then(() => {
          if (!this.el) {
            let error;
            if (this.unmounting) {
              error = new Error('Component is unmounting');
              error.reason = 'unmounting';
            } else {
              error = new Error('Missing element reference');
            }
            throw error;
          }
// updatePlotly: checkpoint 2
          return Plotly.react(this.el, {
            data: this.props.data,
            layout: this.props.layout,
            config: this.props.config,
            frames: this.props.frames,
          });
        })
        .then(() => this.syncWindowResize(shouldInvokeResizeHandler)  // updatePlotly: checkpoint 3)
        .then(this.syncEventHandlers)
        .then(() => this.figureCallback(figureCallbackFunction))
        .then(shouldAttachUpdateEvents ? this.attachUpdateEvents : () => {})
        .catch(err => {
          if (err.reason === 'unmounting') {
            return;
          }
          console.error('Error while plotting:', err); // eslint-disable-line no-console
          if (this.props.onError) {
            this.props.onError(err);
          }
        });
  1. getRef sets ref correctly
  2. componentDidMount
  3. updatePlotly - updatePlotly: checkpoint 1
  4. updatePlotly - updatePlotly: checkpoint 2
  5. componentWillUnmount
  6. getRef unmounting sets ref to null (see https://reactjs.org/docs/refs-and-the-dom.html#callback-refs)
  7. updatePlotly - updatePlotly: checkpoint 3
  8. syncWindowResize calls Plotly.Plots.resize with argument this.el, which was set to null (step 6) and that results in Error while plotting: Error: DOM element provided is null or undefined.

Possible solution
Do not call things in promise then handler when unmounting:

// ...
 .then(() => {
  if(!this.unmounting) {
    return this.syncWindowResize(shouldInvokeResizeHandler))
  }
}
// ...

Alternative solution
Do not use this.unmountig at all and implement solution with cancelable promises:
https://reactjs.org/blog/2015/12/16/ismounted-antipattern.html

@lavor
Copy link

lavor commented Jul 10, 2020

@dmt0 Can you look at my comment above? Sholud I open new issue?

@lavor
Copy link

lavor commented Aug 28, 2020

I opened a new issue #199.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests