Reading a pre-populated sqlite database using react native with expo..
I recently needed to read an existing sqlite database, and return its contents to the react native front-end on an android emulator. I read several related posts on the subject but they all give examples of console logging, never returning. I faced quite some difficulties, and hence decided to create this post as a blog. Hope it helps someone who comes here looking for a solution.
Usually, there are two main issues that you would face. One, you would notice that simply opening a database and fetching won't work. You need to copy the local database to an internal document store and then use that database. Second, you would notice that there is no way of 'returning' the value of a variable in the traditional sense of the term. You must handle it within the async function itself.
So follow me for the solution.
Step 1.
Create the root folder. From the terminal in the root folder, create the react native app with expo using :
expo init newApp
Choose the blank template.
This will create the folder newApp in your root folder with starter files for the expo app.
Step 2.
Run the project with yarn android to ensure the basic app is working.
Step 3.
Create a folder in newApp called src. We will keep all our required components in this folder.
Step 4.
npx expo install expo-asset expo-file-system expo-sqlite @expo/metro-config
Install these libraries since we are going to need them.
Step 5.
In the newApp folder, create a file called metro.config.js and place the following contents in it.
The above configuration basically tells the react native engine to include files with '.db' extension in its assets.
I have a pre-populated sqlite3 database called movie.db which I have created using sqliteStudio.exe and have placed it in the src folder. This file contains a table tmovies, and is populated with movie names and hints. You can download sqliteStudio here.
I have about a 1000 records in the table. I will attempt to retrieve one record with a randomly generated rowid.
Step 7.
Inside newApp, create an src folder. In this folder we will create all our components and also store our database, lets call it 'movie.db'
Step 8.
Lets start b making changes to the App.js. Remove the status bar component. Replace text with contents as below. We will include the DemoComponent from the src folder. We will develop this in the next step. I have posted the final App.js file contents below:
______________________________________________________________________________
import { StyleSheet, Text, View } from 'react-native';
_______________________________________________________________________________
Step 9.
Create the DemoComponent.js file in the src folder. The contents should be as below:
_______________________________________________________________________________
What it does:
The DemoComponent function will return a view with a button called 'Fetch Movie' and the name of the fetched movie in the next line. It will fetch a random movie from the database on an initial render and also subsequently every time the 'Fetch Movie' button is pressed.
How it works:
We will define a state variable called movieName to hold the movie title. We expect fetchMovie function to populate this state variable.
Since we want to display a random movie name from the database at initial render, we will use the useEffect hook, in which we will call the async function fetchMovie and confirm that it has populated the useState hook variable movieName.
What it does:
2. things:
1. Ensure the database connection object is available. There are 3 steps to this.
2. Copies the original source database (our database) to this internal location,
3. Returns an object that refers to the new file.
2. Based on the select clause and the randomly generated number between 1 and 1000, it fetches a record from the new database and returns it to the calling function.
How it works:
Part 1 - 1. First we need to ensure that the db connection is ready.
You will take quite a bit of time to realise that simply using SQLite.openDatabase('..location to source file') does not work. Why? Because SQLite.openDatabase tries to find the file in its internal location and creates a new database if it does not find one. So you would spend hours trying to figure out why it throws an error saying table doesn't exist, when your tables are very much alive and kicking. Now, this internal location is defined by FileSystem.documentDirectory.
Once we understand that it requires your pre-populated database to be available in the internal location, we write a custom openDatabase code that will ensure so, and return a connection.
We now must make our original source database to be available in SQLite folder, inside of the FileSystem.documentDirectory. But since there is no provision of moving the physical db file, we must create a copy of our database in the internal location.
We want to wait until the system checks if the folder SQLite exists in {FileSystem.documentDirectory}. If it doesn't exist, further wait until the system creates it. We do this using the FileSystem.getInfoAsync and FileSystem.makeDirectory Async methods.
Part 1 - 2 - copy source database to internal location.
Now that the folder is ready, we further wait until the system copies the original database (our source database 'movies.db'). We do this using FileSystem.downloadAsync function, which requires two parameters - first, the location of the original source file, which it will pickup using its Asset module, and second, the location (and filename) to which the file must be copied.
Note that 'require' will not work with variables and requires a hardcoded relative path to the file. In our case, since we have placed the database movies.db in the same src/ folder, we can simply refer to it by './movies.db'. If you place it elsewhere, you would need to give relative path to that location and filename.
Now, note that you can await only within an async function. Hence the custom function openDatabase must be defined as an async function.
Part 1- 3. Return the db connection to the calling function.
Now we use SQLite.openDatabase function to return a db connection to the internally located file with the same (or different) name.
Part 2 - Getting the movie name.
const pk = Math.floor(Math.random() * 100) + 1;
const selectSql = "SELECT movie_title FROM tmovies where rowid = ?";
The Math.random() static method returns a floating-point, pseudo-random number that's greater than or equal to 0 and less than 1, with approximately uniform distribution over that range — which you can then scale to your desired range.Thus, pk gives me a random number between 1 and 1000, which I will provide as rowid to the select clause. selectSql is the select clause.
Now, this is how you actually return data from an async sql transaction function to the calling async function.
The function is defined using the async
keyword, indicating that it's an asynchronous function that can use await
to pause execution and wait for promises to resolve.
- The return statement is used to return the result of the asynchronous operation. In this case, it returns the result of the GetMovie() function.
- The await keyword is used to pause execution and wait for the GetMovie() function to complete and resolve to a value. The await keyword can only be used inside an async function.
- After the await statement, a then method is chained to the GetMovie() promise. The then method takes a callback function (movie => setMovieName(movie)) that will be executed when the GetMovie() promise is resolved successfully.
- The callback function (movie => setMovieName(movie)) takes the resolved value of the GetMovie() promise (referred to as movie) and sets the state variable movieName. Here is a key note. You cannot send the value of movie back to a calling non-sync function, Hence we are using the .then chaining feature to do whatever procedure we want to do once the promise is successfully resolved.
- If an error occurs during the promise resolution, the .catch method is chained to handle the error. The catch method takes a callback function (error => console.log('error', error)) that will be executed when the promise is rejected or an error is thrown. It logs the error message to the console. Note that this catch handler only logs the error and does not propagate the error further.
In summary, the fetchMovie function logs a debug message, awaits the completion of the GetMovie() promise, sets the movie name using the resolved value, and logs any errors that occur during the promise resolution.
async await
syntax suggests that the function waits but in fact it doesn't. It just always returns a promise. It is our job to setup the value when it arrives to a usable form for the App to use.Hope this post was useful. Feel welcome to post any corrections, more explanations or views iin your comments below.
P.S The source code is available in my github repo
Comments
Post a Comment