Note: React Hot Loader 3, released a month after I published this article, solves most of the problems described in this post. Give it a try!
My goal was to bring a live editing environment that preserves component state and handles errors gracefully to as many React users as possible.
By all reasonable metrics, React Transform has been a success. If anything, it proved the demand for a better development experience.
I would even say it has been way too popular for such an experimental and unpolished piece of software. This caused some pain for the people who felt pressured to adopt it and experienced problems with configuration.
I am sorry about this, as I had no time to focus on the experience of setting up the tool. There were, and still are, too many low level problems that need to be solved first before addressing the high level problems.
In fact I’ve come to realize that it is not a viable solution in the long term, and I plan to sunset React Transform in a few months in favor of a different project. You might think I have a habit of killing off my projects after they become successful. I assure you that this is not the case.
In this post, I will explain how I came to this conclusion, as well as describe my journey in understanding how hot reloading can be implemented, and the challenges I faced, in more detail than I did before. I hope this will be helpful to anyone who is curious where the magic comes from.
The underlying problem is that I’m inexperienced and I experiment in the open. If I make a mistake, it takes me some time to see that mistake for what it is, and even more time to come up with a solution.
React Transform is a mixed bag. It has some good ideas, but also has a bunch of fundamental problems. The good news is that I think these problems are soluble.
But first, let’s turn to the past.
It’s going to take quite a while so grab some snacks 😉
Trigger warning: accidental complexity.
Today I will not discuss any tools that integrate with the browser engine. Personally, I am only interested in “userland” hot reloading technology. If you see this as an artificial limitation, you might be interested in Amok. It can do some incredible things like replacing functions inside closures thanks to its integration with the browser engine.
There are also some projects that use some of the hot reloading code I released under the hood but differ in their approaches to some questions. For example, LiveReactload is a separate project but it shares some parts of its implementation with React Transform. I’m not going to discuss such projects in this post as I only can tell you about my experience and the problems I have personally encountered.
React Hot Loader
React Hot Loader was my first popular open source project and, to my knowledge, the first tool that allowed you to edit React component files without losing the state or unmounting the components.
How It Started
Like many other projects, React Hot Loader started with a question. I saw that Webpack Hot Module Replacement and React can be combined together in an interesting way, and I was excited.
You might have seen the video I used to introduce React Hot Loader:
The truth is it predates the real React Hot Loader. It was recorded at 6a.m. the day (or, rather, the night) I got the proof of concept working, and it relied on some horrible global variables I put inside the React source code just to record the video. Of course I didn’t tell that to anyone 😉.
I did not expect a lot of feedback but after Christopher Chedeau retweeted it my Twitter notifications went through the roof. I went from 12 to a 100 Twitter followers in a day so I knew people were as excited about it as I was.
I figured that I needed to turn this into a real project on a vacation, and that’s how React Hot Loader came to be a few weeks later.
First Attempt: Using HMR Directly
Before React Hot Loader, my first thought was that I could use Webpack HMR to replace the root component of my app and re-render it with React.
Note that HMR is not specific to React at all. James Long explains HMR well in this article. Basically it’s just a way for modules to say “When a new version of some module I import is available, run a callback in my app so I can do something with it”. This happens every time you save a file when HMR is enabled.
A vanilla HMR implementation of hot reloading a React app looks like this:
As long as you configure Webpack to enable HMR, the code above works fine and redraws your application when a hot reload occurs without refreshing the browser.
This implementation doesn’t use React Hot Loader, React Transform, or anything else—it’s just vanilla Webpack HMR API. It does not change the semantics of your React components. HMR is just a fancy way to poll the development server, inject <script> tags with the updated modules, and run a callback in your existing code.
Updates to nested components work because of how Webpack HMR API is designed. If a module does not specify how to handle updates to itself, the modules that imported it also get included in the hot update bundle, and the updates “bubble up” until they are all accepted by all the modules importing them. If some of the modules don’t end up being accepted, the hot update fails and a warning is printed. To “accept” a dependency, you just call module.hot.accept(‘./name’, callback) which Webpack parses at compile time.
Because we accept updates to App.js inside index.js, we also implicitly accept updates to anything imported from the App.js—such as other components.
For example, let’s say that I edit a Button.js component which is imported by UserProfile.js and NavBar.js. Let’s also say that those modules, in turn, are both imported by App.js.
Because index.js is the only module importing App.js and it includes a module.hot.accept(‘./App’, callback) handler, Webpack will generate an “update bundle” with all those files and then run the callback we supplied.
When we get an updated App.js, we just want to re-render the React app:
In a way, yes.
But not really.
I told you it’s a long story! 🙅
Problem: DOM and Local State are Destroyed
When I say “next version of a module” I just mean that the module code is evaluated again in another <script> tag.
Because the App.js module gets re-evaluated, the class identity does not match the previous component class. So NextApp !== App even though technically they represent different “versions” of the same component.
As far as React is concerned, you’re trying to render a completely different type of component, so React will unmount the previous one. This makes sense: React can’t magically “change” the type of existing instances to a new class even if it wanted to!
This is why, with the approach described above, React destroys both the DOM and the local state of your components.
Possible Solution: Externalize the State
James Kyle recently pointed out that apps with a single state tree, such as Redux apps, don’t benefit as much from preserving the component local state. Often all you really need in such apps is to hold onto the external state tree.
In Redux apps, the complexity of preserving the local React component state might not be worth it because most of the state we’d like to keep in such apps is outside the components anyway.
Inspired by the conversation with him, I prepared this PR that removes React Transform from Redux examples in favor of vanilla HMR API.
You can even have a decent development experience with full reloading when you have a single state tree. For example, you can save the Redux state to localStorage in development, read it before initializing the store so even Cmd+R doesn’t wipe it out, and call it a day.
Many apps can’t afford or don’t want to hold state in a single atom, and I think this is fine. I happen to think that we should not give up on preserving React local state just because some people don’t need it. 😉
However if you do use something like Redux, I strongly suggest you to consider using vanilla HMR API instead of React Hot Loader, React Transform, or other similar projects. It’s just so much simpler—at least, it is today.
Preserving the DOM and Local State
Now you see why naïvely replacing a React component with a re-evaluated version of it destroys the DOM and state of the existing instances.
I see two different ways to fix this:
- Devise a way to detach React instances from DOM nodes and state, create the new instances of the new component types, and somehow “attach” them to the existing DOM and state recursively.
- Alternatively, proxy component types so that the types that React sees stay the same, but the actual implementations change to refer to the new underlying component type on every hot update.
Please let me know if you are aware of any other solutions.
Failed Attempt: Rehydrating the Tree
The first approach is probably better in the long term but React currently provides no capabilities to (de)hydrate the state of React tree and replace the instances without destroying the DOM and running the lifecycle hooks. Even if we reached into the private React APIs to accomplish this, we would still face subtle problems with the first approach.
For example, React components often subscribe to Flux stores and other sideways data sources in componentDidMount. Even if we had a way to silently replace old instances with the new instances without destroying their DOM or state, the old instances would still keep their existing subscriptions, and the new instances would not be subscribed at all.
To subscribe the new instances, we would need to also fire lifecycle hooks on the them during the replacement operation. However then componentDidMount would run twice for the same DOM nodes which might break some assumptions that React components tend to make. In case of third-party components, you wouldn’t even be able to work around those assumptions because it is not your code!
Ultimately, the first approach probably be more feasible if React state subscriptions were declarative and independent of the lifecycle hooks, or if React did not rely so hard on classes and instances. Both may be the case in the future versions of React, but we’re not there yet.
Successful Attempt: Proxying Component Classes
The second approach is the one I use in React Hot Loader and React Transform. It is fairly invasive and changes semantics of your code but it gets the job done with today’s React, both in greenfield and legacy React applications. In most cases, it works great.
The idea is that every React component class gets wrapped into a “proxy”. In this case, I don’t mean an ES2015 Proxy although I’m definitely looking forward to using it instead of an ad-hoc approach I came up with.
Those proxies are just classes that act just like your classes but provide you the hooks to inject the new implementation of the class, at which point the existing instances start to act like the new version of the class. This way both React local state and DOM are preserved.
Problem in Retrospect: Lack of Tests
There are many non-trivial parts to creating a correct proxy, and React Hot Loader didn’t do it well because it didn’t have any tests. New versions of React used to break it all the time, and not writing a comprehensive test suite is the mistake I regret the most about React Hot Loader.
My latest work on proxying React components is available as a library called React Proxy. It is extensively tested, used inside React Transform but is low-level enough to be used separately. It only implements proxies for React components so it doesn’t depend on either Webpack or Babel. For example, I’m aware of people using it inside Electron apps as well as inside big projects with custom build systems.
Problem: Where to Wrap Components in Proxies?
Fun trivia: React Hot Loader is not a “loader” because it implements hot reloading. This is a common misconception. 😀
Similarly, React Hot Loader is also a compile time transform, and a rather dumb one. It wraps every React component that it finds in module.exports into a proxy, and exports the proxied components instead.
With this approach, when <App> renders <NavBar>, it really renders <NavBarProxy> (you wouldn’t know it though!) that calls NavBar#render() from its render() method and changes the internal reference to the newest version of NarBar whenever an updated NavBar.js module executes.
The proxies are stored globally by reasonably-unique IDs derived from the filename and displayName of the component classes. When an updated class is evaluated, the matching proxy is updated to “absorb” the new version of the class, and the components re-render without losing the local state or the DOM.
Looking for components in module.exports sounds like a reasonable approach at first. Developers usually keep every component in its own file anyway so naturally all components are exported. However, as time went by, and especially as new approaches became adopted by React community, I discovered some problems inherent to this approach:
- As higher order components became popular, people started to export the higher level component wrappers instead of the actual components they write. As a result, React Hot Loader didn’t “see” the inner components in module.exports and didn’t create proxies for them. Their DOM and local state would get destroyed on every change to the file. This is especially frustrating for higher order components that provide styling such as React JSS.
- React 0.14 introduced functional components which encourage micro-componentization inside a single file. Even if React Hot Loader inspected toString() of exported functions, looked for createElement() calls and assumed that these functions are React components, it still wouldn’t “see” the local components that aren’t exported. Those components would not become wrapped in proxies, and thus would cause the entire subtrees below them to lose both DOM and state. This jeopardizes the whole idea of preserving the state because it makes it extremely fragile.
Merely asking developers to export those “hidden” components isn’t enough because they are referenced inside the file! If we want to use the proxies, we have to somehow make sure that even references inside the file refer to those proxies instead of the actual components, which we obviously can’t do by overwriting module.exports.
At this point many people were saying: forget about it. Just use a global state solution and don’t attempt to preserve the local state.
Maybe I should have listened 😉.
Problem: Webpack Dependency
I worried: what if other bundlers don’t implement HMR? What if Webpack dies and gets replaced by something else? What about React Native which has been notoriously difficult to make friends with Webpack?
My intention was to at least make it possible for somebody else to use my work outside of Webpack—even if they had to also spend some time to hook that solution up to their bundler.
About the time that I wrote The Death of React Hot Loader I was looking for ways to fix those problems.
Additionally, I was looking for a way to implement error handling. Hot reloading wasn’t very useful when every error thrown inside render() threw React into an invalid state, and I wanted to fix that.
Both wrapping component in a proxy and wrapping component’s render() in a try / catch sounded like “function that takes a component class and does something with it” to me.
So I thought: why not create a Babel plugin that locates React components in your codebase and wraps them in arbitrary customizable transforms?
Wouldn’t it be cool if other people created other developer-time transforms, for example, to overlay components with performance heatmaps?
This is how React Transform came to be.
First Approach: Modularity Overkill
I wasn’t sure which projects and ideas would still be relevant after a while so I decided to take an opposite approach to React Hot Loader. Instead of just one, I created five different projects under the “React Transform” umbrella:
- React Proxy implements low-level proxies for React components.
- React Transform HMR creates a proxy for a passed component and keeps a list of proxies on a global object so they are updated when the transform is called again for the same component.
- React Transform Catch Errors wraps the render() method in a try / catch and displays a configurable component instead if there is an error.
- Babel Plugin for React Transform does its best to find all React components in your codebase, extract information about them at compile time, and wrap them in the transforms that you chose (e.g. React Transform HMR).
- React Transform Boilerplate showed how these technologies work together.
Problem: Too Many Moving Parts
This approach became both a blessing, as it allowed easier experimentation, and a curse, as end users were increasingly confused about how those projects relate to each other. There were too many moving parts exposed to the user: “proxies”, “HMR”, “hot middleware”, “error catcher”, etc.
I made a mistake of hoping Babel 6 would come out soon with “presets” and I’d just be able to ship a preset with the preconfigured sane defaults.
However Babel 6 release came much later than I expected. (I’m not complaining because it was a terrifically complex release and Sebastian was burned out from the project, and did his very best in shipping it, for which I am very grateful.)
React Transform become popular way faster than I expected, and the complicated config I hoped would go away soon kept multiplying in the boilerplate projects, confusing new users. The fact that we included it in Redux examples made it even worse.
When Babel 6 came out, it turns out that the plugin needed a rewrite because I didn’t really know how to write Babel plugins and did it very sloppily. Luckily, James Kyle graciously rewrote it completely for v2.0 before ultimately deciding that the project is a bad idea and resigning from participating in it. Good thing he realized this after writing a v2.0! 😁
Solution: Sane Defaults
After the Babel 6 update and publishing an official preset, the modularity no longer was such a problem, and actually became useful because different environments (e.g. React Native) were able to take the parts they cared about. The lesson I learned is that any time you modularize something, you have to provide a good “default” interface for it. Those who are interested in the internal dependencies and customization will find them anyway.
Problem: Higher Order Components Strike Again
When you solve an issue, try not to introduce the opposite issue.
React Hot Loader could see anything you export from a file but couldn’t see the “local” components declared inside it. For example, React Hot Loader would find the exported component generated by useSheet() higher order component and update the styles on change, but it would not “see” the Counter itself, and for this reason its state would get reset when the file is saved:
React Transform “fixed” this by finding component declarations with static analysis, looking for classes that inherit from React.Component or are declared using React.createClass().
Guess what it’s missing? The exported components! In this example, React Transform would preserve the state of the Counter component and hot reload changes to its render() and handleClick() methods, but any changes to the styles would not get reflected because it doesn’t know that useSheet(styles)(Counter) happens to also return a React component that needs to be proxied.
Most commonly people discover this problem when they notice that their selectors and action creators in Redux are no longer hot reloaded. This is because React Transform does not realize that connect() returns a component, and there is no easy way to tell it.
Problem: Compile to JS Languages Don’t Babel
Naturally, using a Babel plugin excludes users of the compile-to-JS languages. Even if we told them to use Babel as part of their toolchain, we would still not be able to reliably find their class abstractions because of what they compile to. Sure, technically anything is possible but supporting this would probably be a nightmare because we would be relying on implementation details of third-party compilers.
Problem: Wrapping Components Statically Is Invasive
Finding classes that inherit from React.Component or created using React.createClass() is not too hard by itself. However it is potentially error prone, and you really don’t want to introduce false positives here.
With React 0.14, the task is even harder. Any function that returns a valid ReactElement is potentially a component. But you can’t be sure so you have to apply heuristics. For example, you can decide that any function in the top-level scope that is named in PascalCase, uses JSX and accepts no more than two arguments, is probably a React component. Are you going to get some false positives? Yea, probably.
Bad. Even worse, now you have to teach every “transform” to deal both with classes and functions. What if React introduces yet another way to declare components in v16? Would we have to rewrite all the transforms?
Introducing support for functional components has been the most requested feature in React Transform but I just don’t see it happening at this point because of the complexity it would impose on the project and its maintainers, and the potential for breakage due to edge cases.
Shall we give up at this point?
The Road Ahead
I still see both React Hot Loader and React Transform as successful projects despite their inherent flaws and limitations. I believe that great hot reloading experience can be achieved in React, and that we shouldn’t stop trying. In fact, it’s the first time in months that I felt optimistic about hot reloading React components.
React Native ships a hot reloading implementation based on React Transform very soon. It’s stable enough for now, but I also believe that we need a better and simpler solution looking forward.
Below I will describe this solution the way I see it.
Solution: Use Vanilla HMR If You Can
This is a straightforward one.
As James Kyle suggested, if you keep all your state in a single store like Redux, and don’t care too much about preserving the DOM on reloads, consider dropping React Transform and using vanilla HMR or something like isolated-core.
It will make your project simpler!
Solution: Drop Configurable Transforms
Configurable transforms were useful while React Native shipped with a fork of React and didn’t have an HMR implementation, but now it works with react package as is, and implements the necessary subset of HMR as well.
Browserify also now has an HMR plugin which, admittedly, is not bug-free, but I feel much more comfortable about using HMR as the “default” in the project. I think at this point it’s fair to ask other environments to polyfill it like React Native did.
React plans to ship an official instrumentation API that makes use cases like profiling or inspecting components much easier to implement in userland without resorting to wrapping them. In fact, doing this via DevTools API will be much more reliable, as the instrumentation code would be separated from the running application.
This is why I think the idea of highly configurable third-party transforms was a fundamentally flawed one, and we need to drop it. Let’s focus on hot reloading instead.
Solution: Use Error Boundaries
React v15 ships with an initial implementation of error boundaries which let components up the tree catch and display errors that occurred while rendering the components below the tree.
Effectively this means that wrapping components’ render() methods in try / catch blocks should no longer be necessary because you can implement your own <CatchErrors> component and put it at the top of your hierarchy and display errors like React Native does. We’ll probably ship such component together with the future hot reloading solution.
Solution: Proxy at the Call Site
I believe this to be the missing piece and the biggest mistake I made with React Transform. In fact Sebastian Markbåge told me in October that he wasn’t quite impressed with my Babel plugin but I didn’t understand his advice fully until I re-evaluated it a few days ago. The solution was right under my nose all this time.
Finding and wrapping React components in the source is hard to do, and is potentially destructive. This really might break your code.
On the other hand, tagging them is relatively safe. Imagine a Babel plugin that adds something like register(uniqueId, Component) at the end of the every module for anything that looks remotely like a component. For example, we can do this for every top-level function, class, and export:
What would register() do, then? The implementation I have in mind will check whether the passed value at least a function, and if it is, will create a React Proxy around it. It will not, however, replace your class or function! This is the important bit. The proxy will just sit in a global map, patiently waiting until you use… React.createElement().
If we got to React.createElement(), it’s the proof that whatever you pass there as a type really is a React component, no matter how it was declared.
As long as React Proxy supports all types of components (and it already supports both classes and functions), we should be fine. This is why we monkeypatch React.createElement() to refer to our internal map and to use the proxy instead of the passed type. It might look roughly like this:
With some changes to React Proxy necessary to get instanceof and similar checks right both inside and outside the component module, we should be able to get a reasonable approximation of the hot reloading behavior React Transform currently supports, and solve its many issues at the same time:
- There won’t be any false positives for components because wrapping occurs only for what is actually used as a component.
- We can “find” functions, classes, createClass() calls, and every export, and we don’t need to worry about writing complicated tree traversal logic to wrap different things correctly.
- Changing functional components will not reset the DOM or the state of the children trees.
- We can release React Hot Loader 2.0 that uses the same technique under the hood, but without the static analysis. It would only work for exported components, but this will be immediately useful to the compile-to-JS languages which are currently not supported by React Transform at all.
- The generated code would be easier to work with because we can move the generated lines at the end of the file instead of polluting the code like React Transform currently does.
I might be wrong here, and there still might be some problems that I missed, but overall this seems like a big step in the right direction to me, and all the groundwork like having a good proxy implementation or the code to find the createClass() calls is already done. We just need to remove the parts we don’t need, such as wrapping, and fix the edge cases.
If you plan to work on something like this, please let me know so I can help. If you don’t, well, I work at Facebook now, so maybe I can spend some time on this as well 😀.
Hopefully, with all the lessons learned, we’ll have better tooling for hot reloading in 2016. Once we get the low level implementation right, we can make the setup experience user-friendly. And someday we’ll clean up these hacks, and make React friendlier to hot reloading out of the box. But for now, being a step ahead of the accidental complexity is good enough to me.
A Little Update