Skip to content

Commit

Permalink
add web support - sqlite, indexeddb, asyncstorage
Browse files Browse the repository at this point in the history
  • Loading branch information
amarjanica committed Dec 2, 2024
1 parent 98c355d commit b9cfa1e
Show file tree
Hide file tree
Showing 30 changed files with 580 additions and 121 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
expo-env.d.ts
6 changes: 3 additions & 3 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module.exports = {
extends: ['expo', 'prettier', 'plugin:prettier/recommended'],
plugins: ['prettier'],
rules: {
"eslint-comments/no-unlimited-disable": 0,
"prettier/prettier": ["error"],
}
'eslint-comments/no-unlimited-disable': 0,
'prettier/prettier': ['error'],
},
};
29 changes: 24 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
# React Native SQLite Expo Demo

A demo application built with Expo and React Native, showcasing the integration of SQLite
using expo-sqlite within an expo-router project. This app serves as an example of:
- Database SQLite Migration
- Integration Testing: Run integration tests for expo-sqlite in a Node.js environment.
The app functions as a simple "CRUD" (Create, Read, Delete) tasks manager.
using expo-sqlite within an expo-router project.
Works natively and on the web.

# Chapters
## 1. Database migrations and integration testing
I made first version of the app to showcase how you can to database migrations
and configure integration tests to be run in a Node.js environment.
Read more about it in my blog post on [expo sqlite migrations and integration testing](https://www.amarjanica.com/bridging-the-gap-between-expo-sqlite-and-node-js/)
or [watch my YT tutorial](https://youtu.be/5OBi4JtlGfY).
[Codebase](https://github.com/amarjanica/react-native-sqlite-expo-demo/tree/98c355d5b1fa065a5ec6585273232908edfe50ec)

## 2. Web support with SQLite, AsyncStorage and IndexedDB
I've added web support to the app, so it can run on the web. You can dynamically switch between
different storage types: SQLite, AsyncStorage and IndexedDB. SQLite is supported on the web via
[sql.js](https://github.com/sql-js/sql.js/).

# App Screenshot
<p align="center">
<img src="preview.png" alt="App Screenshot example" height="300"/>
</p>

# Run it on Android
I've tested this demo only on android emulator.
I've tested this demo natively on android emulator.
```sh
npm i
# runs on expo go
Expand All @@ -24,5 +32,16 @@ npm run go:android
npm run dev:android
```

# Web App Screenshot
<p align="center">
<img src="preview-web.png" alt="Web app Screenshot example" height="300"/>
</p>

# Run it on the web
```sh
npm i
npm run web
```

# Tests
Tests don't need an emulator. They're just `jest` tests that you can run with `npm test` like any nodejs project.
35 changes: 19 additions & 16 deletions app.config.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,34 @@
/* eslint-env node */
import { ExpoConfig } from "@expo/config-types";
import { ExpoConfig } from '@expo/config-types';

const config: ExpoConfig = {
jsEngine: "hermes",
scheme: "demo",
name: "react-native-sqlite-expo-demo",
slug: "react-native-sqlite-expo-demo",
version: "1.0.0",
orientation: "portrait",
icon: "./assets/icon.png",
userInterfaceStyle: "light",
jsEngine: 'hermes',
scheme: 'demo',
name: 'react-native-sqlite-expo-demo',
slug: 'react-native-sqlite-expo-demo',
version: '1.0.0',
orientation: 'portrait',
icon: './assets/icon.png',
userInterfaceStyle: 'light',
splash: {
image: "./assets/splash.png",
resizeMode: "contain",
backgroundColor: "#ffffff",
image: './assets/splash.png',
resizeMode: 'contain',
backgroundColor: '#ffffff',
},
extra: {
dbName: 'test',
},
ios: {
supportsTablet: true,
},
android: {
package: "com.amarjanica.reactnativesqliteexpodemo",
package: 'com.amarjanica.reactnativesqliteexpodemo',
adaptiveIcon: {
foregroundImage: "./assets/adaptive-icon.png",
backgroundColor: "#ffffff",
foregroundImage: './assets/adaptive-icon.png',
backgroundColor: '#ffffff',
},
},
plugins: ["expo-router"],
plugins: ['expo-router'],
experiments: {
typedRoutes: true,
},
Expand Down
57 changes: 36 additions & 21 deletions app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,62 @@
import { Slot } from 'expo-router';
import { SafeAreaView } from 'react-native-safe-area-context';
import { KeyboardAvoidingView, Platform, StyleSheet } from 'react-native';
import { Button, KeyboardAvoidingView, Platform, StyleSheet, View, Text } from 'react-native';
import React from 'react';
import migrations from '../migrations';
import DbMigrationRunner from '@/DbMigrationRunner';
import logger from '@/logger';
import { SQLiteDatabase } from 'expo-sqlite';
import SQLiteProvider from '@/SQLiteProvider';
import AppDataProvider from '@/data/providers/AppDataProvider';
import { PersistenceType } from '@/data/types';

const Root = () => {
const [ready, setReady] = React.useState(false);
const migrateDbIfNeeded = async (db: SQLiteDatabase) => {
try {
await new DbMigrationRunner(db).apply(migrations);
logger.log('All migrations applied.');
setReady(true);
} catch (err) {
logger.error(err);
}
};
const [persistenceType, setPersistenceType] = React.useState<PersistenceType>(
Platform.select({ web: PersistenceType.indexedDB, default: PersistenceType.sqlite })
);

return (
<SafeAreaView style={styles.container}>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.keyboardView}>
<SQLiteProvider
databaseName="test.db"
onInit={migrateDbIfNeeded}>
{ready && <Slot />}
</SQLiteProvider>
<AppDataProvider persistenceType={persistenceType}>
<Text style={styles.title}>
Persistence type: {persistenceType}, OS: {Platform.OS}
</Text>
<View style={styles.buttons}>
<Button
title={PersistenceType.localstorage}
onPress={() => setPersistenceType(PersistenceType.localstorage)}></Button>
<Button
title={PersistenceType.indexedDB}
onPress={() => setPersistenceType(PersistenceType.indexedDB)}></Button>
<Button
title={PersistenceType.sqlite}
onPress={() => setPersistenceType(PersistenceType.sqlite)}></Button>
</View>
<Slot />
</AppDataProvider>
</KeyboardAvoidingView>
</SafeAreaView>
);
};

const styles = StyleSheet.create({
buttons: {
flexDirection: 'row',
justifyContent: 'space-around',
alignSelf: 'center',
width: '50%',
padding: 10,
},
container: {
flex: 1,
backgroundColor: '#f8f8f8',
},
keyboardView: {
flex: 1,
},
title: {
textAlign: 'center',
fontSize: 20,
padding: 10,
},
});

export default Root;
20 changes: 12 additions & 8 deletions app/detail/[id].tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,38 @@
import { router, useLocalSearchParams } from 'expo-router';
import TaskClient from '@/TaskClient';
import React, { useState } from 'react';
import { Task } from '@/types';
import { Unmatched } from 'expo-router';
import { Button, StyleSheet, Text, View } from 'react-native';
import logger from '@/logger';
import { useSQLiteContext } from '@/SQLiteProvider';
import { useDataContext } from '@/data/DataContext';

const Page = () => {
const { id } = useLocalSearchParams<{ id: string }>();
const ctx = useSQLiteContext();
const client = new TaskClient(ctx);
const decodedId = parseInt(id);
const { tasksClient: client } = useDataContext();
const [task, setTask] = useState<Task | null>(null);
const [ready, setReady] = useState(false);

const goBack = () => {
router.back();
if (router.canGoBack()) {
router.back();
} else {
router.push('/');
}
};

const handleDelete = async () => {
await client.delete(parseInt(id));
await client.delete(decodedId);
goBack();
};

React.useEffect(() => {
const prepare = async () => {
setTask(await client.task(id));
setTask(await client.task(decodedId));
};
logger.log('prepare detail');
prepare().finally(() => setReady(true));
}, [id]);
}, [client, decodedId]);

if (!ready) {
return false;
Expand Down
6 changes: 2 additions & 4 deletions app/index.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { FlatList, Text, TextInput, TouchableOpacity, View } from 'react-native';
import React, { useState } from 'react';
import { useSQLiteContext } from '@/SQLiteProvider';
import TaskClient from '@/TaskClient';
import { Task } from '@/types';
import logger from '@/logger';
import type { ListRenderItem } from '@react-native/virtualized-lists';
import { router } from 'expo-router';
import globalStyles from '@/globalStyles';
import { useDataContext } from '@/data/DataContext';

const LandingPage = () => {
const ctx = useSQLiteContext();
const client = new TaskClient(ctx);
const { tasksClient: client } = useDataContext();
const [tasks, setTasks] = useState<Task[]>([]);
const [newTask, setNewTask] = useState('');

Expand Down
2 changes: 1 addition & 1 deletion babel.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module.exports = function(api) {
module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
Expand Down
9 changes: 4 additions & 5 deletions migrations/001_initial.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import { SQLiteDatabase } from 'expo-sqlite';
import { SQLiteDatabase } from '@/data/sqliteDatabase';
import { DatabaseMigration } from '@/types';

const migration: DatabaseMigration = {
name: 'create initial tables',
async up(db: SQLiteDatabase): Promise<void> {
await db.execAsync(`
CREATE TABLE task (
id INTEGER PRIMARY KEY,
task TEXT NOT NULL,
id INTEGER PRIMARY KEY,
task TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
`);
);`);
},
};

Expand Down
2 changes: 1 addition & 1 deletion migrations/002_populate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SQLiteDatabase } from 'expo-sqlite';
import { SQLiteDatabase } from '@/data/sqliteDatabase';
import { DatabaseMigration } from '@/types';

const migration: DatabaseMigration = {
Expand Down
Loading

0 comments on commit b9cfa1e

Please sign in to comment.