Skip to content
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

react 15.1.0 devtools error #6839

Closed
ellenc345 opened this issue May 23, 2016 · 20 comments
Closed

react 15.1.0 devtools error #6839

ellenc345 opened this issue May 23, 2016 · 20 comments
Labels
Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug

Comments

@ellenc345
Copy link

warning.js?8a56:44Warning: exception thrown by devtool while handling onUnmountComponent: Cannot read property 'type' of undefined

since updating to react 15.1.0 I'm getting the above error multiple times

image

@gaearon
Copy link
Collaborator

gaearon commented May 23, 2016

Hi, thanks for reporting! Can you publish a minimal project reproducing the issue?

@omerts
Copy link

omerts commented May 23, 2016

@gaearon (I am on the same team as ellenc345)
I have investigated the above issue a little.

We have custom hooks on the debugTool.
After upgrading to React 15.1.0, the onUnmountComponent hook, is passing an integer as the handler's parameter, instead of a component instance.

const eventHandlers = {  
  ...
  onUnmountComponent(internalInstance) {
    console.log("Instance: ", internalInstance, typeof internalInstance)
    ...
  }
}

ReactInstrumentation.debugTool.addDevtool(eventHandlers)

Console output:
image

@jimfb
Copy link
Contributor

jimfb commented May 23, 2016

Sounds like a userland issue. Devtool was never public/documented API, and we are still iterating on the API so we do not guarantee stability across even patch releases. I'm going to close this out, unless you are able to reproduce with only the standard devtools.

With regards to the specific point (numbers instead of instances), this is intentional. The internal instances are intended to be internal to React, and third party tools should only be using the reference for identity. We transitioned to using IDs in order to enforce this, by not actually exposing the internal instances at all.

Feel free to continue the discussion on this thread. We can re-open if this turns out to be an actual bug in the core.

@jimfb jimfb added Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug and removed Type: Bug labels May 23, 2016
@jimfb jimfb closed this as completed May 23, 2016
@omerts
Copy link

omerts commented May 24, 2016

@jimfb, thanks for the response.
Is the _debugId guaranteed to be unique, and consistent across renders? In other words, if I re-render a different view, could another component get that same _debugId? Also, if I re-render the same page over, and over again, would the same component always get the same id?

@gaearon
Copy link
Collaborator

gaearon commented May 24, 2016

In other words, if I re-render a different view, could another component get that same _debugId?

They are unique during a single session, as they are assigned by an ever incrementing counter. So no, it can’t be reused.

Also, if I re-render the same page over, and over again, would the same component always get the same id?

No, it is possible that the IDs will change. There are no guarantees about the creation order of components.

@omerts
Copy link

omerts commented May 24, 2016

So it sounds like the _debugId is useless if I want to match an instance of a component between two different renders. With the instance we built our own unique, persistent key, but now that possibility is gone.
It also made the onUnmountComponent hook useless.

Also, as a side note, even though it is not public API, you did change the API to be able to work with multiple devTools (aka ReactInstrumentation.debugTool.addDevtool(eventHandlers)), so when changing the API please take into consideration uses of other devtools, and not just the use cases of react devtools. We used this API to time-travel inner component state through the redux-devtools.

@gaearon
Copy link
Collaborator

gaearon commented May 24, 2016

With the instance we built our own unique, persistent key, but now that possibility is gone.

How did you build a persistent key based on an internal instance? It is not clear.

It also made the onUnmountComponent hook useless.

Why?

Also, as a side note, even though it is not public API, you did change the API to be able to work with multiple devTools (aka ReactInstrumentation.debugTool.addDevtool(eventHandlers)), so when changing the API please take into consideration uses of other devtools, and not just the use cases of react devtools.

We encourage experimentation with the API, but the mere fact that it allows adding several devtools doesn’t mean we won’t change it again and again. As you said, it is not a public API, and to make it useful we need to iterate on it very fast before declaring it public and letting people rely on it.

