react-component/m-list-view

Errors in throttle function after component is unmounted

AleCaste opened this issue · 0 comments

Hello!
We are using this <ListView> component on some of our app screens.
When we enter screen A, the <ListView> component is mounted. All ok.
When we exit screen A, we unmount the whole screen, along with the <ListView> component.
The problem is that sometimes the throttle function used by <ListView> is still active (since it is an async function) and it calls its fn but now the <ListView> is already UNMOUNTED so an error like this is raised:

Uncaught TypeError: Cannot read property 'offsetHeight' of null
    at ScrollView.getMetrics (ScrollView.js?b514:228)
    at handleScroll (ScrollView.js?b514:83)
    at eval (util.js?c94d:32)

Basically the error chain is the following:

// util.js
export function throttle(fn, delay) {
  var delayFlag = true;
  var firstInvoke = true;
  // console.log('exec once');
  return function _throttle(e) {
    if (delayFlag) {
      delayFlag = false;
      setTimeout(function () {
        delayFlag = true;
        // console.log('exec delay time');
ERROR    fn(e);    --> ERROR HERE! (the throttled fn is called when the list component has been unmounted...)
// ScrollView.js    
    value: function componentDidMount() {
      var _this3 = this;

      var handleScroll = function handleScroll(e) {
ERROR    return _this3.props.onScroll && _this3.props.onScroll(e, _this3.getMetrics());  --> ERROR HERE!  (the onScroll function is called consequently, which involves a call to getMetrics)
// ScrollView.js
  this.getMetrics = function () {
    var isVertical = !_this5.props.horizontal;
    if (_this5.props.useBodyScroll) {
      // In chrome61 `document.body.scrollTop` is invalid,
      // and add new `document.scrollingElement`(chrome61, iOS support).
      // In old-android-browser and iOS `document.documentElement.scrollTop` is invalid.
      var scrollNode = document.scrollingElement ? document.scrollingElement : document.body;
      return {
        visibleLength: window[isVertical ? 'innerHeight' : 'innerWidth'],
        contentLength: _this5.ScrollViewRef[isVertical ? 'scrollHeight' : 'scrollWidth'],
        offset: scrollNode[isVertical ? 'scrollTop' : 'scrollLeft']
      };
    }
    return {
ERROR  visibleLength: _this5.ScrollViewRef[isVertical ? 'offsetHeight' : 'offsetWidth'],  --> ERROR HERE! (getMetrics fails since the component is unmounted!!!! and thus _this5.ScrollViewRef is NULL at this stage!!)

Basically the getMetrics function fails since the component is unmounted and thus _this5.ScrollViewRef is NULL at this stage.

Obviously the above error does not happen all the time but is VERY common.
My recommendation is that:

  1. Use a throttle function like https://github.com/lodash/lodash/tree/4.1.1-npm-packages/lodash.throttle (https://www.npmjs.com/package/lodash.throttle) which has a cancel method built in. So inside the componentWillUnmount you call this cancel method of the throttle function. To me this is the recommended solution.
  2. You add a condition inside the handleScroll function to check if the component is still mounted:
    key: 'componentDidMount',
    value: function componentDidMount() {
      var _this3 = this;
      _this3.mounted = true;

      var handleScroll = function handleScroll(e) {
        return _this3.mounted && _this3.props.onScroll && _this3.props.onScroll(e, _this3.getMetrics());
      };
      ...

See the _this3.mounted set and check. Obviously on the componentWillUnmount part you should set that property to false:

    key: 'componentWillUnmount',
    value: function componentWillUnmount() {
      if (this.props.useBodyScroll) {
        window.removeEventListener('scroll', this.handleScroll);
        window.removeEventListener('resize', this.onLayout);
      } else {
        this.ScrollViewRef.removeEventListener('scroll', this.handleScroll);
      }
      this.mounted = false;
    }
  1. Similarly we have to do something similar on 'componentDidUpdate':
    key: 'componentDidUpdate',
    value: function componentDidUpdate(prevProps) {
      var _this2 = this;

      // handle componentWillUpdate accordingly
      if ((this.props.dataSource !== prevProps.dataSource || this.props.initialListSize !== prevProps.initialListSize) && this.handleScroll) {
        setTimeout(function () {
          if (_this2.props.useBodyScroll) {
            window.addEventListener('scroll', _this2.handleScroll);
          } else {
            _this2.mounted && _this2.ScrollViewRef && _this2.ScrollViewRef.addEventListener('scroll', _this2.handleScroll);
          }
        }, 0);
      }
    }
      ...

... note the _this2.mounted condition