# Codegen

> Use when implementing native modules (TurboModules) or native views (Fabric) for a React Native library — Codegen TypeScript specs, threading model, iOS/Android registration, view flattening, New Architecture only.

- Skill: `shankulkarni/codegen` (Agent Skill)
- Install (CLI): `npx skillmds@latest add shankulkarni/codegen`
- Raw SKILL.md: https://api.skillmd.com/api/skills/shankulkarni/codegen/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: Shankulkarni (https://skillmd.com/u/shankulkarni)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/shankulkarni/codegen

---


# Codegen — TurboModules + Fabric

New Architecture only. Codegen generates native interfaces from TypeScript specs.

---

## Threading Model — Know This Before Writing Native Code

### Sync vs Async Methods

```
Sync method  → blocks JS thread for its entire duration
               MUST complete in <16ms (one frame budget)
               Use only for simple reads/computations

Async method → runs on native module background thread
               Promise-based; safe for any duration
               Use for I/O, heavy computation, network
```

```tsx
// TypeScript spec
export interface Spec extends TurboModule {
  // Sync — fast only, <16ms
  getDeviceId(): string
  multiply(a: number, b: number): number

  // Async — safe for heavy work
  processImage(uri: string): Promise<{ uri: string; size: number }>
  readFile(path: string): Promise<string>
}
```

### iOS Threading

```objc
// Sync — runs on JS thread, keep fast
- (double)multiply:(double)a b:(double)b {
  return a * b; // pure computation, instant
}

// Async — dispatch to background
- (void)processImage:(NSString *)uri resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // heavy work here
    NSString *result = [self doHeavyWork:uri];
    resolve(result);
  });
}

// UI updates — must dispatch to main thread
- (void)updateUI:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
  dispatch_async(dispatch_get_main_queue(), ^{
    // UIKit operations here
    resolve(nil);
  });
}
```

### Android Threading

```kotlin
// Sync — JS thread, keep fast
override fun multiply(a: Double, b: Double): Double = a * b

// Async — use coroutines on background dispatcher
override fun processImage(uri: String, promise: Promise) {
  CoroutineScope(Dispatchers.Default + Job()).launch {
    try {
      val result = doHeavyWork(uri)
      promise.resolve(result)
    } catch (e: Exception) {
      promise.reject("ERR_PROCESSING", e.message, e)
    }
  }
}

// Always clean up coroutines on module invalidation
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())

override fun invalidate() {
  super.invalidate()
  scope.cancel() // ← mandatory, prevents leaks
}
```

---

## TurboModule — Full Setup

### 1. TypeScript Spec (`src/NativeMyLib.ts`)

```ts
import type { TurboModule } from 'react-native'
import { TurboModuleRegistry } from 'react-native'
import type { Double, Int32 } from 'react-native/Libraries/Types/CodegenTypes'

export interface Spec extends TurboModule {
  // Sync — primitives only, fast ops
  multiply(a: Double, b: Double): Double
  getVersion(): string

  // Async — I/O, heavy computation
  processImage(uri: string, options: {
    width: Int32
    height: Int32
    quality?: Double
  }): Promise<{ uri: string; size: Int32 }>

  // Events (if module emits)
  addListener(eventName: string): void
  removeListeners(count: Int32): void
}

export default TurboModuleRegistry.getEnforcing<Spec>('RNMyLib')
```

### 2. `package.json` Codegen Config

```json
"codegenConfig": {
  "name": "RNMyLibSpec",
  "type": "modules",
  "jsSrcsDir": "src",
  "android": { "javaPackageName": "com.mylib" }
}
```

### 3. iOS (`ios/RNMyLib.mm`)

```objc
#import "RNMyLib.h"

@implementation RNMyLib

RCT_EXPORT_MODULE()

- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
  return std::make_shared<facebook::react::NativeRNMyLibSpecJSI>(params);
}

- (double)multiply:(double)a b:(double)b { return a * b; }

- (void)processImage:(NSString *)uri options:(JS::NativeRNMyLib::SpecProcessImageOptions &)options
             resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // heavy work
    resolve(@{ @"uri": uri, @"size": @(1024) });
  });
}

@end
```

### 4. Android (`android/src/main/java/com/mylib/MyLibModule.kt`)

```kotlin
package com.mylib

import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReadableMap
import com.mylib.NativeRNMyLibSpec
import kotlinx.coroutines.*

class MyLibModule(reactContext: ReactApplicationContext) : NativeRNMyLibSpec(reactContext) {
  private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())

  override fun getName() = NAME

  override fun multiply(a: Double, b: Double): Double = a * b

  override fun processImage(uri: String, options: ReadableMap, promise: Promise) {
    scope.launch {
      try {
        promise.resolve(uri)
      } catch (e: Exception) {
        promise.reject("ERR_PROCESS", e.message, e)
      }
    }
  }

  override fun invalidate() {
    super.invalidate()
    scope.cancel()
  }

  companion object { const val NAME = "RNMyLib" }
}
```

---

## Fabric Component — Full Setup

### 1. TypeScript Spec (`src/MyLibNativeComponent.ts`)

