Replies: 4 comments 8 replies
-
Adding this discussion from From @sophiebits
|
Beta Was this translation helpful? Give feedback.
-
Took a look at this today on my flight: https://github.com/bvaughn/react/tree/devtools-get-current-component-stack-spike If we wanted to move forward with it, it would likely mean forking React DevTools (for now) as I don't think the team would want to add and support this new API. That's probably okay though, especially as a short term / medium term thing. There is something we should discuss though: I don't think we can support component stacks for production React apps, only DEV ones. Most of React's "current fiber" tracking is DEV only, with the exception of the Let's chat about how to proceed! |
Beta Was this translation helpful? Give feedback.
-
A few of us (@jaril, @jasonLaster, @jcmorrow, @loganfsmyth, @markerikson) spoke in person this afternoon about the component stacks topic, and I promised to write a summary of my thoughts and the constraints on my flight home so that's what this comment is about. General architectureFor a refresher on how React DevTools works, see this overview. The rest of this post will use the following terminology:
Currently, Replay makes use of a fork of devtools that listens to messages being sent from the backend to the frontend and records them as annotations. Then when viewing a recording, Replay forwards those annotations to the frontend (simulating what the backend did during recording) and the frontend displays the React component tree. If the frontend needs to talk to the backend (e.g. to inspect a component's props) Replay forwards the message to the backend to be evaluated. We may need to change the way this works so that we can support features like component stacks and profiling. More on that below. FeaturesThere are a lot of potential features we could build on top of the concept of component stacks. Some are easier than others. Here's my thinking about the order we should probably approach them in.
Note that all of the features being discussed in this post will only be supported for React development builds because React does not track or expose the concept of the currently rendering component in a way that devtools can access for production builds. (Perhaps this is a change that we could negotiate with the React team but I'm skeptical.) Let's take a look at each of these features below and talk about the steps/constraints we're dealing with. 1: Show component stackI've created an experimental branch that adds a few API methods (like import { activate as activateBackend } from 'react-devtools-inline/backend';
// API returned during DevTools initialization:
const devtoolsAPI = activateBackend(contentWindow);
// API methods can be called later, when paused at a point:
const componentStackOrNull = devtoolsAPI.getCurrentComponentStack(); The API would either return // Example component stack array
[
{
// False if this component was rendered as part of the current update;
// true if this component "bailed out" (memoized).
didBailOut: boolean,
// User-friendly display name (e.g. "AdminPanel", "Suspense", "Fragment")
displayName: string | null,
// If this node in the component stack is user code, this will be a class or function.
// Otherwise it will be null for built-ins (e.g. Fragment, Suspense).
type: React$Node | null,
},
// Other components ...
] I don't think this functionality would require changes in the Replay browser, but I think it would require changes on the Replay devtools and backend. Replay browser changes
Replay devtools changes
Note that there's one interesting caveat here: React DevTools might not be able to reliably tell if the component that's currently being inspected (paused on) rendered or bailed out. That's because React doesn't set the If this became a sticking point, we could always drop the different styling (showing whether a component rendered or bailed out) and just show the component stack with one visual style. 2: Step up/out to parent componentBuilding on top of the previous feature, we could allow users to step up the component stack (from the current component to its parents) just like they are able to step up the call stack. In order to support this, I think we'd need to expose some new functionality on the backend. A naive way to implement this would be to expose an API that seeks from the current point backwards, to the most recent time a particular function was invoked. This wouldn't handle all cases though– because a single function (or class) could be rendered multiple times within a React tree (e.g. Replay's own I think that in order to distinguish between multiple "instances" of a component, we would need some way to associate a component with its "fiber". So the API we would really need would be something more like "when was the last time this function was called that was also associated with this specific object instance?"2. I'm not sure exactly how we would build this, since a React component doesn't even have direct access to its "fiber" (but the Note that future planned changes to React1 may add further complications but I think we could address them with a similar object tracking approach. 3: Step between other render passesBuilding on top of the previous feature, we could also allow users to jump between the other times the currently selected component was rendered. In many cases, this would be the same thing as stepping between breakpoints. The difference would be shared components (e.g. Replay's Another way of viewing this is that it's basically extending Replay's existing breakpoint logic and filtering by React fibers. In other words, look at every time a function was invoked– and filter out the invocations that are not associated with a specific fiber2. I think we could leverage the new Replay functionality mentioned above to do this? 4: Step down/into childrenBuilding on top of the previous feature, we could also allow users to step down into a component's children (at least those that rendered without bailing out). There are several challenges with this:
We would need to add additional functionality to both the React devtools backend and Replay. React DevTools backendFirst, the component stack structure returned by devtools would need to be updated to include a few additional attributes: // Example component stack array
[
{
// Same attributes as before:
didBailOut: boolean,
displayName: string | null,
type: React$Node | null,
// New attributes:
// Uniquely identifies the component instance within the React tree.
// This is required because the same class/function may be rendered multiple times in a React app.
componentID: any,
// Uniquely identifies the React renderer instance on the page.
// This is required because (in rare cases) there may be more than one copy of React on a page.
rendererID: any,
},
// Other components ...
] Secondly, devtools would need to expose a new API to crawl a committed root and return a boolean noting whether a specific component was part of that render: const value = devtoolsAPI.wasComponentRenderedDuringTheCurrentCommit(rendererID, componentID); Note that this API would only be safe to call at the point when a React tree was being "committed" since any other time the fiber tree might contain pending changes that make it unsafe to traverse and reason about. Replay changesBecause of the constraint mentioned above, Replay would need to seek to a commit point before calling the Presumably, Replay could seek forward through commits until it found the next one that contained the component instance. This would work in most cases, but would not handle the case where a particular component render was never committed (due to an error or a higher priority update). I think that to rule out this case, Replay would need to only consider future commits that occurred before the next time the component instance were rendered. (Note that future planned changes to React1 would likely further complicate this though and maybe make it impractical unless React and React DevTools add new functionality to more explicitly track bailouts, which may conceivably happen in order to support the recent Timeline profiler, but I don't think it is currently planned.) 1 – Currently each fiber in the React tree has an "alternate" (a copy of the fiber). React uses this structure to clone/fork part(s) of the tree for updates while still preserving the "committed" tree as pristine (in case the update fails or is preempted). There are plans to refactor this structure to something that supports many forks of the committed tree. This will unlock functionality like pre-rendering multiple potential future states to support quick animations/transitions. This change may complicate Replay's filtering logic. 2 – Open question: Is there a way to associate a function call with metadata like this? |
Beta Was this translation helpful? Give feedback.
-
Crazy thought RE: "this only works in DEV bundles because of the current fiber ref" React has enough info (on the package export) to specify the version number running. It would be hacky but Replay could swap out a production version of React for a DEV version during the replay session if we wanted to 😅 This could change behavior (because of things like strict mode) so maybe it would be more complicated than we'd want in some cases. Interesting thought experiment though. |
Beta Was this translation helpful? Give feedback.
-
Original discussion: https://github.com/RecordReplay/devtools/discussions/3809
Beta Was this translation helpful? Give feedback.
All reactions