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


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:
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
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
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.
Hope this helped you get MMKV working smoothly across your app and widget! Happy building 🚀
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