How to access react-native-mmkv in a iOS Widget (React Native Expo)

AKSHAY JADHAVAKSHAY JADHAV
12 min read

In one of my recent React Native Expo apps, I was using react-native-mmkv, a library that allows you to easily use MMKV (efficient, small mobile key-value storage framework developed by WeChat) inside your React Native apps, as the persist layer for @legendapp/state. Everything worked great until I started building an iOS widget and needed to access some of that persisted data from the widget itself.

At first, I tried accessing the MMKV file manually using the app group path and decoding the raw bytecode. That didn’t go so well. After digging through the official MMKV docs, I realized the proper way to access data in extensions like widgets is by using MMKVAppExtension, a separate module built for that purpose.

And that’s when the real journey began figuring out how to get MMKVAppExtension working inside a widget in a React Native Expo app.

💥 The Problem(s)

Once I realized I needed to use MMKVAppExtension, I assumed it would be as simple as adding the pod and importing it in Swift. Not quite.

Here’s what went wrong:

App Group Setup & MMKV Path

According to the react-native-mmkv docs, if you want to share storage between your main app and other targets (like widgets), you just need to configure the App Group and enable the App Group capability. MMKV will then automatically store data in the shared group container, making it accessible across targets.

Sounds simple enough, right? Well, here’s the catch:

By default, react-native-mmkv stores its data in $(Documents)/mmkv/. But when using App Groups, it doesn’t append the /mmkv directory it just saves everything directly in the App Group root path ($(AppGroupPath)/). (GitHub issue)

Default Setup Without App Group Creates mmkv Folder Automatically

No mmkv Folder When App Group Is Set in Info.plist

This behavior is different from the native iOS MMKV lib, which always uses the /mmkv subdirectory. So if you try to access that shared storage from Swift using the native MMKV API, it looks inside the /mmkv folder but the data isn’t there. It’s one level above.

This mismatch makes it feel like the widget and app are out of sync.

The Fix

When initializing MMKV on the React Native side, make sure to pass a custom path and explicitly set it to include the /mmkv directory within your App Group:

import { Paths } from "expo-file-system/next";
import { MMKV, Mode } from "react-native-mmkv";

// Get the URI of the App Group container using expo-file-system's next API
const uri = Paths.appleSharedContainers["group.bundle.id"]?.uri;

// Convert the URI to a file system path (removes "file://" prefix)
const path = new URL(uri).pathname;

const storage = new MMKV({
  id: "storage",
  mode: Mode.MULTI_PROCESS, // Required for sharing between app and extensions
  path: `${path}mmkv`, // Ensure path includes /mmkv folder to match native MMKV behavior
});

This ensures both sides (widget and React Native app) are looking at the exact same location.

Static vs Dynamic Linking

MMKV provides both static and dynamic frameworks. According to their documentation, dynamic frameworks are recommended (iOS setup) for App Extensions so I initially tried to follow that guidance.

I updated my widget Podfile like this:

use_frameworks! :linkage => :dynamic
pod 'MMKVAppExtension'

But then I hit this CocoaPods error:

💡
[!] Unable to integrate the following embedded targets with their respective host targets mmkvwidgetdemo (false) and widget (true) do not both set use_frameworks!.

This basically means:

Your main app target (mmkvwidgetdemo) and your extension target (widget) must both use the same use_frameworks! configuration. One was set to dynamic, the other wasn’t.

Fixing mismatch

To fix this mismatch, I updated expo-build-properties in app.json like this:

plugins: [
  [
    "expo-build-properties",
    {
      ios: {
        useFrameworks: "dynamic"
      }
    }
  ]
]

This resolved the CocoaPods error but introduced a new problem during the build:

App Build Fails (MMKV Linking Error)

Once I enabled dynamic linking, the build failed when trying to compile react-native-mmkv, showing this error:

While digging into this, I found a related issue on GitHub, where someone ran into a similar problem when using use_frameworks!. They resolved it by switching to static linking with frameworks.

Based on that, I decided to try the same and it worked. So, even though dynamic linking might work in some setups, using static linking with use_frameworks! :linkage => :static seemed to be the more reliable path, especially with react-native-mmkv.

Implementation: What Finally Worked

Before we jump into the steps, here’s the base project I was working with:

Starter Project Setup

  • A standard Expo app created with the default starter template.

  • A widget target added using @bacons/apple-targets

💡
You can find a similar starter setup here: https://github.com/akshayjadhav4/mmkv-widget-demo/tree/starter#

Now let’s look at how I configured MMKV using static linking, which worked cleanly with Expo and iOS widget.

React Native

On the app side, we’ll install react-native-mmkv and configure it to write data into the App Group directory so the widget can read it later.

Install Dependencies

npx expo install react-native-mmkv expo-build-properties

Configuring App Group in app.json

In an Expo-managed app (with prebuild), native entitlements and Info.plist changes should be done via app.json file. Here’s what you need to add under the ios key:

