Skip to main content

Barklog now has widgets!

How I added native iOS widgets to my Expo app

•

5 min read


I’ve recently added Home Screen widgets to Barklog, my gaming backlog tracking app (I wrote about shipping it earlier this year). And my favorite part came as a surprise: I can now see my gaming backlog right on my Mac desktop, even though Barklog is an iPhone app!

All thanks to Apple’s Continuity feature.

But getting there was less straightforward than I expected. With expo-widgets, a widget looks just like a React component, yet it plays by very different rules. So I wanted to share what those rules are and how I worked with them.

What I wanted to build

The idea was simple: let users see their gaming backlog without opening the app.

Barklog organizes games into four categories: Playing, Waiting, Completed, and Abandoned.

And “Waiting” isn’t necessarily about unreleased games. It’s a list of games you want to play someday, whether they came out yesterday or ten years ago.

I decided to support two widget sizes:

You can configure which category each widget displays, and tapping a game opens its details in Barklog on your iPhone.

Enter expo-widgets

Historically, implementing iOS widgets in a React Native app required writing native SwiftUI code, configuring a separate WidgetKit extension, and establishing communication between the app and the widget.

But expo-widgets handles much of that for you.

With Expo SDK 58, which I’m currently using, I can define widgets using TypeScript and React with components from @expo/ui/swift-ui. Thankfully, I already used these components throughout the whole app! Expo provides a createWidget() function, which registers the widget, and a special "widget" directive inside the component.

And here’s where it gets interesting.

Even though the widget looks like a React component, it doesn’t run in your normal React Native environment. It runs in an isolated JavaScript runtime inside the widget extension. You can’t use React hooks, access your app’s context, or perform asynchronous operations there! The component has to render synchronously from the data it receives. So my widget can’t simply call the useBacklog() hook that powers the rest of the app and display the results.

So how does it get the data?

Sharing data between the app and widget

In Barklog, the main application already fetches the user’s backlog. Rather than introducing a separate data source for widgets, I reuse that existing query.

Whenever the app receives updated backlog data, it builds a lightweight snapshot containing the information each widget needs: game titles, ratings, covers, deep links, and section counts. Then I publish it using BacklogWidget.updateSnapshot().

The flow is really simple:

API → App’s backlog query → Widget snapshot → WidgetKit

The widget only renders the snapshot. It doesn’t know anything about my API, authentication, or database.

But that simplicity has a price: these widgets don’t fetch fresh data on their own (see Apple’s guide on keeping a widget up to date). Their contents change only when the app sees new backlog data and publishes another snapshot.

The tricky part: game covers

Text was easy. Images were not. My game covers come from IGDB, but the widget can’t fetch them directly, and it can’t access arbitrary files inside the main application’s sandbox either. So if the widget can’t get the covers itself, the app has to hand them over.

The way to do that is an App Group, which provides a shared container accessible to both the app and its widget extension. The main app downloads the covers into this shared directory, which expo-widgets exposes as widgetsDirectory. The snapshot then contains local paths to those images.

I also made sure to download only the covers that the widgets actually need and clean up unused files after publishing a new snapshot. If a cover isn’t available, the widget displays a placeholder instead.

Another small gotcha: the snapshot needs to be compatible with native storage, so null is off the table. Instead, I use empty strings for missing image paths and zero for missing ratings.

Making it feel native

Besides getting the data to show up, I wanted the widgets to feel like a natural part of iOS. They support light and dark appearances, as well as the tinted widget rendering mode.

There’s also a small detail in the layout I quite like: ratings have lower layout priority than titles, so they can give up space when the widget gets too crowded. And because all the UI is built with Expo’s SwiftUI components, the result is rendered natively.

And yes, they work on my Mac!

This was the most satisfying discovery for me. macOS can display widgets from apps installed on your iPhone through Continuity. You don’t need a separate macOS version of the application.

So I added the Barklog widgets to my Mac desktop, and there they were: my actual gaming backlog, sitting alongside my other desktop widgets. It’s a small feature, but seeing something I’ve built become part of my everyday setup is genuinely exciting. ☺️

Final thoughts

The biggest lesson for me: a widget in expo-widgets looks like just another React component, but it’s really a tiny separate app that only knows what your main app tells it. Once I started thinking about it that way, the snapshot, the shared covers, and the “no null” rule all made sense.

expo-widgets makes native widgets much more approachable for React Native developers. But it still helps to understand the underlying widget architecture: the separate runtime, snapshot-based updates, shared file storage, and native rendering constraints.

I also learned that testing the data transformations and synchronization logic is quite different from testing the actual widget UI. The latter still needs verification on a real device.

If you’re interested in the implementation, Barklog is fully open-source. You can explore the widget source code on GitHub.

And if you’re starting with widgets yourself, I recommend checking out the official Expo Widgets documentation.

Happy building! 🎮

Want to receive updates straight in your inbox?

Subscribe to the newsletter

Comments