NativeScript Blog

Syncing data with SolidJS and iOS widgets

Nathan Walker February 16, 2026

With SolidJS + NativeScript, we can keep the UI and state orchestration in TypeScript while still using native iOS capabilities (WidgetKit, AppIntents, Swift Charts, Darwin notifications).

This implementation focuses on a TypeScript-first architecture where the SolidJS app and iOS widget stay in sync.

It was scaffolded using NativeScript's widget generator:

ns widget ios

The Solid app base was created with the following, then choosing TypeScript at the prompts:

ns create myapp --solid

Docs: https://docs.nativescript.org/guide/widgets-ios


With SolidJS + NativeScript, we can keep the app UI and state orchestration in TypeScript while still using native iOS capabilities (WidgetKit, AppIntents, Swift Charts, Darwin notifications).

This gives us:

  • Reactive chart updates in app (createSignal)
  • Shared App Group persistence for app/widget sync
  • Fast cross-process iOS eventing when host app is active
  • A heartbeat-based fallback when host app is not active

Current architecture (with heartbeat fallback)

flowchart LR A[home.tsx SolidJS screen] -->|build payload + write| B[App Group UserDefaults] A -->|reload timelines| C[WidgetKit extension] C -->|read payload| B C -->|if heartbeat fresh| D[Darwin notify widgetRandomizeData] D --> E[AppleWidgetUtils observer] E -->|NSNotification| A C -->|if heartbeat stale| F[mark widgetPendingAction + open app] F --> A A -->|resumeEvent consumes pending action| B

TSX integration

The app entry point for this feature lives in src/components/home.tsx in the sample project, referenced at the bottom of this post.

What home.tsx does

  1. Hydrates initial chart values from shared widget data
  2. Maintains chart UI state as a Solid signal (chartData)
  3. Applies randomization and persists shared payloads
  4. Observes widget-triggered notifications (widgetRandomizeData)
  5. Wires app lifecycle heartbeat (resumeEvent / exitEvent)
  6. On resume, consumes pending widget action if app was launched by widget fallback path

Code walkthrough

Below are the three integration moments that matter most.

1) Initialize from shared storage and keep chart state reactive

const sharedData = readSharedWidgetData();
const initialValues = sharedData?.values?.length ? sharedData.values : defaultValues;

const [chartData, setChartData] = createSignal<LineChartData>(buildChartData(initialValues));

const applyValues = (values: ChartPoint[]) => {
   setChartData(buildChartData(values));
   setTimeout(() => {
      chartRef?.animate(updateAnimation);
   }, 0);
};

This is the app-side "source of render truth." Storage hydration happens once, then Solid signal updates drive chart UI.

2) React to widget-originated Darwin events

observeWidgetNotifications();

observer = Application.ios.addNotificationObserver(WIDGET_RANDOMIZE_DATA, () => {
   const payload = readSharedWidgetData();
   if (payload?.values?.length) {
      randomizeData(payload.values, false);
      return;
   }
   randomizeData();
});

When the host app is active, widget taps arrive via notification and the chart updates immediately without app relaunch.

3) Wire lifecycle heartbeat + pending action replay

disposeLifecycle = wireHostLifecycleHeartbeat(() => {
   const pendingAction = consumePendingWidgetAction();
   if (pendingAction === WIDGET_PENDING_ACTION_RANDOMIZE) {
      const payload = readSharedWidgetData();
      if (payload?.values?.length) {
         randomizeData(payload.values, false);
         return;
      }
      randomizeData();
      return;
   }

   syncChartFromSharedData();
});

This is the fallback path. If the widget had to launch the app (because heartbeat was stale), the app resumes, consumes the pending action, and reconciles state.


TypeScript architecture (src/widget/*)

The implementation is split into clear boundaries:

types.ts

Defines shared app-side contracts:

  • ChartPoint
  • SharedWidgetChartData

constants.ts

Single source for keys and chart settings:

  • Storage/event keys (widgetChartData, widgetRandomizeData, widgetHostHeartbeat, widgetPendingAction)
  • Pending action values (randomizeData)
  • Heartbeat window (45_000 ms)
  • Chart config + animation defaults

manager.ts

Native facade over AppleWidgetUtils:

  • Observe widget notifications
  • Read/write/remove shared strings
  • Trigger widget timeline reload
  • Consume pending widget action atomically (read + clear)

This keeps native interop in one place.

data.ts

Data modeling helpers:

  • Build LineChartData for the in-app chart
  • Build shared payload with delta / percent
  • Read/validate shared payload from App Group
  • Persist payload and reload widget
  • Generate randomized values

lifecycle.ts

Host activity signaling:

  • Write heartbeat timestamp on resume
  • Clear heartbeat on exit
  • Evaluate heartbeat recency
  • Provide wireHostLifecycleHeartbeat(onResume) setup/teardown helper

This is the key piece that allows the widget to choose the best interaction path.


Widget-side behavior (Swift)

The widget (WidgetHomeScreenWidget.swift) now has two intent paths for the same user action:

  1. RandomizeDataIntent (host heartbeat is recent)

    • Randomize + save shared data
    • Post Darwin notification (widgetRandomizeData)
    • Reload widget timelines
  2. RandomizeDataAndOpenAppIntent (host heartbeat is stale)

    • Randomize + save shared data
    • Mark widgetPendingAction = randomizeData
    • openAppWhenRun = true to launch host app
    • App resumes and handles pending action in home.tsx

This avoids losing interactions when the host app is not running.


Shared payload contract

Both sides share the same shape for chart + summary metadata:

interface SharedWidgetChartData {
  title: string;
  values: { x: number; y: number }[];
  updatedAt: number;
  delta: number;
  percent: number;
}

Keeping this contract stable is what makes app/widget synchronization reliable.


Practical outcomes

  • home.tsx stays focused on orchestration and UI state
  • Native bridge logic is centralized in TypeScript modules
  • Widget interaction is resilient whether app is active or not
  • Shared storage remains the single source of truth

Sample project

You can explore the full implementation in this sample repo.

Join the conversation

Share your feedback or ask follow-up questions below.