All skills
expo avatar

/expo-module

@5c8f62e official
by expoexpo/skills2.6k stars
156

Framework (OSS). Guide for creating and writing Expo native modules and views using the Expo Modules API (Swift, Kotlin, TypeScript). Covers module definition DSL, native views, shared objects, config plugins, lifecycle hooks, autolinking, and type system. Use when building or modifying native modules for Expo. Not for migrating an existing Swift module from the definition DSL to the Expo Modules API 2.0 macros; use expo-migrate-module (from the expo-experiments plugin) for that.

Use this Skill: https://skilld.dev/gh/expo/skills/expo-module

This session only. Nothing lands on disk.

referencesnative-module.md

≈1.5k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Native Module DSL Reference

Swift is shown as the primary language. Kotlin follows the same DSL structure (see SKILL.md for both). Kotlin-specific syntax is noted where it meaningfully differs.

Name

Sets the module identifier used in JavaScript.

Name("MyModule")

Constant

Computed once on first access, then cached.

Constant("PI") { 3.14159 }

Function (Synchronous)

Blocks the JS thread until completion. Supports up to 8 arguments.

Function("add") { (a: Int, b: Int) -> Int in
  return a + b
}

AsyncFunction

Returns a Promise. Runs on a background thread by default.

AsyncFunction("fetchData") { (url: URL) -> String in
  let data = try Data(contentsOf: url)
  return String(data: data, encoding: .utf8) ?? ""
}

// Force main queue execution
AsyncFunction("updateUI") { () -> Void in
  // UI work
}.runOnQueue(.main)

Kotlin differences:

// Supports Kotlin coroutines
AsyncFunction("fetchData") Coroutine { url: java.net.URL ->
  withContext(Dispatchers.IO) {
    url.readText()
  }
}

Property

Getter/setter for JS object properties.

// Read-only
Property("version") { "1.0.0" }

// Read-write
Property("volume")
  .get { () -> Float in self.volume }
  .set { (newValue: Float) in self.volume = newValue }

Events

Declares events the module can send to JS. Must be declared before using sendEvent.

// Declaration
Events("onChange", "onError")

// Sending from native (Swift)
sendEvent("onChange", ["value": newValue])

Kotlin difference — uses bundleOf:

sendEvent("onChange", bundleOf("value" to newValue))

JS subscription:

import { useEvent } from "expo";
import MyModule from "./MyModule";

// Hook-based (recommended)
const event = useEvent(MyModule, "onChange");

// Manual subscription
const subscription = MyModule.addListener("onChange", (event) => {
  console.log(event.value);
});
// Clean up: subscription.remove()

OnStartObserving / OnStopObserving

Called when the first listener attaches / last listener detaches. Can be scoped to specific events.

OnStartObserving("onChange") {
  // Start producing events
}

OnStopObserving("onChange") {
  // Stop producing events
}

Type System

Primitives

Swift Kotlin JS
Bool Boolean boolean
Int, Int32 Int number
Int64 Long number
Float, Float32 Float number
Double Double number
String String string
URL java.net.URL / android.net.Uri string
CGPoint - { x, y }
CGSize - { width, height }
CGRect - { x, y, width, height }
UIColor / CGColor android.graphics.Color string (ProcessedColorValue)
Data kotlin.ByteArray Uint8Array

Records (Struct-like types)

struct UserRecord: Record {
  @Field var name: String = ""
  @Field var age: Int = 0
  @Field var email: String?
}

Function("createUser") { (user: UserRecord) -> Bool in
  return true
}

Kotlin difference — uses class instead of struct, optional fields need explicit = null:

class UserRecord : Record {
  @Field var name: String = ""
  @Field var age: Int = 0
  @Field var email: String? = null
}

Enums (Enumerable)

enum Theme: String, Enumerable {
  case light
  case dark
  case system
}

Function("setTheme") { (theme: Theme) in
  // type-safe enum value
}

Kotlin difference — uses enum class with explicit value property:

enum class Theme(val value: String) : Enumerable {
  LIGHT("light"),
  DARK("dark"),
  SYSTEM("system")
}

