📄 expo/modules/native-module-tutorial

File: native-module-tutorial.md | Updated: 11/15/2025

Source: https://docs.expo.dev/modules/native-module-tutorial

Hide navigation

Search

Ctrl K

Home Guides EAS Reference Learn

Archive Expo Snack Discord and Forums Newsletter

Tutorial: Create a native module

Edit page

Copy page

A tutorial on creating a native module that persists settings with Expo Modules API.

Edit page

Copy page


In this tutorial, you build a module that stores the user's preferred app theme: dark, light, or system. On Android, use SharedPreferences , and on iOS, use UserDefaults . You can implement web support with localStorage , but this tutorial does not cover that.

Watch: How to create a native module with the Expo Modules API

Watch: How to create a native module with the Expo Modules API


1

Initialize a new module


First, create a new module. For this tutorial, the module is named expo-settings or ExpoSettings. You can choose a different name, but adjust the instructions to match your choice.

Terminal

Copy

- npx create-expo-module expo-settings

Since you aren't going to actually ship this library, you can hit return for all the prompts to accept the default values.

2

Set up workspace


Clean up the default module to start with a clean slate. Delete the view module since this guide does not use it.

Terminal

Copy

- cd expo-settings

- rm ios/ExpoSettingsView.swift

- rm android/src/main/java/expo/modules/settings/ExpoSettingsView.kt

- rm src/ExpoSettingsView.tsx

- rm src/ExpoSettingsView.web.tsx src/ExpoSettingsModule.web.ts

Locate the following files and replace their contents with the provided minimal boilerplate:

android/src/main/java/expo/modules/settings/ExpoSettingsModule.kt

Copy

package expo.modules.settings import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition class ExpoSettingsModule : Module() { override fun definition() = ModuleDefinition { Name("ExpoSettings") Function("getTheme") { return@Function "system" } } }

ios/ExpoSettingsModule.swift

Copy

import ExpoModulesCore public class ExpoSettingsModule: Module { public func definition() -> ModuleDefinition { Name("ExpoSettings") Function("getTheme") { () -> String in "system" } } }

src/ExpoSettings.types.ts

Copy

export type ExpoSettingsModuleEvents = {};

src/ExpoSettingsModule.ts

Copy

import { NativeModule, requireNativeModule } from 'expo'; import { ExpoSettingsModuleEvents } from './ExpoSettings.types'; declare class ExpoSettingsModule extends NativeModule<ExpoSettingsModuleEvents> { getTheme: () => string; } // This call loads the native module object from the JSI. export default requireNativeModule<ExpoSettingsModule>('ExpoSettings');

src/index.ts

Copy

import ExpoSettingsModule from './ExpoSettingsModule'; export function getTheme(): string { return ExpoSettingsModule.getTheme(); }

example/App.tsx

Copy

import * as Settings from 'expo-settings'; import { Text, View } from 'react-native'; export default function App() { return ( <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> <Text>Theme: {Settings.getTheme()}</Text> </View> ); }

3

Run the example project


Start the TypeScript compiler to watch for changes.

Terminal

Copy

# Run this in the root of the project to start the TypeScript compiler

- npm run build

In a separate terminal window, run the example app.

Terminal

Copy

- cd example

# Run the example app on Android

- npx expo run:android

# Run the example app on iOS

- npx expo run:ios

You should see the text "Theme: system" in the center of the screen when you launch the example app. The value "system" comes from synchronously calling the getTheme() function in the native module. You will change this value in the next step.

4

Get, set, and persist the theme preference value


Android native module

To read the value, look for a SharedPreferences string under the key "theme". If the key does not exist, default to "system". Use the reactContext (a React Native ContextWrapper ) to access the SharedPreferences instance with getSharedPreferences().

To set the value, use the edit() method of SharedPreferences to get an Editor instance. Then, use putString() to set the value. Ensure the setTheme function accepts a value of type String.

android/src/main/java/expo/modules/settings/ExpoSettingsModule.kt

Copy

package expo.modules.settings import android.content.Context import android.content.SharedPreferences import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition class ExpoSettingsModule : Module() { override fun definition() = ModuleDefinition { Name("ExpoSettings") Function("setTheme") { theme: String -> getPreferences().edit().putString("theme", theme).commit() } Function("getTheme") { return@Function getPreferences().getString("theme", "system") } } private val context get() = requireNotNull(appContext.reactContext) private fun getPreferences(): SharedPreferences { return context.getSharedPreferences(context.packageName + ".settings", Context.MODE_PRIVATE) } }

Show More

iOS native module

To read the value on iOS, look for a UserDefaults string under the key "theme". If the key does not exist, default to "system".

To set the value, use the set(_:forKey:) method of UserDefaults. Ensure the setTheme function accepts a value of type String.

