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:
- Small: Displays one game with its cover, title, and rating.
- Medium: Displays up to four games side by side, along with the total number of games in that category.
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! 🎮