Why Widgets Cannot Be React Components
Adding a widget to your Expo app means building a native extension that runs entirely outside your JavaScript bundle. On iOS, a WidgetKit widget is a separate Swift target compiled into your app binary. It has no bridge to React Native, no access to your JS state, and cannot call any of your JavaScript at runtime. On Android, an App Widget is a BroadcastReceiver backed by a RemoteViews layout - again, a separate native component that the home screen OS renders directly, not something Expo's React Native runtime touches.
This is the single most important thing to understand before you start. Every tutorial that starts with 'install this package and import it in App.tsx' is describing a package that bridges widget display, not one that lets you write your widget in JavaScript. The widget itself is always native. What differs between approaches is how much of the native boilerplate is pre-written for you.
The Data Flow: App Group Is the Bridge
Before you write a line of widget code, decide how your widget will read data. The only supported path on both platforms is a shared storage layer your main app writes to and your widget reads from. On iOS this is an App Group - a container shared between your main app target and your widget extension, accessed via UserDefaults(suiteName: 'group.your.bundle.id'). On Android it is SharedPreferences with a named file your AppWidgetProvider reads directly.
Your Expo app writes the data the widget needs into that shared store whenever the relevant data changes - on every foreground session, after a network fetch, after a user action. The widget reads from it when the home screen OS asks it to render. There is no push or callback between them. If your app has not written to the store yet, the widget renders a placeholder or empty state.
- Decide what data your widget will show before you write any native code. Keep it small - a title, a number, a short string. Widgets update infrequently and the simpler the data model, the easier the shared store write.
- On iOS, enable the App Groups capability in your Apple Developer account before you create the widget target. The config plugin needs this entitlement to exist or the build will fail.
- On Android, name your SharedPreferences file something specific to your app so it does not collide with another app's preferences on the same device.
Setting Up an iOS Widget With a Config Plugin
The fastest path for an Expo managed workflow is react-native-widget-extension. Install it, configure it in app.json, and EAS Build handles the rest of the Xcode target setup. You write the widget in Swift inside a directory the package points to, and the config plugin adds the target, the App Group entitlement, and the required Info.plist entries to your build automatically without you opening Xcode.
- Enable App Groups for your bundle identifier in your Apple Developer account under Certificates, Identifiers and Profiles. Create a group named group.your.bundle.id (replace with your real bundle ID).
- Run npm install react-native-widget-extension and add it to the plugins array in your app.json with your bundle identifier and widget target name.
- Create a widgets directory in your project root. Inside it, write your Swift widget file. The package's documentation shows the minimum WidgetKit structure - a Provider that returns a timeline of entries, an Entry struct, and a View that renders it.
- Write the App Group read in your Swift widget: let defaults = UserDefaults(suiteName: 'group.your.bundle.id') and then pull the keys your main app writes.
- Write the App Group write from your React Native app using @react-native-async-storage/async-storage configured to the group, or use a dedicated package like react-native-shared-group-preferences.
- Run eas build --platform ios to compile. The config plugin wires the target into the binary; you never touch an Xcode project file.
Setting Up an Android App Widget
Android App Widgets need three pieces: an AppWidgetProvider class (a BroadcastReceiver that handles update events), a RemoteViews XML layout defining the widget's look, and an appwidget-provider XML metadata file that tells Android the widget's dimensions, update frequency, and initial layout. All three get wired into your AndroidManifest.xml via a config plugin.
In an Expo managed workflow, you write a local config plugin that copies the Kotlin file, the two XML files, and the manifest entries into the native project at build time. This is a few dozen lines of JavaScript that Expo's config plugin API makes straightforward. Claude Code can write the entire plugin once you describe the three native files you want to add.
- Write the AppWidgetProvider Kotlin class. It overrides onUpdate(), reads your SharedPreferences, builds a RemoteViews object, and calls appWidgetManager.updateAppWidget(). Keep the logic minimal - this runs in a background process with tight memory constraints.
- Write the layout XML as a RemoteViews layout. Only a subset of standard Android views are supported in RemoteViews: TextView, ImageView, Button, LinearLayout, RelativeLayout, FrameLayout. No custom views, no Compose.
- Write the appwidget-provider XML with minWidth, minHeight, updatePeriodMillis (minimum 30 minutes on Android), and initialLayout pointing to your layout file.
- Write the config plugin to copy all three files into the native project and add the receiver and meta-data elements to AndroidManifest.xml inside the application tag.
- Add the plugin to your app.json plugins array and run eas build --platform android.
Populating the Widget From Your React Native App
Once the native widget can read from shared storage, you need your React Native app to write there. On iOS the most reliable package for this is react-native-shared-group-preferences, which exposes a setItem / getItem API scoped to an App Group. On Android, react-native-shared-preferences or a simple native module wrapping SharedPreferences both work. Write to the shared store in your app at the points where the underlying data changes - after a successful API call, after a user action, when the app comes to foreground.
On iOS you can also trigger a widget timeline reload from your app using WidgetKit's reloadAllTimelines. This forces the widget to re-read the store and re-render immediately rather than waiting for the OS to schedule the next update. The package react-native-widget-extension exposes this as a reloadAllTimelines() call you can trigger from JavaScript. Call it right after you write new data to the App Group.
What Claude Code Handles and What You Decide
The mechanical parts of this - the Swift widget file, the Kotlin provider, the two Android XML files, the config plugin JavaScript, the shared store reads and writes - are all things Claude Code writes well once you give it the data model and a description of what the widget should show. Prompt it with your data shape and a plain description of the widget UI, and it produces a working draft you can review and adjust.
What Claude Code cannot decide is what your widget should actually show and how often it should update. A widget that displays stale or empty data because the app never wrote to the shared store looks broken even if every line of code is correct. The design decisions - which piece of data earns a place on someone's home screen, and when it gets written - are the ones that make a widget worth adding at all.
- Pick one high-value piece of data, not a dashboard. Home screen widgets that try to show everything show nothing useful. The most-used widgets show one number, one status, or one next action.
- Make sure your app writes to shared storage on every foreground session, not just when users explicitly trigger a save. A widget that shows yesterday's count because the app only writes on explicit saves will get deleted.
- Test the widget on a real device before you submit. The simulator handles the basics, but real home screen rendering and the update cycle only behave exactly as they will in production on device.
TL;DR
Add an iOS widget via react-native-widget-extension: enable an App Group in your Apple Developer account, write a small Swift WidgetKit target, and let the config plugin handle the Xcode wiring in EAS Build. Add an Android App Widget via a local config plugin: write the Kotlin provider, the RemoteViews layout XML, and the appwidget-provider metadata, then copy them into the native project at build time. In both cases the data bridge is shared storage your main app writes and your widget reads. Claude Code writes all the native boilerplate once you describe the data and the display - your job is to pick what is worth showing on someone's home screen.
Short, practical drops on app ideas, validating fast, store listings, and passing app review. No spam, unsubscribe anytime.
Frequently asked questions
Can I write my Expo widget in React Native instead of Swift or Kotlin?
No. Widgets are native extensions that run in their own process outside the React Native runtime. You write iOS widgets in Swift using WidgetKit, and Android widgets in Kotlin using AppWidgetProvider and RemoteViews. Packages like react-native-widget-extension handle the EAS Build wiring, but the widget UI itself is always native code.
Do I need to eject from Expo managed workflow to add a widget?
No. You can add a widget in Expo managed workflow using a config plugin - either a community package like react-native-widget-extension for iOS, or a local config plugin you write for Android. The plugin adds the native files and manifest entries at EAS Build time so you never modify the native project directly.
How does my widget know what data to display?
Through shared storage your main app writes to. On iOS this is an App Group accessed via UserDefaults with a suite name matching your group identifier. On Android it is SharedPreferences. Your React Native app writes the relevant data whenever it changes, and the widget reads from that same store when the OS asks it to render.
How often can an iOS widget update its data?
WidgetKit gives each widget a timeline of entries and decides when to render each one based on the system budget. You can also trigger an immediate reload from your app by calling reloadAllTimelines() right after you write new data to the App Group. For real-time data, this is the path - write the new value, then call reload.
Will Apple reject my app for having a widget?
Not for having a widget. App Review checks that your widget accurately represents what it claims to show and that it does not use widget placement as a mechanism to display ads or misleading content. A widget that shows a genuine piece of data from your app - a counter, a status, a next item - is straightforward to review.
Last reviewed by David on September 10, 2026


