Enhance your React Native App with Vega native APIs

Going Native on Vega: Custom UI Components and Background Tasks

Most of your React Native for Vega business logic and UI lives in JavaScript, or comes from community libraries. Sometimes though, your app may need to interact with native platform APIs that aren’t provided by React Native or an existing library. For example, you might want a custom view that draws, animates, and handles focus in native code. Or maybe you want work that runs on a schedule while your app is closed. The Vega SDK now lets you do both in C++ and use the result from your React Native code like any other component or module.

To show you how, we’ve published two sample apps:

  1. Native UI Component sample: a simple view pager whose image card and arrow buttons are native C++ views built with the Vega UI toolkit and rendered inside your React tree.
  2. Periodic Task sample: a C++ Turbo Module that wraps the Vega Task Manager, so your app can run headless JavaScript on a repeating schedule even when it isn’t open.

Both samples use React Native 0.83 and run on a Fire TV Stick and on the Vega Virtual Device. Let’s dig in. In order to get started make sure you are on Vega SDK 2.0.x or greater and using React Native 0.83.

:light_bulb: New to Vega? Start with Getting Started with Vega and come back here once you have a Hello World app running.


Sample 1: Vega Native UI Component Sample

This sample is the Vega counterpart of native components in React Native. Most of the screen is ordinary React Native. Two building blocks, <ImageCard> and <ButtonContainer>, are C++ views.

React Native decides what to show and reacts to events. Native code does the drawing, focus handling, image loading and animation.

The sample covers a lot of ground:

  • Props from JS to native, including strings, integers and arrays
  • Events from native to JS. The arrow button reports a remote-control press back to a React callback.
  • React children inside a native view. The native button draws its own border and fill, then mounts a React-rendered chevron inside it.
  • Native focus handling with the D-pad, including native focus visuals
  • Native animation. The card runs a slide-and-fade transition with the UI toolkit animator.
  • Native image loading and prefetching with the UI toolkit image loader and its cache
  • Accessibility. Both views publish labels, roles and live regions, so VoiceView announces them.
  • A “fully drawn” launch marker, so you can measure time to fully drawn with the Vega KPI tool

How a native component is wired up

There are four pieces.

1. A view manager in C++. Each component has a view manager that implements IViewManager plus the delegates it needs: IPropsDelegate for props, ILayoutDelegate for layout, IMountDelegate for React children and ICommandDispatchDelegate for commands from JS. A factory returns the right one for each component name:

Ptr<IViewManager> ViewManagerFactory::makeViewManager(StringView name) {
    auto const viewModule = apmf::GetProcessObject()
                                ->getComponent("/com.amazon.kepler.uitoolkit.view")
                                .TryQueryInterface<IViewModule>();
    // ... graphicsModule from /com.amazon.kepler.uitoolkit.graphics

    if (name == "ImageCard") {
        return Ptr<ImageCardViewManager>::Make(viewModule, graphicsModule);
    }
    if (name == "ButtonContainer") {
        return Ptr<ButtonContainerViewManager>::Make(viewModule, graphicsModule);
    }
    // ...
}

2. Props, declared on both sides. The native side registers each prop and its default, then reads updates as they arrive:

propsRegistryBuilder->stringPropWithDefault("imageUrl", imageSource_);
propsRegistryBuilder->arrayProp("prefetchUrls");
propsRegistryBuilder->intPropWithDefault("slideDirection", slideDirection_);

On the JS side, a *Spec.ts file registers the same component name, its props (validAttributes) and its events with React Native.

3. Events from native to JS. When the user presses OK or Select on the remote, the button emits an event:

if (keyStateEvent->state() == KeyState::UP && isActivationKey) {
    self->context_->getEventEmitter()->emit(view, "tapped");
}

The JS wrapper maps that event to an onTapped callback.

4. Autolinking. react-native.config.js tells the build which native components your package provides:

autolink: {
  SampleNativeUIComponents: {
    uiComponentName: '/com.amazondeveloper.nativecomponentexample',
    linkDynamic: true,
    provider: 'application',
    components: ['ImageCard', 'ButtonContainer'],
    turbomodules: [],
  },
},

After that, your native views are just TSX/JSX:

<Button displayName="Previous" isNext={false} onTapped={handleTapped} />
<ImageCard
  imageUrl={selectedImage.url}
  description={selectedImage.description}
  slideDirection={slideDirection}
  a11yLabel={cardLabel}
  prefetchUrls={prefetchUrls}
  onImageLoaded={reportContentLoaded}
  {...cardTransition}
