Inferno: TypeError: Cannot read property 'flags' of null

Created on 26 May 2017  Â·  21Comments  Â·  Source: infernojs/inferno

[email protected]

Uncaught TypeError: Cannot read property 'flags' of null
    at patch (patching.js:15)
    at patchComponent (patching.js:338)
    at patch (patching.js:20)
    at patchKeyedChildren (patching.js:440)
    at patchChildren (patching.js:204)
    at patchElement (patching.js:88)
    at patch (patching.js:28)
    at patchComponent (patching.js:283)
    at patch (patching.js:20)
    at patchChildren (patching.js:195)

Details

  1. Occurs only when component is updated. If I render component only once without updating it -- no error occurs.
  2. It involves a component which calls props.onNewValue inside a constructor (but only once, on the first construction).
  3. I have a component with conditional children that are updated here. It is tricky to isolate, sorry.
bug

All 21 comments

This is most likely setState related ping @trueadm

I have seen this same issue happening in these cases:

1 - you are updating something that has not yet mounted (invalid usage)
2 - Your application crashed and then that happens after it (Inferno does not recover from runtime exception)
3 - You have some callback (maybe click) and that is callback for higher level component which does setState before child has mounted or something like that

What you could try is move this:

It involves a component which calls props.onNewValue inside a constructor (but only once, on the first construction).

Into componentWillMount?

@Havunen thanks, componentDidMount worked for me!
Meanwhile I composed an isolated reproduction in jsfiddle (quite large though).
Code below is copied from jsfiddle.

// normally these would be come in the form of ES2015 import statements
const {Component, linkEvent, createElement} = Inferno;
// =========== CONFIGS LIST ================

  const InfoLi = function InfoLi(props) {

    const iddy = props.conf.key;
    return (
      <li>
        <input
          type="checkbox"
          checked={props.checked}
          id={iddy}
          onChange={props.onChange}
        /><label for={iddy}>{props.conf.label}</label>
        <div>{props.children}</div>
      </li>
    );

  };

  class ConfigsList extends Component { 

    constructor(props) {

      super(props);
      this.state= {
        checks: props.configs.map((conf) => Boolean(conf.value)),
      };

    }

    handleCheck(index, ifChecked) {

      console.log('handle CHECK', index, ifChecked);
      this.setState({
        checks: this.state.checks.map(
          (ch, i) => i === index ? ifChecked : ch
        )
      });

    }

    handleNewValue(index, newValue) {

      console.log('handle NEW VALUE', index, newValue);
      this.props.onConfChanged({
        targetIndex: index,
        newValue: newValue,
      });

    }

    render(props) {

      return (
        <ol>
        {
          props.configs.map((conf, index) => {

            const childElement = props.configToChild && props.configToChild[conf.key];

            const child = childElement && this.state.checks[index]
              && createElement(
                childElement,
                Object.assign({}, props, {
                  conf,
                  onNewValue: (newValue) => this.handleNewValue(index, newValue),
                })
              );

            console.log('CHIIIIILD', child);

            /* OTHER ERROR: TypeError: Cannot set property '_updating' of null
            return (
              <li>
                <input
                  type="checkbox"
                  checked={conf.value}
                  onChange={(event) => this.handleCheck(index, event.target.checked)}
                /><label>{conf.label}</label>
                <div>{child}</div>
              </li>);
            /**/

            return (<InfoLi
              conf={conf}
              checked={conf.value}
              onChange={(event) => this.handleCheck(index, event.target.checked)}
            >
              {child}
            </InfoLi>);

          })
        }
        </ol>
      );

    }

  }
// ================== MAIN =================

  class ProxyEditor extends Component {

    constructor(props/*{ conf, onNewValue }*/) {

      super(props);
      console.log('CONSTRUCTOR')
      const oldValue = props.conf.value;
      const newValue = oldValue || 'BA BA BA!';
      if (!oldValue) {
        this.props.onNewValue(newValue);
      }

    }

    render() {
      return (<div/>)
    };
  }

  class Main extends Component {

    constructor(props) {

      super(props);
      this.state = {
        configs: [
          {
            key: 'customProxyStringRaw',
            value: false,
            label: 'Use proxy? (click this)',
            category: 'ownProxies',
          },
          {
            key: 'This one is needed for reproduction too!',
            value: false,
            label: 'needed too',
            category: 'ownProxies',
          },
      ],

      };
      this.handleModChange = this.handleModChange.bind(this);

    }

    handleModChange({targetIndex, newValue}) {

      this.setState({
        configs: this.state.configs.map(
          (oldConf, index) => index !== targetIndex
            ? oldConf
            : Object.assign({}, oldConf, {value: newValue})
        )
      })

    }

    render(props) {

      return createElement(
        ConfigsList,
        Object.assign({}, props, {
          configs: this.state.configs,
          configToChild: {
            customProxyStringRaw: ProxyEditor,
          },
          onConfChanged: this.handleModChange,
        })
      );

    }

  }
//==========================================

Inferno.render(
  <Main/>,
  document.getElementById('app-root'),
);

