Sooner or later, every serious React Native app hits a wall the JavaScript layer cannot climb on its own: a proprietary SDK a partner handed you as an iOS framework and an Android AAR with no JavaScript wrapper, a hardware capability the community library hasn't caught up to, or a platform API that only exists as a Swift or Kotlin call. React Native's component and API surface is wide, but it was never meant to cover everything a native platform can do — it was meant to make the common 90% fast to build, and to get out of the way for the other 10%. A native module is how you write that other 10% yourself, in the platform's own language, and hand it a JavaScript-shaped door to walk through.
Getting the door wrong is a common way to hurt an app's own performance. A native module is not a free API call — it crosses from the JavaScript thread into native code, and depending on the architecture that crossing either goes through a serializing message bridge or through a direct JSI (JavaScript Interface) binding. Treat that crossing as if it were just another function call, chattering across it dozens of times to fetch small pieces of state, and you can turn a fast native operation into a visibly janky one. Treat it with respect — batch what you send, keep the surface coarse-grained, know which thread your native code actually runs on — and a native module disappears into the app as if it had always been there.
This lesson builds the mental model: how a JavaScript call actually reaches Swift or Kotlin code, what threads are involved, and how to write a first native module on each platform correctly. Lesson 19 goes further and builds a full TurboModule using the New Architecture's codegen pipeline; this lesson is the foundation that makes that pipeline make sense, because you cannot appreciate what codegen and JSI fixed until you have seen the shape of the problem they were fixing.
Analogy🏏Cricket
🏏 Think of it like cricket: When the on-field umpire is unsure about a run-out, the decision does not get made on the spot by shouting across the ground — it goes through a defined channel to the third umpire, who is sitting in a separate room with access to replay feeds the on-field umpire cannot see. The on-field umpire sends a specific, structured request — "check this dismissal" — not a vague description of the whole passage of play, and the third umpire sends back a specific, structured answer — out or not out — not a running commentary. Both umpires are doing real work, but they are working in different environments with different tools, and the review system exists specifically to let a request cross from one environment to the other without either side needing to understand how the other one operates internally. A native module is that same channel between two environments: the JavaScript thread, which knows React state and component trees, and the native platform, which knows Bluetooth stacks, camera hardware, and secure enclaves. Just as the on-field umpire sends a narrow, well-defined request rather than opening a live video feed to the third umpire, a well-designed native module call sends one coarse-grained request rather than chattering back and forth for every small piece of state. Just as the third umpire's room has tools the field never has, native code has platform capabilities JavaScript never has — and the whole point of the bridge is making that crossing structured and cheap rather than ad hoc and expensive. The insight is that a boundary between two different environments needs a deliberate protocol, and how disciplined that protocol is determines whether the review system speeds the game up or turns every close call into a stoppage.
🏏 Showing the Cricket analogy — a Cricket version isn’t available for this concept yet.