react-native-nitro-direction
v0.1.3
Published
Flip a React Native app between LTR and RTL at runtime — no restart, no reload, no remount. Native iOS + Android, built on Nitro Modules.
Maintainers
Readme
react-native-nitro-direction
Flip your React Native app between LTR and RTL at runtime — no restart, no bundle reload, no remount. One call re-mirrors the React Native content and generic native view hierarchy in place, keeping navigation state, scroll position, and form state intact.
Built on Nitro Modules. Native owns the
direction, JS reads and sets it. Same pattern as Appearance, Dimensions,
Keyboard. Three exports — setDirection, getDirection, onDirectionChanged.
I18nManager.forceRTL(true) needs an app restart on the New Architecture. This
package triggers React Native's own cold-start re-layout paths on every live
Fabric surface instead.
- Runtime flip — LTR ⇄ RTL without restarting
- JSI-direct — no bridge serialization, native callbacks
- Fabric & bridgeless — iOS + Android, New Architecture only
- Autolinked — zero
MainApplication/ Podfile edits - Three exports —
setDirection,getDirection,onDirectionChanged
Requirements
- React Native New Architecture (Fabric) — default on RN 0.76+ / Expo SDK 52+
react-native-nitro-modulespeer dependency- A bare app (RN CLI) or development build (Expo). Native code does not run in Expo Go
- iOS 13+ / Android API 23+. The package's manifest merges
android:supportsRtl="true"for you
Tested against: React Native 0.86 · react-native-nitro-modules 0.36.x
⚠️ The flip depends on non-contractual React Native internals — see How it works. Re-verify on major RN upgrades.
Installation
bun add react-native-nitro-direction react-native-nitro-modulesRebuild the native app — the hybrid object must compile into the binary:
cd ios && pod install && cd ..
npx react-native run-ios
npx react-native run-androidAutolinks on both platforms. No manual native steps.
Quick start
import {
setDirection,
getDirection,
onDirectionChanged,
} from 'react-native-nitro-direction'
// Flip the entire app
await setDirection('rtl')
await setDirection('ltr')
// Read the current direction
getDirection() // 'ltr' | 'rtl'
// Subscribe to native direction changes
const cleanup = onDirectionChanged((direction) => {
// fires after the visual flip has landed on the main thread
})getDirection() is the authoritative synchronous JS read. Use
onDirectionChanged to keep React state and navigation direction contexts in
sync. React Native's I18nManager.isRTL is only reactive when the host app
includes a compatible React Native event patch.
API
| Export | Signature | Description |
| --- | --- | --- |
| setDirection | (direction: 'ltr' \| 'rtl') => Promise<'ltr' \| 'rtl'> | Flip the app's layout direction at runtime. Resolves once the native re-layout has landed. |
| getDirection | () => 'ltr' \| 'rtl' | Get the current layout direction. |
| onDirectionChanged | (callback: (dir: 'ltr' \| 'rtl') => void) => () => void | Subscribe to direction changes. Returns a cleanup function. |
| useDirection | () => 'ltr' \| 'rtl' | React hook returning the current direction. Shares one module-level store across every caller — no Context provider required. |
Integration guide
Cold start — set direction before the first paint
Do this before rendering your navigator so the stored language is already correct on launch:
// app root, before <NavigationContainer> or the first <Stack>
const direction = getStoredDirection() // your logic — persisted pref, device locale, etc.
await setDirection(direction)Language switch — direction first, then strings
Call setDirection before i18n.changeLanguage(). The tree must be RTL
when the new strings measure; otherwise the text bakes its alignment while the
tree is still LTR.
async function switchLanguage(locale: string) {
const direction = isRTLLanguage(locale) ? 'rtl' : 'ltr' // your logic
await setDirection(direction) // 1. flip direction (native, no restart)
await i18n.changeLanguage(locale) // 2. swap the strings
}Text alignment
React Native only mirrors <Text> / <TextInput> when their style includes
textAlign. Use textAlign: 'left' — it's start-relative, so RN keeps it left
in LTR and flips it to right under RTL. Don't hardcode a mirrored value; the
native flip already does the swap, so double-mirroring cancels out.
const styles = StyleSheet.create({
title: { textAlign: 'left' }, // ✅ flips with direction
body: { /* no textAlign */ }, // ❌ stays left under RTL
})flexDirection: 'row' reverses automatically — no changes needed.
React Navigation / Expo Router
The native-stack push/pop animation and swipe-back gesture read their direction
from LocaleDirContext, which defaults to the cached startup value and never
re-reads at runtime. Keep this context synchronized with the package:
import { getDirection, onDirectionChanged } from 'react-native-nitro-direction'
import { LocaleDirContext } from 'expo-router/react-navigation'
import { useEffect, useState, type ReactNode } from 'react'
function DirectionProvider({ children }: { children: ReactNode }) {
const [direction, setDirection] = useState(getDirection)
useEffect(() => onDirectionChanged(setDirection), [])
return (
<LocaleDirContext.Provider value={direction}>
{children}
</LocaleDirContext.Provider>
)
}Subscribe to direction changes
useEffect(() => {
return onDirectionChanged((direction) => {
// recompute styles, invalidate caches, drive a styling engine plugin
})
}, [])The callback fires after the visual flip has landed on the main/UI thread, so styling engines can safely recompute.
The package does not own navigation controllers, headers, tab bars, or gestures.
Navigation libraries should refresh their own chrome from this callback. The
repository's example2 shows the small version-matched react-native-screens
chevron patch used alongside LocaleDirContext.
How it works
setDirection(direction) sets RN's direction flags synchronously
(RCTI18nUtil on iOS, I18nUtil SharedPreferences on Android) — so
getDirection() is coherent immediately. Then it dispatches the view work to
the main/UI thread in a single pass:
iOS
- Update RN's direction flags, then bust Yoga's size-keyed layout cache — bump each surface's min/max size by 1pt and immediately restore it, forcing a real re-layout in the new direction
- Mirror the generic window/view hierarchy. Navigation libraries retain ownership of their navigation controllers, headers, tab bars, and gestures through their own direction context.
Android
forceLayout()+requestLayout()on everyReactSurfaceView—onMeasure→updateLayoutSpecs→constraintLayout, re-readingisRTL- Set the decor view's
layoutDirectionso inheriting views (toolbars, headers) re-resolve
The key insight: Fabric normally reads the layout direction once at surface
creation. Calling forceRTL updates the flag but doesn't re-mirror anything.
This package triggers RN's own cold-start re-layout path on the live surface,
which is why the flip works without a restart.
Non-contractual internals
The mechanism depends on React Native internals not covered by the public API:
- Yoga keying its per-node layout cache on size + measure mode, not direction — hence the size-nudge (iOS)
ReactSurfaceView.onMeasurereadingI18nUtil.isRTLfresh on every measure pass (Android)
If a future RN version changes these, the live re-layout path may need a
version-specific update. The iOS bridge uses the public surface sizing methods
available on RCTFabricSurface and falls back to a normal layout pass when a
surface is not a Fabric surface.
When upgrading React Native: run the example app and flip every screen.
Troubleshooting
| Symptom | Likely cause |
| --- | --- |
| Nothing happens | Native module not in the binary — rebuild (pod install + run). Confirm react-native-nitro-modules is installed. |
| Only flips after restart | You're on New Architecture? Confirm newArchEnabled. Are you await-ing setDirection()? |
| Text doesn't right-align | Add textAlign: 'left' to that style. Unset alignment never flips. |
| Text alignment is stale after language switch | Call setDirection before i18n.changeLanguage. |
| Push animation / swipe-back stuck in old direction | Override LocaleDirContext (see React Navigation section). |
License
MIT