/>
<Button displayName="Next" isNext={true} autoFocus={true} onTapped={handleTapped} />

Good to know

  • Declare every prop in both places. If a prop isn’t in the JS view config and in registerProps, it never reaches your native code.
  • Tune native behavior with Fast Refresh. The animation duration, distance and easing are props set from theme.ts. You can adjust the native transition without rebuilding. Changes to C++ still need a rebuild and a reinstall.
  • Native views are invisible to screen readers by default. Install an accessibility delegate with IAccessibilityService::setDelegate, set the label and role, and call invalidate() when the label changes. Use one delegate per view, not one per view manager.

To add your own component, the sample README walks through it step by step using a Badge component as the example.


Sample 2: Vega Periodic Background Tasks Sample

Some work shouldn’t wait for the user to open your app, for example refreshing cached content, syncing settings, or prefetching data for the next launch. The Vega Task Manager can launch a headless JavaScript task on a schedule, and the schedule stays in place after your app exits.

Task Manager doesn’t have a general-purpose JavaScript API yet, so this sample includes a small C++ Turbo Module, PeriodicTasks, that exposes it to JS. You can copy it into your own app.

What you’ll see

  1. Open the app and press Schedule (60s). The task runs once right away, then every 60 seconds.
  2. Close the app.
  3. Come back a few minutes later. The app shows every run that happened while it was closed:
On launch: getState('periodicDemo') -> PENDING, 4 runs, latest 2026-09-30T16:02:09.523Z, ...

How it fits together

There are three pieces.

1. A Turbo Module that wraps Task Manager (com.amazon.kepler.tasks):

auto request = module->makeTaskRequestBuilder()
                   ->requireTime(timeProperties)  // repeat interval
                   ->repeating()
                   ->persistAfterAppDeath()       // keep the schedule after the app exits
                   ->name(name)                   // unique per app, prevents duplicates
                   ->build(componentId);          // a [[components.task]] from manifest.toml
auto registration = manager->submit(request);

2. A headless task component. You declare it in manifest.toml and give it its own process group, because a process group can’t mix runtime modules:

[[components.task]]
id = "com.amazondeveloper.periodictasksample.periodicTask"
runtime-module = "/com.amazon.kepler.runtime.react_native_kepler_headless_4@IReactNativeKeplerHeadless_0"

You register its entry point in task.js:

HeadlessEntryPointRegistry.registerHeadlessEntryPoint2(
  `${TASK_COMPONENT_ID}::doTask`,
  () => doPeriodicTask,
);

3. Shared state. The task runs in its own process, so it can’t share memory with your UI. It records each run through the Turbo Module to a file in the app’s /data directory, and the UI reads that file on launch.

Scheduling from your app takes a single call:

import {PeriodicTasks} from '@amazon-devices/periodic-task-scheduler';

await PeriodicTasks.scheduleRepeating({
  componentId: 'com.amazondeveloper.periodictasksample.periodicTask',
  name: 'periodicDemo',
  intervalSeconds: 60,
});
PeriodicTasks.getState('periodicDemo'); // 'PENDING' | 'RUNNING' | ... | 'NOT_FOUND'
PeriodicTasks.cancel('periodicDemo');

Good to know

  • Use the longest interval you can. The minimum is 60 seconds, and background work costs device resources. Run times aren’t exact, because Task Manager may delay a run when the system is busy.
  • Installing a new build clears the schedule. Schedule your task again when the app starts.
  • Catch every error inside doTask. If doTask rejects, the task never completes and stays RUNNING.
  • The headless runtime is limited. Don’t import UI components or UI APIs from react-native, and note that AsyncStorage isn’t available there. Your own Turbo Modules do load in the headless runtime, which is how the sample shares state.
  • Each run gets up to 5 minutes.

Picking the right vpkg

Both samples build one vpkg per architecture. Installing the wrong one is the most common issue developers hit, because vega run-app still reports success.

Target vpkg
Fire TV Stick armv7
Vega Virtual Device Mac aarch64
Vega Virtual Device on x86_64 Linux x86_64

Try it yourself

Each sample is a self-contained project:

npm install
npm run build
vega run-app build/<arch>-release/<app>_<arch>.vpkg

Each README covers the project layout, the full API, device logging and troubleshooting.

We’d love to see what you build with native code on Vega. If there’s a platform capability you’d like to see in a sample, let us know in the feature requests category.

Relevant Links