byprofile_photo

How React Native Fast Refresh Works

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!

๐Ÿš€ What Is Fast Refresh?

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 Bundler

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.

How It Communicates With Simulator Or Device

Metro also opens a WebSocket connection with the appโ€™s JavaScript runtime (on the device or simulator).

Dependency Graph

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"]
    ]
  }
}

๐ŸŒ HMR And Websocket Communication

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

๐Ÿ“ฆ Module Systems

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).

๐Ÿ” From HMR to Fast Refresh

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>;
}

๐Ÿ Final Thoughts

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 ๐Ÿš€