ios/ExpoSettingsModule.swift

Copy

import ExpoModulesCore public class ExpoSettingsModule: Module { public func definition() -> ModuleDefinition { Name("ExpoSettings") Function("setTheme") { (theme: String) -> Void in UserDefaults.standard.set(theme, forKey:"theme") } Function("getTheme") { () -> String in UserDefaults.standard.string(forKey: "theme") ?? "system" } } }

TypeScript module

Update the ExpoSettingsModule.ts to add a TypeScript interface for the ExpoSettingsModule native module to update the theme.

src/ExpoSettingsModule.ts

Copy

import { NativeModule, requireNativeModule } from 'expo'; import { ExpoSettingsModuleEvents } from './ExpoSettings.types'; declare class ExpoSettingsModule extends NativeModule<ExpoSettingsModuleEvents> { setTheme: (theme: string) => void; getTheme: () => string; } // This call loads the native module object from the JSI. export default requireNativeModule<ExpoSettingsModule>('ExpoSettings');

Now, call your native modules from TypeScript.

src/index.ts

Copy

import ExpoSettingsModule from './ExpoSettingsModule'; export function getTheme(): string { return ExpoSettingsModule.getTheme(); } export function setTheme(theme: string): void { return ExpoSettingsModule.setTheme(theme); }

Example app

You can now use the Settings API in your example app.

example/App.tsx

Copy