{
....
..
  "ios": {
    "supportsTablet": true,
    "appleTeamId": "YOUR_TEAM_ID",
    "bundleIdentifier": "your_bundle_identifier", // Your app’s unique identifier
    "entitlements": {
      "com.apple.security.application-groups": [
        "group.your_bundle_identifier"
      ]
    },
    "infoPlist": {
      "AppGroup": "group.your_bundle_identifier"
    }
  }
}

What Each Field Does:

  • bundleIdentifier: Your app’s unique identifier.

  • entitlements.com.apple.security.application-groups: Adds the App Group capability to your app so it can access shared storage with the widget.

  • infoPlist.AppGroup: This adds a AppGroup key to your compiled Info.plist required for MMKV.

Enable Static Framework Linking

We initially ran into issues trying to use MMKV with dynamic frameworks specifically, build errors due to symbol conflicts and missing symbols.

Since react-native-mmkv worked best with static linking, and we’ll also be installing MMKVAppExtension statically via CocoaPods, we explicitly enable static frameworks in our Expo setup using the expo-build-properties plugin:

In app.json

"plugins": [
  [
    "expo-build-properties",
    {
      "ios": {
        "useFrameworks": "static"
      }
    }
  ]
]

Creating MMKV Instance with App Group Path

To ensure the iOS widget can read the same data, we need to configure react-native-mmkv to use the App Group container path instead of the default internal storage.

MMKV won’t automatically create a mmkv directory inside the App Group path, so we manually set the path to ensure both the app and widget point to the same location.

import { Paths } from "expo-file-system/next";
import { MMKV, Mode } from "react-native-mmkv";
import { Platform } from "react-native";

// Get the URI of the App Group container using expo-file-system's next API
const uri = Paths.appleSharedContainers["group.your_bundle_identifier"]?.uri;

// Convert the URI to a file system path (removes "file://" prefix)
const sharedPath = new URL(uri).pathname;

const path =
  Platform.OS === "ios"
    ? `${sharedPath}mmkv` // Ensure path includes /mmkv folder to match native MMKV behavior
    : undefined; // : On Android, react-native-mmkv will fall back to its default path, which is totally fine if as here widget is iOS-only.

const storage = new MMKV({
  id: "storage",
  mode: Mode.MULTI_PROCESS, // Required for sharing between app and extensions
  path: path,
});

when set to mode: MULTI_PROCESS, the MMKV instance will assume data can be changed from the outside (e.g. App Clips, Extensions or App Groups).

Once you’ve run the app with the shared path correctly configured in React Native, you’ll notice that MMKV now creates a /mmkv folder inside your App Group container:

Saving Data to MMKV

// Save data
storage.set("userName", "USER");

// Read it back
const name = storage.getString("userName");
console.log("Name from MMKV:", name);

iOS Widget: Reading Shared MMKV Data

Now that our React Native app is writing data into the shared App Group location, let’s set up the Widget target to read from the exact same MMKV storage.

Add MMKVAppExtension Pod for the Widget Target

I used @bacons/apple-targets to generate my iOS widget target. It handled most of the setup automatically but I still needed to manually install the MMKVAppExtension pod for the widget to work correctly.

To do that, I created a Podfile at the root of my widget folder: targets/widget/pods.rb

Inside that file, I added the following:

use_frameworks! linkage: :static
pod 'MMKVAppExtension'

After adding that to targets/widget/pods.rb, I ran the following command to regenerate the iOS project and make sure CocoaPods installs the new dependencies.

npx expo prebuild -p ios --clean

After running the expo prebuild command, open your Xcode project. In the sidebar under Pods, you should see MMKVAppExtension listed along with its associated files like libMMKV, MMKVHandler, etc.

To open Xcode from project root run command:

xed ios

If it appears like as shown in the screenshot, you’re good to go. MMKV is now available to use inside your widget target.

Initializing MMKV for iOS Widget

When you’re working with MMKV in your iOS app, the official documentation suggests initializing it within the AppDelegate. However, widgets don’t have an AppDelegate since they are lightweight and don’t follow the traditional app lifecycle. So, where do you initialize MMKV for a widget?

In the case of widgets, we can use the WidgetBundle’s init() method to perform this initialization. This ensures that MMKV is properly set up when the widget is loaded, allowing you to share data between your app and its widget.

Here’s how I did it:

import WidgetKit
import SwiftUI
import MMKVAppExtension

@main
struct exportWidgets: WidgetBundle {

  init() {
    // Define the App Group identifier (must match the one in app.json and Info.plist)
    let appGroupIdentifier = "group.your_bundle_identifier"

    // Get the path to the shared container for the app group
    guard let groupDir = FileManager.default.containerURL(
      forSecurityApplicationGroupIdentifier: appGroupIdentifier
    )?.path else {
      fatalError("Couldn't find app group container directory")
    }

    // Initialize MMKV with the shared container directory
    // This allows both the main app and the widget to read/write from the same MMKV storage
    MMKV.initialize(
      rootDir: nil,
      groupDir: groupDir,    // Shared app group directory
      logLevel: .info        // Set logging level (for debugging)
    )
  }

