Article is about bridging my demo sqlite app to work in web, and extends the concepts and implementation detailed in the sqlite in expo article.
To adapt my SQLite demo app for the web, I explored the following approaches:
- Avoid SQLite in the browser
Use alternative storage options like LocalStorage or IndexedDB. - Use SQLite in the browser
Yes, SQLite can work in the browser using tools like SQL.js. - Support All Options
Why not make the app flexible enough to support multiple storage backends? That’s what I aimed to achieve.
I don't want to bore you with all the refactoring I made. I’ll highlight the key parts here. If you’re interested in the complete walkthrough, check out my YouTube video.
Why This Article?
A YouTube user asked me if I could extend my demo and include redux in it. Sure I can, I love Redux and use it in most of my apps. Then I wanted to make an article just covering redux and all the tools that can help you in react native development. Then I wanted to showcase how redux devtools is great. But, I'd prefer to run the web version of the app. Basically, I fell into the rabbit hole.
Making the App Work on the Web
Get started with react native web by running in the root of your project:
npx expo install react-dom react-native-web @expo/metro-runtime
add a command to package.json scripts
"scripts": {
"web": "expo start --web"
}
And run npm run web.
In my demo case, it won't work. It uses native modules that aren't ported for browser usage.
What I get is a white screen:
What I usually see as a fix for this problem is making a platform specific module with adapter for the web.
I don't like platform-specific modules. I've found myself a couple of times fixing modules that don't actually need a fix, only to be left wondering why things aren't working as expected. That said, sometimes they are unavoidable.
Using LocalStorage
LocalStorage persists data indefinitely in the browser until it is explicitly cleared by the user. As such, it is a valid solution for an offline app in the browser.
There's a but. It is synchronous, which can block the main thread for large data operations. It also has a limited storage capacity, which is around 5MB depending on the browser.
In React Native, you can use AsyncStorage, which uses localstorage under the hood when running in the browser. But, it is platform independent and localstorage ops are wrapped in a promise. It still has limited storage capacity.
Using IndexedDB
This was the first time I've used indexedDB and I noticed it's designed to be asynchronous. IndexedDB also has a significantly larger storage limit. It is a part of the Quota Management API, which means available space is allocated based on available disk space. I used idb for implementing indexedDB based storage for the browser. Btw, indexdb is supported in all major browsers except Opera mini.
Using SQLite in the browser
This was also my first time using SQLite in the browser and I chose SQL.js. SQL.js uses WebAssembly to bring SQLite functionality to the browser.
It uses a virtual database file stored in memory, and thus doesn't persist the changes made to the database.
And it has blocking operations.
Tying it all together
To support all three storage options dynamically, I introduced abstractions to avoid direct platform-specific dependencies. My app determines the storage backend at runtime in the _layout
:
const [persistenceType, setPersistenceType] = React.useState<PersistenceType>(
Platform.select({ web: PersistenceType.indexedDB, default: PersistenceType.sqlite }));
//...
<AppDataProvider persistenceType={persistenceType}>
// other view components in here
</AppDataProvider>
AppDataProvider
The AppDataProvider
selects the appropriate data provider component based on the persistenceType
:
const PersistenceProviderWrapper: React.FC<DataProviderProps & { persistenceType: PersistenceType }> = ({
persistenceType,
...props
}) => {
const Component: React.FC<DataProviderProps> = {
[PersistenceType.sqlite]: SQLiteDataProvider,
[PersistenceType.indexedDB]: IndexedDBDataProvider,
[PersistenceType.localstorage]: LocalStorageDataProvider,
}[persistenceType];
return <Component {...props} />;
};
const AppDataProvider: React.FC<{
children: React.ReactNode;
persistenceType: PersistenceType;
}> = ({ children, persistenceType }) => {
return (
<PersistenceProviderWrapper persistenceType={persistenceType}>
{(props) => {
return <DataContext.Provider value={{ tasksClient: props.taskClient }}>{children}</DataContext.Provider>;
}}
</PersistenceProviderWrapper>
);
};
export default AppDataProvider;
This structure allowed me to modularize the storage logic and reuse it across different storage backends.
SQLite Adaptation for the Web
For SQLite in the browser, I used SQL.js and implemented the same SQLiteDatabase
interface as expo-sqlite
. I first had to extract all calls to the expo-sqlite
to a new module, and re-export the imports.
And for the web implementation I did the same. I created a web module, and made a different implementation that follows the same SQLiteDatabase interface.
All of the code is available at GitHub. This demo is a just a base for you to get an idea what you can do with your expo app.