import * as Settings from 'expo-settings'; import { Button, Text, View } from 'react-native'; export default function App() { const theme = Settings.getTheme(); // Toggle between dark and light theme const nextTheme = theme === 'dark' ? 'light' : 'dark'; return ( <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> <Text>Theme: {Settings.getTheme()}</Text> <Button title={`Set theme to ${nextTheme}`} onPress={() => Settings.setTheme(nextTheme)} /> </View> ); }

When you rebuild and run the app, the "system" theme is still set. Pressing the button does nothing, but when you reload the app, the theme changes. This happens because the app does not fetch the new theme value or re-render. You will fix this in the next step.

5

Emit change events for the theme value


Ensure developers using your API can react to theme value changes by emitting a change event whenever the value updates. Use the Events definition component to describe the events your module emits, sendEvent to emit the event from native code, and the EventEmitter API to subscribe to events in JavaScript. The event payload is { theme: string }.

Android native module

Events payloads are represented as Bundle instances on Android, which you can create using the bundleOf function.

android/src/main/java/expo/modules/settings/ExpoSettingsModule.kt

Copy

package expo.modules.settings import android.content.Context import android.content.SharedPreferences import androidx.core.os.bundleOf import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition class ExpoSettingsModule : Module() { override fun definition() = ModuleDefinition { Name("ExpoSettings") Events("onChangeTheme") Function("setTheme") { theme: String -> getPreferences().edit().putString("theme", theme).commit() this@ExpoSettingsModule.sendEvent("onChangeTheme", bundleOf("theme" to theme)) } Function("getTheme") { return@Function getPreferences().getString("theme", "system") } } private val context get() = requireNotNull(appContext.reactContext) private fun getPreferences(): SharedPreferences { return context.getSharedPreferences(context.packageName + ".settings", Context.MODE_PRIVATE) } }

Show More

iOS native module

ios/ExpoSettingsModule.swift

Copy

import ExpoModulesCore public class ExpoSettingsModule: Module { public func definition() -> ModuleDefinition { Name("ExpoSettings") Events("onChangeTheme") Function("setTheme") { (theme: String) -> Void in UserDefaults.standard.set(theme, forKey:"theme") sendEvent("onChangeTheme", [ "theme": theme ]) } Function("getTheme") { () -> String in UserDefaults.standard.string(forKey: "theme") ?? "system" } } }

Show More

TypeScript module

src/ExpoSettings.types.ts

Copy

export type ThemeChangeEvent = { theme: string; }; export type ExpoSettingsModuleEvents = { onChangeTheme: (params: ThemeChangeEvent) => void; };

src/index.ts

Copy

import { EventSubscription } from 'expo-modules-core'; import ExpoSettingsModule from './ExpoSettingsModule'; import { ThemeChangeEvent } from './ExpoSettings.types'; export function addThemeListener(listener: (event: ThemeChangeEvent) => void): EventSubscription { return ExpoSettingsModule.addListener('onChangeTheme', listener); } export function getTheme(): string { return ExpoSettingsModule.getTheme(); } export function setTheme(theme: string): void { return ExpoSettingsModule.setTheme(theme); }

Example app

example/App.tsx

Copy

import * as Settings from 'expo-settings'; import { useEffect, useState } from 'react'; import { Button, Text, View } from 'react-native'; export default function App() { const [theme, setTheme] = useState<string>(Settings.getTheme()); useEffect(() => { const subscription = Settings.addThemeListener(({ theme: newTheme }) => { setTheme(newTheme); }); return () => subscription.remove(); }, [setTheme]); // Toggle between dark and light theme const nextTheme = theme === 'dark' ? 'light' : 'dark'; return ( <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> <Text>Theme: {Settings.getTheme()}</Text> <Button title={`Set theme to ${nextTheme}`} onPress={() => Settings.setTheme(nextTheme)} /> </View> ); }

Show More

6

Improve type safety with Enums


It's easy to make mistakes when using the Settings.setTheme() API in its current form, as it allows any string value. Improve the type safety of this API by using an enum to restrict the possible values to system, light, and dark.

Android native module

android/src/main/java/expo/modules/settings/ExpoSettingsModule.kt

Copy

package expo.modules.settings import android.content.Context import android.content.SharedPreferences import androidx.core.os.bundleOf import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition import expo.modules.kotlin.types.Enumerable class ExpoSettingsModule : Module() { override fun definition() = ModuleDefinition { Name("ExpoSettings") Events("onChangeTheme") Function("setTheme") { theme: Theme -> getPreferences().edit().putString("theme", theme.value).commit() this@ExpoSettingsModule.sendEvent("onChangeTheme", bundleOf("theme" to theme.value)) } Function("getTheme") { return@Function getPreferences().getString("theme", Theme.SYSTEM.value) } } private val context get() = requireNotNull(appContext.reactContext) private fun getPreferences(): SharedPreferences { return context.getSharedPreferences(context.packageName + ".settings", Context.MODE_PRIVATE) } } enum class Theme(val value: String) : Enumerable { LIGHT("light"), DARK("dark"), SYSTEM("system") }

Show More

iOS native module

ios/ExpoSettingsModule.swift

Copy

import ExpoModulesCore public class ExpoSettingsModule: Module { public func definition() -> ModuleDefinition { Name("ExpoSettings") Events("onChangeTheme") Function("setTheme") { (theme: Theme) -> Void in UserDefaults.standard.set(theme.rawValue, forKey:"theme") sendEvent("onChangeTheme", [ "theme": theme.rawValue ]) } Function("getTheme") { () -> String in UserDefaults.standard.string(forKey: "theme") ?? Theme.system.rawValue } } enum Theme: String, Enumerable { case light case dark case system } }

Show More

TypeScript module

src/ExpoSettings.types.ts

Copy

export type Theme = 'light' | 'dark' | 'system'; export type ThemeChangeEvent = { theme: Theme; }; export type ExpoSettingsModuleEvents = { onChangeTheme: (params: ThemeChangeEvent) => void; };

src/ExpoSettingsModule.ts

Copy

import { NativeModule, requireNativeModule } from 'expo'; import { ExpoSettingsModuleEvents, Theme } from './ExpoSettings.types'; declare class ExpoSettingsModule extends NativeModule<ExpoSettingsModuleEvents> { setTheme: (theme: Theme) => void; getTheme: () => Theme; } // This call loads the native module object from the JSI. export default requireNativeModule<ExpoSettingsModule>('ExpoSettings');

src/index.ts

Copy

import { EventSubscription } from 'expo-modules-core'; import ExpoSettingsModule from './ExpoSettingsModule'; import { Theme, ThemeChangeEvent } from './ExpoSettings.types'; export function addThemeListener(listener: (event: ThemeChangeEvent) => void): EventSubscription { return ExpoSettingsModule.addListener('onChangeTheme', listener); } export function getTheme(): Theme { return ExpoSettingsModule.getTheme(); } export function setTheme(theme: Theme): void { return ExpoSettingsModule.setTheme(theme); }

Example app

If you change Settings.setTheme(nextTheme) to Settings.setTheme("not-a-real-theme"), TypeScript will raise an error. If you ignore the error and press the button, you will see the following runtime error:

ERROR Error: FunctionCallException: Calling the 'setTheme' function has failed (at ExpoModulesCore/SyncFunctionComponent.swift:76) → Caused by: ArgumentCastException: Argument at index '0' couldn't be cast to type Enum<Theme> (at ExpoModulesCore/JavaScriptUtils.swift:41) → Caused by: EnumNoSuchValueException: 'not-a-real-theme' is not present in Theme enum, it must be one of: 'light', 'dark', 'system' (at ExpoModulesCore/Enumerable.swift:37)

The last line of the error message shows that not-a-real-theme is not a valid value for the Theme enum. The only valid values are light, dark, and system.

Congratulations! You have created your first Expo Module for Android and iOS.

Next steps


Expo Modules API Reference

Create native modules using Kotlin and Swift.
Tutorial: Creating a native view

A tutorial on creating a native view with Expo Modules API.