◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 27 days left. If your app calls it, check the migration target●SDK 58 — Moves to React Native 0.88. R8 is now on by default in Android release builds, and iOS prebuild generates a SceneDelegate●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 27 days left. If your app calls it, check the migration target●SDK 58 — Moves to React Native 0.88. R8 is now on by default in Android release builds, and iOS prebuild generates a SceneDelegate●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-06-15Advanced

Putting a Working Button in a Rork App's Widget — Implementing App Intents So a Tap Acts Without Opening the App

How to put a button in a Rork-generated Expo app's widget that changes state without launching the app. We wire App Intents and WidgetKit together through an App Group, all the way to reloadTimelines — including the two places I lost real time.

Rork611App Intents6WidgetKit11Expo234iOS118

✦ Premium Article

One morning I was looking at my habit-tracker widget and my hand just stopped. To tick a single item, I open the app, wait for a load screen, scroll to the row, then tap. Almost five seconds for one piece of one-tap data. If I could check it directly on the widget, those five seconds would become zero.

Since iOS 17, a button inside a widget can run work without launching the app. I knew the mechanism. But making it happen inside a Rork-generated, Expo-based app turned out to be far more involved than I expected — because the React Native world and the widget world run in different processes, in different languages.

This walks through getting the full round-trip working in a Rork app: tap a button in the widget, update state without opening the app, and have the widget redraw immediately. I'll leave the two places I got stuck explicit.

Why "add a button" doesn't work

Start with the structure. A home screen widget runs as an App Extension — a separate target from your app. There is no JavaScript engine inside that extension process. Not a single line of React Native runs there. That's exactly why asking Rork's chat to "add a button to my widget" gives you a button on an in-app screen, never one that completes its work on the home screen.

For a button inside a widget to act without launching the app, you have to provide and mesh together three parts yourself.

First, an App Intent — a unit of work you can hand to Button(intent:) that the system performs behind the scenes. Second, an App Group — the shared store that lets the app and the extension read and write the same data. Third, a timeline reload — the trigger that makes the widget redraw after the intent changes state.

Standard Rork (Expo-based) provides none of these automatically. Rork Max (which generates native Swift) can scaffold the whole WidgetKit extension for you, but if you're bolting this onto an app already shipped on Expo, the config-plugin route described here is the realistic one.

Get the App Group working first

Order matters. Before the button, make the app and the extension able to see the same data. If this isn't solid, you later get the worst kind of bug: the button fires, but nothing on screen changes, and the cause is hard to isolate.

Pick an App Group identifier (e.g. group.net.rorklab.habit). In your Apple Developer target settings, enable the same App Group on both the app and the extension. In Expo, add it to the entitlements in app.json.

{
  "expo": {
    "ios": {
      "entitlements": {
        "com.apple.security.application-groups": [
          "group.net.rorklab.habit"
        ]
      }
    }
  }
}

To write into this shared store from React Native, you need a native module that talks to UserDefaults(suiteName:) — a library like expo-shared-defaults, or a thin bridge added through a config plugin. The minimal Swift bridge looks like this.

import Foundation
 
@objc(SharedDefaults)
class SharedDefaults: NSObject {
  static let suite = UserDefaults(suiteName: "group.net.rorklab.habit")
 
  @objc func setBool(_ value: Bool, key: String) {
    SharedDefaults.suite?.set(value, forKey: key)
  }
 
  @objc func getBool(_ key: String, resolver resolve: RCTPromiseResolveBlock,
                     rejecter reject: RCTPromiseRejectBlock) {
    resolve(SharedDefaults.suite?.bool(forKey: key) ?? false)
  }
}

Now you have a two-way foundation: the widget extension can read a value the JS side wrote, and the JS side can read a value the extension wrote. Verify this round-trip with a single boolean first — it makes everything downstream far easier.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦Understand, with working Swift, how a button inside a widget runs without launching the app — the AppIntent / perform round-trip
✦A concrete path to connect a Rork Expo app and its extension target through an App Group: write-back, reloadTimelines, redraw
✦Take home the two traps that cost me two hours — taps doing nothing, and state rolling back — plus how to avoid each
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

⬡ Dev Tools2026-09-05
Your Widget Extension Can Submit a BGTaskScheduler Request. It Just Cannot Register the Handler
Calling BGTaskScheduler from a widget extension compiles, submits, and returns true — and then nothing runs. Here is why registration belongs to the host app only, and how I moved refresh ownership back where it belongs across my wallpaper apps.
⬡ Dev Tools2026-04-23
Integrating Siri and Shortcuts into Rork Apps: App Intents with Expo, Without the Pitfalls
A production-grade guide to adding Siri Shortcuts and App Intents to a React Native app generated by Rork. Bridge native Swift via an Expo Config Plugin, model AppShortcut / AppEntity / EntityQuery correctly, and ship Donations and Control Center widgets without breaking Apple's conventions.
⬡ Dev Tools2026-10-30
It runs in development but the iOS Release build dies on launch — checking whether a missing scheme is the cause
In an expo-router app, an app.json with no scheme can crash only the iOS Release build at launch. Why Debug and Android survive, the two ways to fix it, how to think about leaving the scheme out on purpose, and a pre-submission check.