```ts
import type { HostComponent, ViewProps } from 'react-native'
import type { DirectEventHandler, Double, Int32 } from 'react-native/Libraries/Types/CodegenTypes'
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'

type OnLoadEvent = { width: Double; height: Double; duration: Double }

type NativeProps = ViewProps & {
  uri: string
  resizeMode?: 'cover' | 'contain' | 'stretch'
  autoPlay?: boolean
  muted?: boolean
  onLoad?: DirectEventHandler<OnLoadEvent>
  onError?: DirectEventHandler<{ message: string; code: Int32 }>
  onProgress?: DirectEventHandler<{ position: Double; duration: Double }>
}

export default codegenNativeComponent<NativeProps>('MyLibView') as HostComponent<NativeProps>
```

### 2. `package.json` Codegen Config for Views

```json
"codegenConfig": {
  "name": "RNMyLibSpec",
  "type": "components",
  "jsSrcsDir": "src"
}
```

---

## View Flattening — `collapsable={false}`

Fabric aggressively flattens layout-only views. If a native component expects a specific child at a specific index, the child may be removed silently.

```tsx
// ❌ Fabric may remove this wrapper — native code breaks
<View style={styles.wrapper}>
  <NativeVideoView />
</View>

// ✅ Prevent flattening
<View style={styles.wrapper} collapsable={false}>
  <NativeVideoView />
</View>
```

Detect flattening issues with:
- **iOS**: Xcode → Debug View Hierarchy (look for missing layers)
- **Android**: Android Studio → Layout Inspector

Properties that prevent flattening (no need for `collapsable`): `backgroundColor`, `borderWidth`, `shadowColor`, event handlers, `opacity < 1`, `overflow: 'hidden'`.

---

## Codegen Type Reference

| TypeScript | Import | Notes |
|---|---|---|
| `Double` | `CodegenTypes` | 64-bit float |
| `Float` | `CodegenTypes` | 32-bit float |
| `Int32` | `CodegenTypes` | 32-bit integer |
| `string` | — | native string |
| `boolean` | — | native bool |
| `ReadonlyArray<T>` | — | typed array |
| `DirectEventHandler<T>` | `CodegenTypes` | fires on target only |
| `BubblingEventHandler<T>` | `CodegenTypes` | bubbles up tree |

---

---

## Expo Support for Native Libraries

Two separate concerns. Do not confuse them.

### 1. `expo-module.config.json` — Auto-linking

Tells Expo's auto-linker which native classes to register. Without this file, the library won't link in Expo managed or bare workflow.

The class names must match what you named your native classes. Use the actual class names from your iOS and Android implementation.

**TurboModule** — the iOS class is `RNMyLib` (from `RCT_EXPORT_MODULE()`), Android class is `MyLibModule` (from `getName()`):
```json
{
  "platforms": ["apple", "android"],
  "ios": {
    "modules": ["RNMyLib"]
  },
  "android": {
    "modules": ["com.mylib.MyLibModule"]
  }
}
```

**Fabric view** — the iOS view component is `RNMyLibView`, Android is `MyLibViewManager`:
```json
{
  "platforms": ["apple", "android"],
  "ios": {
    "components": ["RNMyLibView"]
  },
  "android": {
    "components": ["com.mylib.MyLibViewManager"]
  }
}
```

How to find the right class names:
- iOS: look for `RCT_EXPORT_MODULE()` or `RCT_EXPORT_VIEW_COMPONENT()` in your `.mm` file — the argument (or class name if no argument) is what goes here
- Android: look for the class extending `NativeXXXSpec` or `SimpleViewManager` — use the fully qualified class name

Add `"expo-module.config.json"` to `files` in `package.json` so it ships with the package.

### 2. `app.plugin.js` — Config Plugin (only when needed)

A config plugin modifies the consumer's native project at build time — adding permissions to `AndroidManifest.xml`, entries to `Info.plist`, Gradle dependencies, etc.

You only need this if your library requires native project configuration beyond just linking. Examples:
- Library needs camera permission → add `NSCameraUsageDescription` to Info.plist
- Library needs a background mode → add to `UIBackgroundModes`
- Library needs a custom Gradle dependency not auto-linked

Most simple TurboModules and Fabric views do NOT need a config plugin — auto-linking is sufficient.

If you do need one:
```js
// app.plugin.js
const { withInfoPlist } = require('@expo/config-plugins')

module.exports = function withMyLib(config) {
  return withInfoPlist(config, config => {
    config.modResults.NSCameraUsageDescription = 'Required for camera preview'
    return config
  })
}
```

Then add to `package.json`:
```json
{
  "main": "app.plugin.js"
}
```

The directory entry field `configPlugin: true` refers to this `app.plugin.js` — not `expo-module.config.json`. Every native library should have `expo-module.config.json`. Only libraries that modify native project config need `app.plugin.js`.

---

## Rules

- `TurboModuleRegistry.getEnforcing` — never `NativeModules`
- `codegenNativeComponent` — never `requireNativeComponent`
- Sync methods must complete in **<16ms** — anything longer goes async
- Android: always cancel coroutine scope in `invalidate()`
- iOS: always `dispatch_async` for work >1ms
- Never edit auto-generated files in `build/` or `android/build/`
- Module name in spec must exactly match `getName()` on Android, `RCT_EXPORT_MODULE()` on iOS
- All methods declared in spec must be implemented — Codegen enforces at build time
- Codegen specs in TypeScript — never Flow