Either Types (Union types)

Function("process") { (input: Either<String, Int>) in
  if let str = input.get(String.self) {
    // handle string
  } else if let num = input.get(Int.self) {
    // handle number
  }
}

Also available: EitherOfThree<A, B, C>, EitherOfFour<A, B, C, D>.

JavaScript Values (Direct JS manipulation)

For advanced use in synchronous functions running on JS thread:

Function("callback") { (fn: JavaScriptFunction<String>) in
  let result = fn("arg1", "arg2")
}

Shared Objects

Bridge native class instances to JS with automatic lifecycle management. Instances are deallocated when neither JS nor native code holds a reference.

Defining a Shared Object

class ImageContext: SharedObject {
  private var image: UIImage

  init(image: UIImage) {
    self.image = image
    super.init()
  }

  func rotate(degrees: Double) {
    image = image.rotated(degrees: degrees)
  }
}

Kotlin difference — takes RuntimeContext in constructor, override sharedObjectDidRelease() for cleanup:

class ImageContext(
  runtimeContext: RuntimeContext,
  private var bitmap: Bitmap
) : SharedObject(runtimeContext) {

  fun rotate(degrees: Float) { /* ... */ }

  override fun sharedObjectDidRelease() {
    if (!bitmap.isRecycled) bitmap.recycle()
  }
}

Exposing via Class DSL

Class("Context", ImageContext.self) {
  Constructor { (path: String) -> ImageContext in
    return ImageContext(image: UIImage(contentsOfFile: path)!)
  }

  Function("rotate") { (ctx: ImageContext, degrees: Double) -> ImageContext in
    ctx.rotate(degrees: degrees)
    return ctx
  }

  Property("width")
    .get { (ctx: ImageContext) -> Int in ctx.width }
}

Other Class DSL components: StaticFunction, StaticAsyncFunction, AsyncFunction.

SharedRef

Specialized shared reference for passing typed objects between modules:

final class ImageRef: SharedRef<UIImage> {}

JS Usage

const ctx = await ImageModule.create("/path/to/image.png");
ctx.rotate(90);
console.log(ctx.width);

Source: SKILL.md on GitHub

No alerts17d4 checks · Risk SAFE
  • Gen Agent Trust Hub17d

    The skill provides developer documentation and reference guides for creating and writing Expo native modules and views. All code examples and scripts align with legitimate development practices, and no security risks were identified.

  • Socket17d

    No alerts

  • Snyk17d

    Risk: LOW · No issues

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 5c8f62e. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 3 days ago.

Activeupdated 2 months ago
version
1.0.0
  • TypeScript
  • expo
  • native-modules
  • swift
  • kotlin
  • react-native
  • ios
  • android
  • config-plugins

README badge

README badge for expo/skills/expo-module

Guides you through creating and modifying Expo native modules in Swift (iOS) and Kotlin (Android), with TypeScript bindings. Use this skill when building native modules, native views, config plugins, or wrapping platform SDKs for React Native consumption.

Generated from the current SKILL.md.

What is the difference between a local module and a standalone module?
Local modules are for a single app and live in expo.autolinking.nativeModulesDir or modules/ folder, using the host app's tooling. Standalone modules have their own package metadata, scripts, and example app, and are intended for reuse, monorepos, or publishing.
Should I manually create native files or use create-expo-module?
Use create-expo-module to scaffold. It sets up the expected layout, expo-module.config.json, podspec/Gradle files, TypeScript bindings, and the example app flow. Manually creating files is error-prone and not recommended.
How do I add a new platform to an existing Expo module?
Use create-expo-module add-platform-support instead of manually copying native directories. This ensures correct file placement and configuration.
What platforms does this skill cover?
Swift (iOS), Kotlin (Android), and TypeScript. The skill provides DSL patterns for all three to create modules, native views, and shared objects.
Do I need to manually edit expo-module.config.json?
The scaffold generates it automatically, but you may need to edit it to register native modules and configure platform-specific settings like fully-qualified class names for Android.

Generated from the current SKILL.md. These answers refresh after source changes.