We don’t make changes to it for no reason. Exposing internal instances is dangerous because it blocks some work on the new reconciler (#6170) which is a big goal for React this year. We are effectively creating a fork of React inside React so that the logic can be written in a different way, and reconciliation can be done in multiple passes. This means any internal structures are likely to change anyway. For all I know, internal instances might not even get created in some cases when we know it’s unnecessary.

If a long chain of tools (including our own!) comes to depend on internal structures, the transition to the incremental reconciler will be very painful. Even React DevTools currently reaches into internal instances, which is problematic because it has to maintain a module that tries to make sense of them which will break as soon as we change the private field names, or skip instance creation for some cases. This is why we needed to add an API that would be instance-agnostic as soon as possible. It exposes events like onMountComponent(id), onSetText(id), onSetDisplayName(id), onSetChildren(id), etc, which lets us keep the tooling decoupled from the reconciler. We will be gradually transitioning React DevTools to use this new devtool API as well.

We used this API to time-travel inner component state through the redux-devtools.

It sounds cool, but can you help me understand:

  • Why do you need the internal instances to generate a unique key, and how do you guarantee that it is indeed unique across refreshes?
  • How does unique key across refreshes help you implement time travel?

Thanks.

@omerts
Copy link

omerts commented May 24, 2016

@gaearon, thanks for the detailed response.

Why do you need the internal instances to generate a unique key, and how do you guarantee that it is indeed unique across refreshes?
How does unique key across refreshes help you implement time travel?

Since _rootNodeID was deprecated, and the is no other unique component id, we need the internal instances to generate our own unique keys (we actually needed them before too for getting the _rootNodeID). We guarantee that it is persistent, and unique (enough for our need), by generating a key based on the component's name,and location in the component tree (yes we iterate up the tree, and possibly get quite a long key, but it works and is only for devTools).
In short (long answer below), we need a unique, and persistent key across renders in order to be able to identify the right component into which we will inject the cached component state.

We are using the redux-devtools, without redux, and we have created a special adapter to interface with the redux-devtools.
In our scenario, we have a Map, with a generated unique component key as the key, and the inner-state object as the value. Whenever a component mounts, or setState is called, we store the new state under the matching component's key. We also have another Map, that updates onMount/unMount, that holds the current component instance for each unique key. When ever we pass a state object to the redux-devTools, we also pass a snapshot of the the current inner-state map, as a state injecting thunk, for later injection.

const createStatesInjectingThunk = (states) => {
  const clonedStates = new Map(states) // Inner-states map

  return () => {     
    clonedStates.forEach((state, key) => {         
      const currentComponentInstance = _componentKeyMap.get(key) // Component instance map
      if (currentComponentInstance != null && currentComponentInstance._instance) {
        currentComponentInstance._instance.state = state      
      }
    })
  }
}

So we use the unique key to manage our component instance map, and to match b/w a state, and a current component instance.

If the above is unclear, please let me know, and I will revise it.

@gaearon
Copy link
Collaborator

gaearon commented May 24, 2016

We guarantee that it is persistent, and unique (enough for our need), by generating a key based on the component's name,and location in the component tree (yes we iterate up the tree, and possibly get quite a long key, but it works and is only for devTools).

This should work now too. Check out ReactComponentTreeDevtool. It gets added automatically, and it tracks parent/child relationships. You can write something like

function getUniqueKey(id) {
  var parentID = ReactComponentTreeDevtool.getParentID(id);
  if (parentID) {
    var siblingIDs = ReactComponentTreeDevtool.getChildIDs(parentID);
    var index = siblingIDs.indexOf(id);
    return getUniqueKey(parentID) + ' > ' + index.toString();
  } else {
    return 'root';
  }
}

@omerts
Copy link

omerts commented May 25, 2016

Thank you very much again for your time :).

Is the id returned by getParentID(id), consistent across refreshes?

@gaearon
Copy link
Collaborator

gaearon commented May 25, 2016

No, we don't have such guarantees. But I am not using an ID itself: I'm using a position in the tree. I assume this is what you mean by being consistent with refreshes. Give it a try.

@omerts
Copy link

omerts commented May 26, 2016

@gaearon - Building the unique key using your function looks good. The problem now is that we don't have the instance in order to get the component's state on mount, or in order to inject the state later.
Is there any way to get the instance by the _debugId?

@gaearon
Copy link
Collaborator

gaearon commented May 26, 2016

@omerts

Not at the moment. However, we are interested in ReactComponentTreeDevtool.getState(debugID) being implemented, as it will be needed for React DevTools. Would you like to send a PR?

After implementing it, we will need to add corresponding APIs for setting props and state from DevTools, but let’s discuss them separately.

@omerts
Copy link

omerts commented May 26, 2016

Will work on a PR in the next few days :)

@omerts
Copy link

omerts commented Jun 1, 2016

Created PR #6936

@omerts
Copy link

omerts commented Jun 2, 2016

@gaearon Depeneding on ReactComponentTreeDevtool.getChildIDs to generate a unique ID, isn't good on initial mount since the onSetChildren event, which updates the childIDs in ReactComponentTreeDevtool, is the last event to be emitted during performInitialMount, and therefore generating an ID based on childIDs in other hooks (which run before), returns -1 for siblingIDs.indexOf(id);

@gaearon
Copy link
Collaborator

gaearon commented Jun 2, 2016

Can you show the code? I don't fully understand how you are using it.

@omerts
Copy link

omerts commented Jun 5, 2016

@gaearon, sorry for the late response

This is a simplified code example to reproduce the above problem:
Dev Tool

const eventHandlers = {  
  onBeforeMountComponent(debugID, element) {
    if (element.type === 'button') {
      console.log("onBeforeMountComponent", getUniqueKey(debugID), element.type)
    }
  },
  onBeforeUpdateComponent(debugID, element) {
    if (element.type === 'button') {
      console.log("onBeforeUpdateComponent", getUniqueKey(debugID), element.type)
    }
  }
}

ReactInstrumentation.debugTool.addDevtool(eventHandlers)

function getUniqueKey (id) {
  var parentID = ReactComponentTreeDevtool.getParentID(id)
  if (parentID) {
    var siblingIDs = ReactComponentTreeDevtool.getChildIDs(parentID)
    var index = siblingIDs.indexOf(id)
    return getUniqueKey(parentID) + ' > ' + index.toString()
  } else {
    return 'root'
  }
}

JSX (double render is for the onBeforeUpdateComponent to emit)

class Test extends React.Component {
  render () {
    return <div>
            <button>Test</button>
           </div>
  }
}

ReactDOM.render(<Test />,document.getElementById('root'))
ReactDOM.render(<Test />,document.getElementById('root'))

Output:

onBeforeMountComponent root > -1 > -1 button
onBeforeUpdateComponent root > 0 > 0 button

image

@gaearon
Copy link
Collaborator

gaearon commented Jun 6, 2016

onBefore* is probably too early, have you tried the hooks that run after, like onMountComponent?

@omerts
Copy link

omerts commented Jun 7, 2016

Running the code later works perfect. The thing is that in #6936, I have added a new event onStateUpdated which is raised when a component's state changes. There is a change in a component's state before the childIDs are set.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug
Projects
None yet
Development

No branches or pull requests

4 participants