Before starting the article, I wanted to demonstrate the entire flow with a demo. You can explore the process by changing the color!
React Native, where the cross platform magic happens. But besides this fancy slogan, it has an amazing feature called Fast Refresh. You change the javascript code, the change is instantly showed up to the simulator or device screen. But how does it work under the hood. I will try to explain it below. So, lets jump straight!
Fast Refresh was introduced in React Native 0.61, replacing the older HMR system with a more reliable and React-aware layer. React native has HMR system before that but fast refresh is HMRโs tailored version for react native. To understand, weโll look at some key concepts of it.
Metro is the javascript bundler for React Native. Takes in options, an entry file, and gives you a JavaScript file including all JavaScript files back. Every time you run a react native project, a compilation of many javascript files are done into a single file.
Metro also opens a WebSocket connection with the appโs JavaScript runtime (on the device or simulator).
Metro creates a dependency graph for your entire project. Each module has a unique id. When you make a change in a file, Metro parses the updated file, then rebuilds only the affected modules by walking the dependency graph โ instead of recompiling the whole app. Lastly, metro sends a "update patch" message like below, via Websocket
{
type: "update",
body: {
modules: [
[123, "new version of the module code as a string"]
]
}
}
The app has an embedded HMR client (JavaScript runtime). And it listens the metro bundlers messages via Websocket. When the runtime receives "update patch" message, it injects the new module code to the module.
__r[123] = new Function("module", "exports", "require", "new module code here");
It is that simple, but lets go a little deeper in HMR flow, especially module systems, which makes this possible
React Native uses CommonJs like module system. Every module is wrapped like
__r[123] = function (module, exports, require) {
// your module code here
};
What does CommonJS like means? Well, CommonJS is the older module system. To give a visual representation
// greet.js
function greet(name) {
return `Hello, ${name}`;
}
module.exports = { greet };
// main.js
const { greet } = require("./greet");
console.log(greet("Eren"));
But wait โ donโt we use import and export nowadays?
Yes! We write ESModules (ESM), but Metro transpiles them into CommonJS behind the scenes. Why?
๐ค Why Not Use ESModules Directly?
Why metro does that? It is because dynamic exporting is available CommonJs module system, not ESModules. Dynamic export means the ability to modify what a module exports at runtime. Here is an example.
// dynamic.js
if (Math.random() > 0.5) {
module.exports = { greet: () => console.log("Hello") };
} else {
module.exports = { greet: () => console.log("Hi") };
}
ESModules does not allow that to enforce static structure. That lets the webpack, rollup ,... etc. do Tree Shaking, static analysis, code splitting easily. Besides, it has another system called Live Bindings. (we well not cover any of these).
HMR alone doesnโt know or care about React โ it just swaps modules.
Fast Refresh adds React-aware logic on top using React Refresh.
If the updated module exports a React component, React Refresh compares previous and new versions code. If the component isRefresh Boundary Safe, component wil be re-rendered in place and so state and context are preserved.
Otherwise, it triggers a full reload. Lets talk about what Refresh Boundary Safe is.
How to be Refresh Boundary Safe Component?
React compares the old and new function signatures, i.e., the shape and order of hooks. If the signature is not changed, the component is refresh boundary safe!
I can give you a very basic example, If you add a useEffect before an existing useState, you change the order of hooks โ and React Fast Refresh will trigger a full remount, so state will be lost.
// example-component.jsx
function ExampleComponent() {
const [count, setCount] = useState(0);
return <Text>{count}</Text>;
}
// โ Not Safe (hook order changed)
function App() {
useEffect(() => {}, []);
const [count, setCount] = useState(0); // ๐ฅ state will be lost
return <Text>{count}</Text>;
}
However, if you add the hook after existing ones, the signature is preserved and state remains intact.
// โ
Safe (hook order not changed)
function App() {
const [count, setCount] = useState(0);
useEffect(() => {}, []);
return <Text>{count}</Text>;
}
Fast Refresh is one of the most powerful parts of the React Native developer experience. By building on top of HMR, Metro, and React Refresh, it lets developers iterate on UI and logic with near-instant feedback โ while preserving state.
I hope this breakdown helped you understand whatโs happening behind the scenes every time you hit โSaveโ!
โ Found this helpful? Let me know or share it with a fellow React Native dev ๐