Skip to main content
Targets like widgets, share extensions, and App Clips run in separate processes and can’t directly access your app’s data. App Groups provide a shared container for storing data accessible to all your targets.

How It Works

App Groups create a shared storage area identified by a group ID (e.g., group.com.yourapp.data). All targets with the same App Group entitlement can read and write to this storage using NSUserDefaults.

Setup

1

Define App Group in main app

Add the App Group entitlement to app.json:
app.json
{
  "expo": {
    "ios": {
      "bundleIdentifier": "com.yourapp",
      "entitlements": {
        "com.apple.security.application-groups": [
          "group.com.yourapp.data"
        ]
      }
    },
    "plugins": ["@bacons/apple-targets"]
  }
}
A good default is group.<bundle-identifier>. Use a more generic name if you plan to share across multiple apps.
2

Add App Group to your target

Configure your target to use the same App Group:
targets/widget/expo-target.config.js
/** @type {import('@bacons/apple-targets/app.plugin').ConfigFunction} */
module.exports = (config) => ({
  type: "widget",
  entitlements: {
    // Sync App Groups from main app
    "com.apple.security.application-groups":
      config.ios.entitlements["com.apple.security.application-groups"],
  },
});
Or hardcode the group:
module.exports = {
  type: "widget",
  entitlements: {
    "com.apple.security.application-groups": ["group.com.yourapp.data"],
  },
};
3

Run prebuild

npx expo prebuild -p ios --clean
You may need to create an EAS Build or open Xcode to sync the entitlements with Apple’s servers.

ExtensionStorage API

The @bacons/apple-targets package provides a native module for easy interaction with shared storage:

Writing Data from React Native

import { ExtensionStorage } from "@bacons/apple-targets";

// Create storage instance with your App Group ID
const storage = new ExtensionStorage("group.com.yourapp.data");

// Store a string
storage.set("userName", "Evan");

// Store a number
storage.set("score", 42);

// Store an object
storage.set("user", {
  name: "Evan",
  score: 42,
});

// Store an array of objects
storage.set("leaderboard", [
  { name: "Alice", score: 100 },
  { name: "Bob", score: 90 },
]);

// Remove a key
storage.set("oldKey", undefined);
// or
storage.remove("oldKey");

Reading Data from React Native

import { ExtensionStorage } from "@bacons/apple-targets";

const storage = new ExtensionStorage("group.com.yourapp.data");

// Get a value (returns string | null)
const userName = storage.get("userName");

// Parse JSON for objects
const userJson = storage.get("user");
if (userJson) {
  const user = JSON.parse(userJson);
  console.log(user.name, user.score);
}

Reading Data from Swift

Access the same data in your widget or extension:
let defaults = UserDefaults(suiteName: "group.com.yourapp.data")

// Read a string
let userName = defaults?.string(forKey: "userName")

// Read a number
let score = defaults?.integer(forKey: "score")

// Read an object (stored as JSON)
if let userData = defaults?.data(forKey: "user"),
   let user = try? JSONDecoder().decode(User.self, from: userData) {
    print("Name: \(user.name), Score: \(user.score)")
}

Writing Data from Swift

let defaults = UserDefaults(suiteName: "group.com.yourapp.data")

// Write a string
defaults?.set("New Value", forKey: "userName")

// Write a number
defaults?.set(100, forKey: "score")

// Write an object as JSON
let encoder = JSONEncoder()
if let userData = try? encoder.encode(user) {
    defaults?.set(userData, forKey: "user")
}

// Sync immediately
defaults?.synchronize()

Reloading Widgets

After updating shared data, reload your widgets to reflect the changes:
import { ExtensionStorage } from "@bacons/apple-targets";

const storage = new ExtensionStorage("group.com.yourapp.data");

// Update data
storage.set("score", 100);

// Reload all widgets
ExtensionStorage.reloadWidget();

// Or reload a specific widget by kind
ExtensionStorage.reloadWidget("widget");
For Control Widgets:
ExtensionStorage.reloadControls();

// Or reload a specific control
ExtensionStorage.reloadControls("com.yourapp.control");

Complete API Reference

ExtensionStorage Instance Methods

class ExtensionStorage {
  constructor(suiteName: string);

  // Store a value (undefined removes the key)
  set(
    key: string,
    value:
      | string
      | number
      | Record<string, string | number>
      | Array<Record<string, string | number>>
      | undefined
  ): void;

  // Get a value (returns null if not found)
  get(key: string): string | null;

  // Remove a key
  remove(key: string): void;
}

ExtensionStorage Static Methods

class ExtensionStorage {
  // Reload all widgets or a specific widget by kind
  static reloadWidget(name?: string): void;

  // Reload all controls or a specific control by kind
  static reloadControls(name?: string): void;
}

Example: Widget with Live Data

Here’s a complete example of a widget that displays data from your app:
import { useState } from 'react';
import { Button, Text, View } from 'react-native';
import { ExtensionStorage } from '@bacons/apple-targets';

const storage = new ExtensionStorage('group.com.yourapp.data');

export default function App() {
  const [count, setCount] = useState(0);

  const handlePress = () => {
    const newCount = count + 1;
    setCount(newCount);
    
    // Save to shared storage
    storage.set('count', newCount);
    
    // Reload the widget
    ExtensionStorage.reloadWidget();
  };

  return (
    <View>
      <Text>Count: {count}</Text>
      <Button title="Increment" onPress={handlePress} />
    </View>
  );
}

Background Updates

For more advanced use cases like updating widgets when your app is in the background, see:

Automatic App Group Sync

Some target types automatically sync App Groups from your main app:
  • Widgets (widget)
  • Share Extensions (share)
  • App Clips (clip)
  • Watch Apps (watch)
For these targets, you can omit the entitlements and they’ll inherit from your app.json.
You can override this behavior by explicitly setting entitlements in expo-target.config.js.

Troubleshooting

Entitlements not syncing: Run an EAS Build or open Xcode and navigate to Signing & Capabilities to force sync with Apple’s servers.
Data not appearing: Call synchronize() in Swift after writing data, and check that both targets have the exact same App Group ID.
Widget not updating: Make sure you’re calling ExtensionStorage.reloadWidget() after setting data.

Next Steps

Build docs developers (and LLMs) love