Hot module reloading is a great way to improve developer experience, hitting save and seeing the output with the snap of a finger is great.
HMR by default can't really assume how a front-end framework works so it isn't able to just work out of the box, here solutions like react-hot-loader
, react-fast-refresh
and prefresh
come into play.
In this article we'll mainly talk about prefresh
and react-fast-refresh
since the philosophy used by react-fast-refresh
is the base for prefresh
.
Well how does it work? This is a three-parter, we'll have the code-transformation part (Babel), the bundler part (we'll use webpack) and the actual runtime.
Code-transformation
In prefresh
we use react-refresh/babel
to perform this transformation for us. This will insert two methods:
- register
- sign
register
will be inserted after every Component
and will tell us what functions are declared within a file as well as their name.
Imagine the following code fragment:
const App = () => {
const [count, setCount] = useState();
return (<p>{count}</p>)
}
Here the babel plugin would insert register(App, 'App')
. This helps us build up a registry of components which we can identify by file, ...
The sign
function is a higher-order-function which will be used to create an instance for every component within the file. This instance will be used to calculate a unique implementation signature for a component or custom-hook.
So for instance a Component with a custom-hook will create a signature for that custom-hook and will also sign that custom-hook. This way we can see when changes happen to either of these.
The component changes arguments it passes to the custom-hook? The signature has changed.
The implementation of the custom-hook changes? The signature changed.
When the signature changes drastically we can't preserve the state of the component which is being swapped out, this could result in undeterministic behavior.
Here's an example illustrating this transformation.
Bundler
In the code-transformation part we saw that we utilised two functions: sign
and register
, these aren't just magically available. We need to provide them to our modules, this is the responsibility of the bundler. The bundler has an additional responsibility and that's hot-module-reloading itself, this is mostly available in dev-servers like webpack-dev-sever
or the webpack HMRPlugin.
To achieve providing sign
and register
we'll have to inject code into every module, this code has to safely reset itself so we don't leak into other modules.
const prevRefreshReg = self.$RefreshReg$;
const prevRefreshSig = self.$RefreshSig$;
self.$RefreshSig$ = () => {
return (type, key, forceReset, getCustomHooks) => {
// Call runtime with signed component
};
};
self.$RefreshReg$ = (type, id) => {
// Register Component in runtime
};
try {
// Here's your code, your bundler will wrap the module you provided it with.
} finally {
// Restore to prevent leaking into the next module.
self.$RefreshReg$ = prevRefreshReg;
self.$RefreshSig$ = prevRefreshSig;
}
Now we've ensured that the code injected by the babel-plugin actually calls a valid function.
There's a bit more that we need to do inside of this plugin and that's react to hot-updates. In our case we only want to have files that contain Components hot-reload since these are the only ones our runtime will be able to react to.
This comes down to injecting:
if (module.hot && hasComponents(module)) {
const previousHotModuleExports =
module.hot.data && module.hot.data.moduleExports;
if (previousHotModuleExports) {
try {
runtime.flushUpdates();
} catch (e) {
self.location.reload();
}
}
module.hot.dispose(function(data) {
data.moduleExports = __prefresh_utils__.getExports(module);
});
module.hot.accept(function errorRecovery() {
require.cache[module.id].hot.accept(errorRecovery);
});
}
You might wonder why we aren't wrapping custom-hooks in these HMR-boundaries, this is because HMR has a concept of bubbling. When we save on a custom-hook it will bubble up, we only use hooks inside of components so this will bubble up to all Components importing this custom-hook (or to nested custom-hooks and up to Components using that).
This connects the dots from our HMR to the runtime, but what does this runtime actually do. How does the virtual-dom allow us to manipulate HMR?
Runtime
Now that we're getting to the final part we're straying away a bit from how React handles this runtime. This runtime is specific to Preact and won't be a 1:1 mapping with how React does it.
A first thing to understand is that the Components we've been wrapping in the above examples don't map to one virtual-node, they map to several since a component can be used more than once. This means that inside of our runtime we need a way to track which Component maps to which virtual dom-nodes.
In Preact specifically we have a concept of option hooks (yes Marvin the secret is out). In our case we can use the vnode
option which will fire every time Preact creates a virtual dom-node. All of these nodes have a property called type
which represents a function signature and this function signature is what we've been wrapping in all of the above, the Component. This means that now we have a way to map a Component to an array of Virtual dom-nodes.
This actually means that we already have a lot since every time we hot-reload we'll see a set of register
calls, these calls imply modules that are being hot-reloaded. All that's left at this point is a flush.
A flush means that we'll observe all these register
calls, get the Components. All of these Components map to a set of Virtual dom-nodes, we can iterate over these and swap out their current .type
for the new one, this ensures the vnode will use the new component-code. When we've swapped these old implementations out we can check whether or not this component has changed signature and reset the hooks-state accordingly. Finally we'll call the infamous forceUpdate
method and see the new result on our screen.
Concluding
I hope you've enjoyed this insight into fast-refresh, please ask any questions you like on Twitter or here in the comments.
You can find all Prefresh integrations here.
Top comments (0)