@Havunen This is similar to #895 in that it occurs (for me) when dispatching actions.

@Omlet Does the dispatch start with event? If so then its related to setState issue, and need to be fixed in v4

@ilyaigpetrov Can you test if this bug is fixed in latest alpha please? 3.9.0-alpha.1a5cdaaa, that is the version label for all packages.

I've created fiddle with 3.9.0-alpha.1a5cdaaa but it behaves the same way as previous @latest fiddle created in this thread — both don't throw error any more, but checkboxes still don't work, I guess because of #1146.

Yeah to make fiddle working we need to rewrite controlled elements logic. Its good to hear there is no error anymore :)

The problem here is that, there is callback inside component constructor. Now when that callback is called, higher order component will do new render cycle and during that cycle: the current component is null because it is not yet constructed :(

I experienced this too with inferno-router.

What happened

On a route component, I render a simple div that says hello. I used componentDidUpdate and componentDidMount to call a method that basically checks a particular parameter on the url via this.props.match.params with /my-route/param1.

Sequence:

  1. User clicked on a link.
  2. User is taken to a THIS (where I am experiencing this error) route.
  3. DidMount fires.
  4. The app verifies if the param1 is valid (if it's not, the user gets redirected back to the homepage, if it is, it updates the topbar's title via the state management library) that I'm using.
  5. User manually edited the param1 on the url, then pressed enter (I noticed that when this happens, instead of remounting, the route component gets updated instead, that's why I have that componentDidUpdate handler).
  6. DidUpdate fires.
  7. The app noticed that the param1 has changed, so it check again if it's valid.

Only 2 things can happen here.

  1. param1 is invalid, the user gets redirected to the homepage (this does not produce the error).
  2. param1 is valid, the topbar's title is updated via an action (since the topbar's state lives on the global state level).

However, the error does not happen on ComponentDidMount wether param1 is valid or not. It only happens on componentDidUpdate WHEN param1 is valid and the topbar's title got updated.

Here's the catch, I can actually click on the x button to close the error, right? When I do, I see that the topbar's title was updated.

I am using v5.3.0

@sprlwrksAprilmintacpineda Are you able to create some place to reproduce the issue you are having, otherwise its difficult to create test case for it.

@Havunen not yet, I think these kind of error occur during multiple component updates. You can actually get around by setTimeout.

Also, it's not a good idea to do any state updates before the component mounts, you should ensure that the component has mounted before any state changes. In my case it was multiple component updates. My theory is that when I do a state change on the children and at the same time update the global store, the parent would also update, but by the time the callback that updates the children gets call the parent is still remounting so the child, whose state is suppose to change, is null.

Hey @aprilmintacpineda Do you still face this issue without setTimeout when using inferno 5.4.2 If so, it would be nice to have steps to reproduce it somewhere, I would like to fix this.

@Havunen hmm, I haven't tested it out without the setTImeout. I'll try to create a repro tomorrow after going through some errand. I think it would really be helpful to implement getDerivedStateFromProps. ReactJS also experienced the same problem but they handled it better, this is why they came up with getDerivedStateFromProps to prevent such event from taking place, maybe we should follow react's example.

getDerivedStateFromProps is invoked right before calling the render method, both on the initial mount and on subsequent updates. It should return an object to update the state, or null to update nothing.

So if we implement getDerivedStateFromProps users have a better option to handle state updates. getSnapshotBeforeUpdate would also be very helpful.

Btw, do we defer state updates?

Imagine if you have a component whose state changes based on the prop, you'll have to handle that on the componentDidMount and do setState there, which will update the component thereby running the componentDidMount again. So you have to check whether to update the state on the componentDidMount or not or else you'll be trapped in an infinite update. This is quite ineffective since you just did two updates. This can be handled better by calling the component's static property getDerivedStateFromProps.

What I am wondering about is this:

This method doesn’t have access to the component instance

They must have intentionally made it static to force users not to do anything with the component inside the method. Clever.

Hmm, looks like it's gone, I'm doing multiple state updates and without using the callbacks for the setState,

because before I do:

// update this component's state
this.setState({
  ...this.state,
  // updates
}, () => {
  // update ancestor component's state
  this.props.updateMyAncestorsState({
    // something
  });
});

Now I do:

// update ancestor component's state
this.props.updateMyAncestorsState({
  // something
});

// update this component's state
this.setState({
  ...this.state,
  // updates
});

Anyhow, keep this open for the mean time. If could not replicate it tomorrow then it must have been fixed.

How did you fixed it? what went wrong?

In inferno you can do setState in componentWillMount and componentWillReceiveProps and it wont render again only once and use new state.

About the issue there was bug that previous vNode was changed before update finished so that might have caused strange issues

But I think I will just add those lifecycle methods for next release and new createRef API too

@Havunen I can't reproduce this issue anymore.

The errors in this thread has been fixed. The original repro will not work in React either so there is something wrong in it. Inferno works same way as React so I'm closing this as working as intended now.

Here is React version behaving same way as Inferno:
https://jsfiddle.net/2ukzrbwg/

Was this page helpful?
0 / 5 - 0 ratings