Smithery Logo
MCPsSkillsDocsPricing
Login
Smithery Logo

Accelerating the Agent Economy

Resources

DocumentationPrivacy PolicySystem Status

Company

PricingAboutBlog

Connect

© 2026 Smithery. All rights reserved.

    evanbacon

    expo-modules

    evanbacon/expo-modules
    Coding
    25

    About

    SKILL.md

    Install

    Install via Skills CLI

    or add to your agent
    • Claude Code
      Claude Code
    • Codex
      Codex
    • OpenClaw
      OpenClaw
    • Cursor
      Cursor
    • Amp
      Amp
    • GitHub Copilot
      GitHub Copilot
    • Gemini CLI
      Gemini CLI
    • Kilo Code
      Kilo Code
    • Junie
      Junie
    • Replit
      Replit
    • Windsurf
      Windsurf
    • Cline
      Cline
    • Continue
      Continue
    • OpenCode
      OpenCode
    • OpenHands
      OpenHands
    • Roo Code
      Roo Code
    • Augment
      Augment
    • Goose
      Goose
    • Trae
      Trae
    • Zencoder
      Zencoder
    • Antigravity
      Antigravity
    ├─
    ├─
    └─

    About

    Building great Expo native modules for iOS and Android. Views, APIs, Marshalling, Shared Objects, Expo Documentation, Verifying Expo modules.

    SKILL.md

    Great module standards

    • Design native APIs as if you contributing W3C specs for the browser, take inspiration from modern web modules. eg std:kv-storage, clipboard.
    • Aim for 100% backwards compatibility like the web.
    • Create escape hatches for single-platform functionality.
    • Avoid extraneous abstractions. Directly expose native functionality.
    • Avoid unnecessary async methods. Use sync methods when possible.
    • Prefer string union types for API options instead of boolean flags, enums, or multiple parameters. eg instead of capture(options: { isHighQuality: boolean }), use capture(options: { quality: 'high' | 'medium' | 'low' }).
    • Marshalling is awesome for platform-specific APIs.
    • New Architecture only. NEVER support legacy React Native architecture.
    • ALWAYS use only Expo modules API.
    • Prefer Swift and Kotlin.
    • Use optionality for availability checks as opposed to extraneous isAvailable functions or constants. eg snapshot.capture?.() instead of snapshot.isAvailable && snapshot.capture().
    • ALWAYS support the latest and greatest API features.

    Example of a GREAT Expo module:

    import { NativeModule } from "expo";
    
    declare class AppClipModule extends NativeModule<{}> {
      prompt(): void;
      isAppClip?: boolean;
    }
    
    // This call loads the native module object from the JSI.
    const AppClipNative =
      typeof expo !== "undefined"
        ? (expo.modules.AppClip as AppClipModule) ?? {}
        : {};
    
    if (AppClipNative?.isAppClip) {
      navigator.appClip = {
        prompt: AppClipNative.prompt,
      };
    }
    
    // Add types for the global `navigator.appClip` object.
    declare global {
      interface Navigator {
        /**
         * Only available in an App Clip context.
         * @expo
         */
        appClip?: {
          /** Open the SKOverlay */
          prompt: () => void;
        };
      }
    }
    
    export {};
    
    • Simple web-style interface.
    • Global type augmentation for easy access.
    • Docs in the type definitions.
    • Optional availability checks instead of extraneous isAvailable methods.

    Example of a POOR Expo module:

    import { NativeModulesProxy } from "expo-modules-core";
    const { ExpoAppClip } = NativeModulesProxy;
    export default {
      promptAppClip() {
        return ExpoAppClip.promptAppClip();
      },
      isAppClipAvailable() {
        return ExpoAppClip.isAppClipAvailable();
      },
    };
    

    Great documentation

    • If you have a function like isAvailable(), explain why it exists in the docs. Research cases where it may return false such as in a simulator or particular OS version.
    • Document OS version availability for functions and constants in the type definitions.

    BAD module standards

    • APIs that are hard to import, e.g. import * as MediaLibrary from 'expo-media-library'; instead of import { MediaLibrary } from 'expo/media';
    • Extraneous abstractions over native functionality. The native module is installing on the global, do not wrap it in another layer for no reason.
    • Extraneous async methods when sync methods are possible.
    • Boolean flags instead of string union types for options.
    • Supporting legacy React Native architecture.

    Views

    Take API inspiration from great web component libraries like BaseUI and Radix.

    • https://base-ui.com/react/components/progress
    • https://www.radix-ui.com/primitives/docs/components/progress

    Consider if you're building a control or a display component. Controls should have more interactive APIs, while display components should be more declarative.

    Prefer functions on views instead of useImperativeHandle + findNodeHandle.

    AsyncFunction("capture") { (view, options: Options) -> Ref in
      return try capture(self.appContext, view)
    }
    

    Remember to export views in the module:

    import ExpoModulesCore
    
    public class ExpoWebViewModule: Module {
      public func definition() -> ModuleDefinition {
        Name("ExpoWebView")
    
        View(ExpoWebView.self) {}
      }
    }
    

    Marshalling-style API

    Consider this example https://github.com/EvanBacon/expo-shared-objects-haptics-example/blob/be90e92f8dba9b0807009502ab25c423c57e640d/modules/my-module/ios/MyModule.swift#L1C1-L178C2

    Using @retroactive Convertible and AnyArgument to convert between Swift types and dictionaries enables passing complex data structures across the boundary without writing custom serialization code for each type.

    extension CHHapticEventParameter: @retroactive Convertible, AnyArgument {
        public static func convert(from value: Any?, appContext: AppContext) throws -> Self {
            guard let dict = value as? [String: Any],
                  let parameterIDRaw = dict["parameterID"] as? String,
                  let value = dict["value"] as? Double else {
                throw NotADictionaryException()
            }
            return Self(parameterID: CHHapticEvent.ParameterID(rawValue: parameterIDRaw), value: Float(value))
        }
    }
    
    extension CHHapticEvent: @retroactive Convertible, AnyArgument {
        public static func convert(from value: Any?, appContext: AppContext) throws -> Self {
            guard let dict = value as? [String: Any],
                  let eventTypeRaw = dict["eventType"] as? String,
                  let relativeTime = dict["relativeTime"] as? Double else {
                throw NotADictionaryException()
            }
            let eventType = CHHapticEvent.EventType(rawValue: eventTypeRaw)
            let parameters = (dict["parameters"] as? [[String: Any]])?.compactMap { paramDict -> CHHapticEventParameter? in
                try? CHHapticEventParameter.convert(from: paramDict, appContext: appContext)
            } ?? []
            return Self(eventType: eventType, parameters: parameters, relativeTime: relativeTime)
        }
    }
    
    extension CHHapticDynamicParameter: @retroactive Convertible, AnyArgument {
        public static func convert(from value: Any?, appContext: AppContext) throws -> Self {
            guard let dict = value as? [String: Any],
                  let parameterIDRaw = dict["parameterID"] as? String,
                  let value = dict["value"] as? Double,
                  let relativeTime = dict["relativeTime"] as? Double else {
                throw NotADictionaryException()
            }
    
            return Self(parameterID: CHHapticDynamicParameter.ID(rawValue: parameterIDRaw), value: Float(value), relativeTime: relativeTime)
        }
    }
    
    extension CHHapticPattern: @retroactive Convertible, AnyArgument {
        public static func convert(from value: Any?, appContext: AppContext) throws -> Self {
            guard let dict = value as? [String: Any],
                  let eventsArray = dict["events"] as? [[String: Any]] else {
                throw NotADictionaryException()
            }
            let events = try eventsArray.map { eventDict -> CHHapticEvent in
                try CHHapticEvent.convert(from: eventDict, appContext: appContext)
            }
            let parameters = (dict["parameters"] as? [[String: Any]])?.compactMap { paramDict -> CHHapticDynamicParameter? in
                return try? CHHapticDynamicParameter.convert(from: paramDict, appContext: appContext)
            } ?? []
            return try Self(events: events, parameters: parameters)
        }
    }
    
    internal final class NotAnArrayException: Exception {
        override var reason: String {
            "Given value is not an array"
        }
    }
    
    internal final class IncorrectArraySizeException: GenericException<(expected: Int, actual: Int)> {
        override var reason: String {
            "Given array has unexpected number of elements: \(param.actual), expected: \(param.expected)"
        }
    }
    
    internal final class NotADictionaryException: Exception {
        override var reason: String {
            "Given value is not a dictionary"
        }
    }
    

    Later this can be used to implement methods that accept complex data structures as arguments.

    Function("playPattern") { (pattern: CHHapticPattern) in
        let player = try hapticEngine.makePlayer(with: pattern)
        try player.start(atTime: 0)
    }
    

    Use shorthand where possible, especially when the JS value matches the Swift value:

    Property("__typename") { $0.__typename }
    

    Shared objects

    Shared objects are long-lived native instances that are shared to JS. They can be used to keep heavy state objects, such as a decoded bitmap, alive across React components, rather than spinning up a new native instance every time a component mounts.

    • https://docs.expo.dev/modules/shared-objects/
    • https://expo.dev/blog/the-real-world-impact-of-shared-objects

    Interacting with AppDelegate

    To interact with HealthKit, the module may need to respond to app lifecycle events. This can be done by implementing the ExpoAppDelegateSubscriber protocol.

    import ExpoModulesCore
    
    public class ExpoHeadAppDelegateSubscriber: ExpoAppDelegateSubscriber {
    
    // Any AppDelegate methods you want to implement
      public func application(
        _ application: UIApplication,
        continue userActivity: NSUserActivity,
        restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
      ) -> Bool {
        launchedActivity = userActivity
    
       // ...
    
        return false
      }
    }
    

    Then add the subscriber to the expo-module.config.json:

    {
      "platforms": ["apple", "android", "web"],
      "apple": {
        "modules": ["ExpoHeadModule", ...],
        "appDelegateSubscribers": ["ExpoHeadAppDelegateSubscriber"]
      }
    }
    

    Expo ecosystem integration

    • Create a Config Plugin for setting up all permissions and entitlements.
    • Permissions APIs should follow Expo's permission model and implement hooks.
      • Ref: https://github.com/expo/expo/blob/843d5e108ff70539ac353721d3a7765a5d08d502/packages/expo-media-library/src/MediaLibrary.ts#L502-L519
    • Document when things don't work in Expo Go and link to dev client instructions.
    • Consider creating Expo devtools plugins for interacting with native APIs. Optimize for Claude Code usage, e.g. a Bun CLI before a UI.
      • Ref: https://docs.expo.dev/debugging/devtools-plugins
    • If any feature launches the app or could benefit from deep linking, add an Expo Router integration. A good example is expo-quick-actions which has a expo-quick-actions/router import for automatic deep linking. Other good examples are Expo notifications (open settings, redirect notifications), widgets, siri shortcuts.
      • Ref: https://github.com/EvanBacon/expo-quick-actions

    Verification

    • Run yarn expo run:ios --no-bundler in an Expo app to headlessly compile the module and verify there are no compilation errors.
    Recommended Servers
    Svelte
    Svelte
    InstantDB
    InstantDB
    Astro Docs
    Astro Docs
    Repository
    evanbacon/apple-health
    Files