How React Native Manages State and Props Across the Bridge: A Deep Dive into the Architecture
React Native maintains state entirely within the JavaScript runtime while props are diffed and serialized across the bridge via UIManager.updateView, with the reconciler in ReactNativeRenderer-dev.js computing minimal update payloads to optimize performance.
React Native's architecture relies on a bridge to communicate between JavaScript and native platforms. Understanding how state and props traverse this boundary is essential for optimizing performance in the facebook/react-native repository. While state remains a pure JavaScript concern managed by React's Fiber scheduler, props must be serialized and batched across the bridge to update native views.
Where State Lives in React Native
State in React Native is a purely JavaScript-side concept. When you call this.setState or useState dispatchers, updates are queued in React's Fiber scheduler without any immediate interaction with native code.
The state object lives on the Fiber node within the JavaScript runtime. It persists across renders and only influences the native side indirectly when the reconciler determines that new props must be pushed across the bridge. This separation ensures that frequent state updates do not incur serialization overhead unless they result in visible prop changes.
How Props Cross the Bridge
While state remains in JavaScript, props must traverse the bridge to configure native views. The journey from JavaScript state change to native view update follows a precise pipeline implemented in the renderer.
The Render and Reconcile Phase
When state changes trigger a re-render, React's reconciler walks the Fiber tree and computes the difference between previous and new props for each host component (such as <View> or <Text>). This diffing logic lives in diffProperties inside ReactNativeRenderer-dev.js (and its production/Fabric equivalents).
According to the source code at packages/react-native/Libraries/Renderer/implementations/ReactNativeRenderer-dev.js, the diffProperties function (around line 1567) compares the previous and next prop objects to identify which values have actually changed.
Building the Update Payload
The diffProperties function returns an update payload—a flattened object containing only the changed native props such as style, text content, or layout attributes. This minimal payload design reduces the serialization cost and bridge traffic.
The payload structure typically follows a key-value format where indices alternate between property keys and values, optimized for the native side to parse efficiently.
Committing Updates via UIManager
Once the payload is ready, the renderer calls commitUpdate (found around line 2129 in ReactNativeRenderer-dev.js). This function writes the new payload into the internal instanceProps map and then invokes UIManager.updateView (or FabricUIManager.updateView for Fabric).
This represents the first actual JavaScript-to-native crossing in the update cycle. The UIManager.updateView method is defined in packages/react-native/Libraries/ReactNative/UIManager.js and serves as the JavaScript façade that forwards calls to the native implementation.
The Bridge Serialization Process
The actual crossing from JavaScript to native code happens through the BatchedBridge, a thin wrapper around MessageQueue defined in packages/react-native/Libraries/BatchedBridge/BatchedBridge.js and MessageQueue.js.
When UIManager.updateView is called, the request is queued in the BatchedBridge. At the end of the JavaScript execution tick, all queued native calls are serialized into a JSON payload and shipped across the bridge in a single batch. This batching mechanism minimizes the overhead of crossing the JavaScript-native boundary.
On the native side, platform-specific modules receive the batched calls. On Android, UIManagerModule.java (located in packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java) processes updateView calls, while iOS uses RCTUIManager.m (found in ReactCommon/cxx/react/renderer/uimanager/RCTUIManager.mm for modern architectures).
Fabric vs. Paper Architecture
React Native supports two rendering architectures: Paper (the legacy bridge) and Fabric (the new architecture).
The renderer determines which path to use by checking isFabricReactTag. Even-numbered tags belong to Fabric, while odd-numbered tags use Paper. Both paths eventually call a native updateView method, but the underlying native modules differ:
- Paper: Uses the traditional
UIManagermodule with asynchronous batched bridge calls. - Fabric: Uses
FabricUIManagerwith a C++ renderer and JSI (JavaScript Interface) for synchronous communication, reducing latency.
The FabricUIManager implementation is exposed in packages/react-native/Libraries/Renderer/implementations/FabricUIManager.js.
Bypassing the Reconciler with setNativeProps
For performance-critical animations that require bypassing React's diffing overhead, React Native exposes setNativeProps. This method allows direct manipulation of native view properties without triggering a full render pass.
The implementation resides in ReactNativeRenderer-dev.js around line 17340. When called, _proto.setNativeProps builds an update payload and immediately invokes UIManager.updateView, skipping the Fiber reconciler entirely.
Use this sparingly, as it breaks the declarative React model and can cause inconsistencies if the component later re-renders with conflicting props.
Example: Direct Native Updates
import React, {useRef} from 'react';
import {View, Text} from 'react-native';
export default function AnimatedBox() {
const boxRef = useRef(null);
// Bypasses React diff; calls UIManager.updateView directly
const shrink = () => {
boxRef.current?.setNativeProps({
style: {transform: [{scale: 0.5}]}
});
};
return (
<View ref={boxRef} style={{width: 100, height: 100, backgroundColor: 'tomato'}}>
<Text onPress={shrink}>Tap to shrink</Text>
</View>
);
}
Example: State-Driven Updates
import React, {useState} from 'react';
import {View, Text, StyleSheet} from 'react-native';
export default function Counter() {
const [count, setCount] = useState(0);
// Each tap triggers setState → React schedules a render →
// diffProperties produces {text: '1'} → UIManager.updateView is called
return (
<View style={styles.box}>
<Text>{count}</Text>
<Text style={styles.button} onPress={() => setCount(c => c + 1)}>
Increment
</Text>
</View>
);
}
const styles = StyleSheet.create({
box: {padding: 10, backgroundColor: '#eee'},
button: {color: 'blue'},
});
Summary
- State remains entirely within the JavaScript runtime on Fiber nodes and never crosses the bridge directly.
- Props are diffed by
diffPropertiesinReactNativeRenderer-dev.jsto produce minimal update payloads. - The UIManager module (
UIManager.js) serves as the gateway for prop updates crossing into native code. - BatchedBridge queues and serializes calls at the end of each JavaScript tick to minimize bridge overhead.
- Fabric (even tags) and Paper (odd tags) use different native modules but follow the same conceptual flow.
setNativePropsprovides an escape hatch for direct native updates, bypassing React's reconciler entirely.
Frequently Asked Questions
Does React Native sync state to the native side?
No. State is a pure JavaScript concept managed by React's Fiber scheduler within the JS runtime. When state changes trigger a re-render, only the resulting prop differences are serialized across the bridge via UIManager.updateView. The native side never sees the actual state objects, only the prop payloads derived from them.
What is the difference between Paper and Fabric in bridge communication?
Paper (legacy) uses the traditional bridge with asynchronous batched calls through UIManager.js, while Fabric (new architecture) uses FabricUIManager with the C++ renderer and JSI (JavaScript Interface) for synchronous communication. The renderer distinguishes them by tag parity: even-numbered React tags use Fabric, odd tags use Paper.
How does setNativeProps improve performance?
setNativeProps bypasses React's Fiber reconciler and diffProperties entirely. It directly calls UIManager.updateView with the supplied prop changes, eliminating the overhead of render passes and diffing calculations. This is ideal for high-frequency animations but risks prop synchronization issues if React later reconciles conflicting values.
Where is the bridge batching logic implemented?
The batching logic resides in packages/react-native/Libraries/BatchedBridge/BatchedBridge.js and MessageQueue.js. These modules queue native method calls during the JavaScript execution tick and flush them in a single serialized batch at the end of the tick, minimizing the cost of crossing the JavaScript-native boundary.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →