Rendering custom native views synchronously as custom components
Fabric replaces the asynchronous bridge with a shared C++ shadow tree that JavaScript and native code access directly through JSI. Building a custom native view as a custom component means understanding how ComponentDescriptor, ShadowNode and generated codegen code work together, from the first spec file to the finished component view on iOS and Android.
Table of Contents
- 1. From the asynchronous bridge to the synchronous Fabric pipeline
- 2. ComponentDescriptor and ShadowNode: the building blocks of a Fabric component
- 3. From TypeScript spec to generated Fabric interface
- 4. Building a custom Fabric view on iOS: from ComponentDescriptor to ComponentView
- 5. Building a custom Fabric view on Android: ViewManager meets ComponentDescriptor
- 6. Synchronous state management without a round trip through the bridge
- 7. The event pipeline: how interactions travel synchronously through Fabric
- 8. Migrating legacy view manager code: the interop layer as a bridge
- 9. Performance characteristics and debugging Fabric components
- 10. Summary
- 11. FAQ
1. From the asynchronous bridge to the synchronous Fabric pipeline
In the original React Native architecture, every piece of communication between JavaScript and native code ran through the bridge: an asynchronous channel that serialized messages as JSON, batched them and sent them in both directions through a message queue. For rendering, that meant JavaScript described a shadow tree, which was sent across the bridge to the native side, had its layout computed with Yoga, and only then produced or updated the actual native views. Every step in that chain cost time for serialization and thread switching, which became noticeable with complex lists or frequent updates.
Fabric replaces that chain with a shared C++ core layer that JavaScript and native code both address directly through JSI. The shadow tree exists only once, in C++, and both the JavaScript runtime and iOS and Android access it by reference instead of shipping copies across a bridge. That makes layout computation and the commit phase synchronous, which matters especially for custom components that need to know precisely when a new version of their shadow node has actually landed on screen.
2. ComponentDescriptor and ShadowNode: the building blocks of a Fabric component
Every Fabric component consists of at least three C++ classes: a ShadowNode holding the immutable state of a component instance in the shadow tree, a ComponentDescriptor telling Fabric how to create, clone and supply props to a ShadowNode, and a props class mapping the typed properties coming from JavaScript. These three building blocks are deliberately separated from the actual platform view: the shadow tree only knows about layout and props, not UIKit or the Android view system.
Only the ComponentView on iOS, or the equivalent Fabric view adapter on Android, translates a ShadowNode into a visible native view. This separation makes it theoretically possible to reuse the same ShadowNode for different rendering targets, for example off-screen rendering or tests without a real UI, something that was practically impossible with the old view manager model.
3. From TypeScript spec to generated Fabric interface
A custom Fabric component does not start with C++, but with a TypeScript spec file that codegen reads as the contract between JavaScript and the native side. It declares props, events and the component type, usually through codegenNativeComponent, and codegen generates the matching C++ headers for the props classes, Objective-C interfaces for iOS and Java interfaces for Android at build time.
The big advantage over the old view manager API is that type mismatches between JavaScript and native side surface at build time, not as a silent crash or misinterpreted prop at runtime. Adding a new property only requires changing the spec file, running codegen, and immediately seeing which native files need updating, because the generated code changes accordingly.
4. Building a custom Fabric view on iOS: from ComponentDescriptor to ComponentView
On iOS a Fabric component subclasses RCTViewComponentView, which receives the typed props handed down from the shadow tree in updateProps and translates them into native UIKit properties. Unlike the old view manager approach, where properties were set individually through key value pairs, a complete, immutable props object arrives here that can be compared efficiently against the previous state to detect actual changes.
The component is registered through a static supportedViewConfigCls method and a Fabric component descriptor provider registration entered into the global component registry at app startup. Missing this registration makes Fabric silently fall back to an empty view for the affected component, a debugging pitfall that regularly causes confusion in practice.
5. Building a custom Fabric view on Android: ViewManager meets ComponentDescriptor
On Android the classic ViewManager remains the connecting piece but gains a Fabric-specific layer: the generated ViewManagerDelegate maps props from the generated Java interface onto concrete setter methods, so the actual ViewManager class only needs to implement the setters without handling reflection or manual prop parsing.
Every Fabric-capable Android component also needs an entry in the ReactPackage that supplies both the view manager and the matching component descriptor name, so the native runtime knows during shadow tree construction which C++ component descriptor implementation is responsible for a given component name. A misspelled name here also leads to a silently empty view instead of a clear error message.
6. Synchronous state management without a round trip through the bridge
A central feature of Fabric is the ability to hold native state directly inside the shadow tree instead of sending every state change back to JavaScript through a full React re-render cycle. Through the generated State class, a native component, say a scroll offset or a text measurement, can commit its state directly, and Fabric ensures that state is consistently reconciled with the next layout pass.
This matters especially for components that change state frequently and at high rates, for example a native text field with cursor position or a scroll view with momentum scrolling. Without this mechanism, every position change would first have to pass through the JavaScript layer before reaching the shadow tree, which would cause noticeable delays at input rates of 60 or 120 hertz.
7. The event pipeline: how interactions travel synchronously through Fabric
Events like touches or layout changes are dispatched in Fabric through a dedicated EventEmitter instance per shadow node, prioritized and batched on the C++ side before being handed to the JavaScript runtime. What matters for custom components is that events marked as direct go straight to the registered JavaScript handler without the detour through the classic bubbling logic of the old system.
The spec file determines through the event type whether an event is prioritized synchronously, for example for continuous feedback like drag gestures, or treated as a lower priority, discrete event. Choosing this priority incorrectly risks either unnecessary rendering pressure for trivial events or noticeable lag on time-critical interactions like a slider.
8. Migrating legacy view manager code: the interop layer as a bridge
For libraries that do not yet have a native Fabric component, the Fabric interop layer automatically wraps old view manager based components in a compatible Fabric shell. That allows a gradual transition where not every third-party component needs to be rewritten immediately, but it costs part of the performance benefit because the old prop diffing mechanism is still used internally.
For a real migration it pays off to first write the props and events spec, then implement the generated interfaces, and only remove the old view manager code at the end instead of maintaining both in parallel. In practice, the bigger effort usually is not the C++ part but cleanly migrating imperative commands that used to run through UIManager.dispatchViewManagerCommand and now need their own typed commands in the spec.
9. Performance characteristics and debugging Fabric components
The most noticeable performance effect of Fabric shows up in lists with many simultaneously visible custom components, because layout computation and commit run synchronously without bridge serialization, noticeably reducing jank during fast scrolling. For custom components it is worth watching the number of re-renders in the shadow tree, which can be observed through Flipper or the built-in Fabric logging.
Classic sources of bugs are forgotten component descriptor registrations, which lead to empty views without an error message, and crashes caused by thread-unsafe access to UIKit or Android view objects from a background thread, since Fabric deliberately performs layout computation off the main thread. Anyone debugging a custom Fabric component should therefore first check whether UI mutations actually land on the main thread before diving deeper into the C++ layer.
| Aspect | Old bridge architecture | Fabric | Impact on custom components |
|---|---|---|---|
| Data exchange | Asynchronous JSON serialization over the bridge | Direct C++ access through JSI, no serialization | Less latency for props and state updates |
| Shadow tree | Separate copies in JS and native | One shared C++ instance | Consistent state without sync bugs |
| Layout commit | Asynchronous, often visibly delayed | Synchronous within the same render cycle | Less layout flicker for dynamic views |
| Prop typing | Manual, runtime errors on typos | Codegen-generated, checked at build time | Errors caught earlier when adding props |
| Imperative commands | dispatchViewManagerCommand with string names | Typed commands from the spec | Fewer runtime crashes from wrong command names |
| Registration | View manager class with manual setup | ComponentDescriptor provider in a global registry | Missing registration surfaces early as an empty view |
| Debugging tool | Chrome DevTools without native insight | Flipper with Fabric-specific logging | Re-renders and commits in the shadow tree directly visible |
Mironsoft
React Native app development and Magento integration
A mobile app for the Magento shop that actually runs smoothly?
We build React Native apps cleanly connected to the Magento REST or GraphQL API, from the first line of code to publishing on the App Store and Google Play.
App Concept
Plan the architecture and feature scope of a Magento-connected app together.
Magento API Integration
Cleanly connect product catalog, cart, and checkout to the shop API.
Store Publishing
Guide the App Store and Google Play release process without pitfalls.
10. Summary
Fabric Custom Components at a Glance
ShadowNode & ComponentDescriptor
Form the C++ foundation of every Fabric component, fully separated from the visible platform view.
Codegen
Generates typed interfaces for iOS and Android from a TypeScript spec, catching errors at build time instead of runtime.
Synchronous state
Lets native components commit frequent state directly in the shadow tree without a round trip through JavaScript.
Interop layer
Eases a gradual migration of old view manager components, at the cost of part of the performance benefit.