  var body: some Widget {
    widget()
  }
}
  • WidgetBundle: The exportWidgets struct conforms to WidgetBundle, which is a container for one or more widgets. Since widgets don’t use an AppDelegate, we initialize our storage and shared resources inside the init() method of this struct.

  • App Group Identifier: We specify the App Group identifier that must match the one defined in both the app.json and Info.plist. This allows the app and the widget to access shared resources.

  • Shared Container URL: Using FileManager.default.containerURL(forSecurityApplicationGroupIdentifier:), we fetch the path to the app’s shared container directory. This shared directory is where both the main app and widget can store and access data.

  • Initializing MMKV: MMKV is initialized using the shared app group directory (groupDir). This means the main app and the widget both point to the same MMKV storage, ensuring they can read and write from the same data source.

Accessing MMKV Data Inside the Timeline Provider

To read data from the shared MMKV instance inside the widget’s TimelineProvider, you need to use the same mmapID and mode that you used in the react-native app.

import MMKVAppExtension 

private func getUsername() -> String {
    // MMKV instance using the same mmapID and MULTI_PROCESS mode
    let userMMKV = MMKV(mmapID: "storage", mode: .multiProcess)
    // Read a string value (defaults to a placeholder if not found)
    return userMMKV?.string(forKey: "userName") ?? "No User"
}

Full TimelineProvider Code:

struct Provider: AppIntentTimelineProvider {
    private func getUsername() -> String {
        let userMMKV = MMKV(mmapID: "storage", mode: .multiProcess)
        return userMMKV?.string(forKey: "userName") ?? "No User"
    }

    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date(), configuration: ConfigurationAppIntent(), username: "User")
    }

    func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> SimpleEntry {
        return SimpleEntry(date: Date(), configuration: configuration, username: getUsername())
    }

    func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline<SimpleEntry> {
        var entries: [SimpleEntry] = []
        let username = getUsername()

        // Generate a timeline consisting of five entries an hour apart, starting from the current date.
        let currentDate = Date()
        for hourOffset in 0 ..< 5 {
            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
            let entry = SimpleEntry(date: entryDate, configuration: configuration, username: username)
            entries.append(entry)
        }

        return Timeline(entries: entries, policy: .atEnd)
    }
}

struct SimpleEntry: TimelineEntry {
    let date: Date
    let configuration: ConfigurationAppIntent
    let username: String
}

Wrapping Up

At this point, your widget should be able to read MMKV data saved from the React Native app, thanks to:

  • Consistent use of mmapID and .multiProcess mode across both app and widget.

  • Correct setup of the App Group and pointing MMKV to its path.

  • Using static linking for MMKVAppExtension via CocoaPods.

If everything’s in place, you can now rebuild the app and see your widget reflect the stored data from the app side.

Final Code: https://github.com/akshayjadhav4/mmkv-widget-demo

Bonus: Accessing MMKVAppExtension with Bridging Header - Avoid If Using Prebuild

Warning: This method is not compatible with Expo Prebuild workflows. The bridging header path won’t be correctly injected into the native Xcode build settings, which will result in build errors unless you manually patch expo-apple-targets SWIFT_OBJC_BRIDGING_HEADER in target build settings.

You might wonder: “Can’t I just install MMKVAppExtension without messing with "useFrameworks" settings from app.json”

Yes, you can install:

pod 'MMKVAppExtension'

but it won’t automatically work in Swift code. That’s because MMKVAppExtension is written in Objective-C, and without proper linking, Swift won’t recognize it as a module leading to: No such module 'MMKVAppExtension' error.

To make it work, you need to manually bridge the Objective-C headers:

  • Create a Bridging Header

    • In Xcode, right-click on your widget target folder (e.g., widget/) in the file navigator.

    • Select New File from template → Choose iOS > Source > Objective-C File and click Next.

    • Name the file anything (e.g., WidgetBridgingHeader.m) and make sure your widget target is selected under "Targets".

    • Click Create.

    • Xcode will prompt: “Would you like to configure an Objective-C bridging header?”

    • Click Yes.

    • Xcode creates a file named something like widget-Bridging-Header.h.

    • Inside this new .h file, add:

#import <MMKVAppExtension/MMKV.h>

That’s it! Now your Swift code inside the widget can access MMKV through the Objective-C bridge.

The code remains unchanged the only difference is that you no longer need to import MMKVAppExtension in the Swift file as we did before.

💡
If you’re curious about trying this approach with Expo Prebuild, I’ve shared a patched repository. It includes a tweak to @bacons/apple-targets that automatically sets the SWIFT_OBJC_BRIDGING_HEADER in your target’s build settings.

Hope this helped you get MMKV working smoothly across your app and widget! Happy building 🚀

0
Subscribe to my newsletter

Read articles from AKSHAY JADHAV directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

AKSHAY JADHAV
AKSHAY JADHAV

Software Engineer