Reactime: Open source Chrome dev tool for tracking and visualizing state changes in React applications
This is a low-level examination of the technologies that make up Reactime's core functionality. If you're interested in more of a high-level overview, check out my other post on Reactime.
Reactime is a chrome extension which allows developers to step through a series of state changes in their app, allowing them to explore how the chain of events is firing off with a great deal of granularity. It is built on a UI playground which mimics Redux DevTools, but works for Hooks, Context API, regular old stateful class components, and now, Concurrent Mode (Don't worry if you're not familiar - we'll get to that.) Sound good? Good, let's dive in…
How does Reactime work?
At its core, Reactime is a function which exports another function which exports another function. Deep breath. The heart of Reactime lives in the linkFiber.js module - this is where the majority of the business logic lives. linkFiber.js essentially is a chain of helper functions which call each other in sequence, building out a copy of the current React Fiber tree, and checking which type of state we're working with (that is, stateful components, hooks, or context api) and have some logic which handles each case accordingly. TL;DR: Every time a state change is made in the accompanying app, the Reactime extension creates a Tree "snapshot" of the current state, and adds it to the current "cache" of snapshots in the extension.
We need to go deeper
When I said a function returning a function returning a function, what that meant was, linkFiber.js is a module which exports an IIFE which lives in index.js, and this function exports a function which wraps the root of our HTML structure, like document.getElementById('root'). By grabbing the root of the DOM elements, we are able to construct our Fiber Tree based on the hierarchy of elements in our app, traversing each branch of the tree and appropriately parsing or discarding elements as needed (for example, we choose to keep stateful component fibers but discard Suspense and other Symbol-type denoted fibers).
Parsing Hooks components & working with AST's
Depending on what type of state you are working with, the Fiber tree will be constructed differently - some properties will be "missing", some will be in other places, and some entirely new ones will appear. In an app that uses React hooks, something really interesting happens. Because a hook-based Fiber tree root will have a notable absence of the stateNode property and in its place will be a populated memoizedState, we can grab the root type and construct an Abstract Syntax Tree from the hooks structure using Acorn (a parsing library) in order to extract the hook getters and match them with their corresponding setters in an object. This object will be exported (by astParser.js) and sent back to linkFiber.js, where the hooks will be saved, and we can move on to the next child or sibling node to essentially repeat the process.
One of the most interesting design choices here is that the previous teams working on Reactime chose to implement a function that changes the functionality of the setState dispatch or hooks equivalent, and in the new setState, not only does it invoke the old functionality, but additionally updates the current state snapshot tree and sends this snapshot back to the UI. This is what allows us to view UI updates in real time when we're using the time travel features of the chrome extension. Pretty neat stuff!
Fiber root vs Concurrent fiber root
Remember in the go deeper section when I said the function needs to wrap the HTML root? This doesn't quite work in Concurrent Mode - since the setup is a little different. Concurrent Mode requires the developer to wrap the HTML root in a new React function which (under the hood) uses a chain of functions to manually create a Fiber Root, which in turn renders our component. We can then take the evaluated result of createRoot and call reactime() with _reactRootContainer to start off the process. In Concurrent Mode, if we had tried to call reactime(document.getElementById('root')) the old way, it would error out - the _fiberRoot (an invisible top-level HTML component which sits on top of the HTML structure) would not be present.
Context Mimics Flux Architecture
Reactime utilizes React Hooks and Context API with functional components, to create a single store of state using Flux design patterns, handling complex state logic with useReducer eliminating unnecessary prop drilling and the overhead of Redux implementation. Reactime uses this mimicked masterState for functional components allowing for time-travel debugging.
React Router & Browser History Caching
When time-travel debugging to a prior state from a different endpoint React Router is unable to mount components in the snapshot if the routes are not persistent. We can leverage the browser's History API to pushState() for every timeJump enabling Reactime to re-mount components referenced in the current snapshot. Recursively traversing the React Fiber Tree, we look for Router node to record the path of the current state and add a state to the browser's session history stack. The browser does not attempt to load this URL after a call to pushState() and the new URL is resolved relative to the current URL.
Issues Still As-Of-Yet Unresolved
We're still working out some kinks - we haven't gotten around yet to really extensively testing Reactime with GraphQL, Apollo and Relay. Also, try as we may, we just haven't seemed to figure out yet why the first hooks click doesn't register in the DOM. As per Reactime 2.0 collaborator Andy:
"I think I have a good idea. In the webpack config settings, create a template where you add a footer div to the very end of all the client's pages. Have the tree keep building and only create the snapshot once the footer div renders. This should in theory be the last node on all linkFiber linkedlists - this could also be where you can try and catch that first click issue with the hooks."
We'll get there - one piece at a time.
Reactime is an open-source project and you - the reader - are more than welcome to collaborate and make it even better. We would certainly appreciate any and all help! Even if you just want to try it out - play around, break stuff, put in an issue on github, check it out and let us know what you think. Make sure to download the chrome extension!
Reactime 3.0 Team