2

I have a method in a class which returns JSON object and then outside the class in a function I create an instance of this class, call one of the methods and expect to read returned JSON values. But what happens is that the variable assigned to the method is called at the very start before method even collects the data resulting in undefined. I tried short circuit (&&) which worked for me before, but not this time. I also tried using hooks instead of let and returning that, which ultimately worked for me but for some reason, it was looping as if I put it in while(1). All the methods were taken from an SQLite tutorial and modified.

function Screen() {
    const a = new App();
    let prodReturned = null;
    prodReturned = a.getProducts();
    return (
        <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
            <Text>text: {prodReturned && JSON.stringify(prodReturned)}</Text>
        </View>
    );
}
export default class App extends Component {

getProducts() {
        let products=[];
        this.listProduct().then((data) => {
            products = data;
            console.log(typeof(data),data,'this returns object type and correct json form')
            return products;
        }).catch((err) => {
            console.log(err);   
        })
}

listProduct() {
        return new Promise((resolve) => {
            const favdrinks = [];
            this.initDB().then((db) => {
                db.transaction((tx) => {
                    tx.executeSql('SELECT p.favorites FROM drinks p', []).then(([tx, results]) => {
                        console.log("Query completed");
                        var len = results.rows.length;
                        for (let i = 0; i < len; i++) {
                            let row = results.rows.item(i);
                            console.log(`Drinks favorited: ${row.favorites}`)
                            const { favorites } = row;
                            favdrinks.push({
                                favorites
                            });
                        }
                        console.log(favdrinks);
                        resolve(favdrinks);
                    });
                }).then((result) => {
                    this.closeDatabase(db);
                }).catch((err) => {
                    console.log(err);
                });
            }).catch((err) => {
                console.log(err);
            });
        });
}

}

How do I make sure to call the method and assign the return value to a variable once it is prepared to do so?

Pedro Gonzalez
  • 95
  • 2
  • 11

3 Answers3

3

React components must render synchronously. This is how React was designed; it cannot and will never change. Any way you hear of to render asynchronously is an abstraction that caches old render results and returns them synchronously. For this reason, you'll never be able to do what your code indicates.

However, I suspect that you don't actually care if the function is called before the data loads (which is what your code suggests), but rather that you want it called and to display a loading menu until the asynchronous operation completes and gives you its result. This is a common problem in React and is solved by having an initial empty state that you later set.

import React, { useState } from 'react';
function MyComponent() {
    // Since you're using TypeScript, you can type this by
    // setting the generic parameter
    // e.g. useState<Product[] | null>(null);
    const [products, setProducts] = useState(null);
    listProducts().then(function(loadedProducts) {
        setProducts(loadedProducts);
    });
    return (
        <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
            <Text>text: {products !== null && JSON.stringify(products)}</Text>
        </View>
    );
}

To make this more ES6:

import React, { useState } from 'react';
const MyComponent = () => {
    const [products, setProducts] = useState(null);
    listProducts().then(setProducts);
    return (
        <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
            <Text>text: {products && JSON.stringify(products)}</Text>
        </View>
    );
}

There's just one problem with this: every time MyComponent is called, the data is loaded. You may want this in some circumstances, but for the vast majority of the time, you only want to reload the products when something changes. This is where the useEffect hook comes in. It accepts a function that will only be called if the dependency list has changed.

useEffect(() => {
    // This is called once on the first render and only 
    // called again whenever either dep1 or dep2 changes

    // Note that a change in dep1 or dep2 does not necessarily
    // mean your component will be called again (i.e. be
    // rerendered). That only happens if dep1 or dep2 came
    // from a useState hook, because to change a value from
    // a useState hook, you must call the setter, which tells
    // React to rerender.
    console.log(dep1 + dep2);
}, [dep1, dep2]);

If you add the current minute to the dependency list, the product list will update at most every minute.

useEffect(() => {
  listProducts().then(setProducts);
}, [
    // Time in milliseconds rounded down to the minute
    Math.floor(Date.now() / (60 * 1000))
]);

If you only want to call the function once, make the dependency list empty. If you want to call it after every render, don't specify a dependency list at all (i.e. useEffect(callback)). Read more here.

A few other things: your App class is pointless. You may be used to object-oriented programming from languages like Java and C#, but modern JavaScript avoids classes as much as is reasonable. Moreover, you don't need to extend React.Component because you aren't ever rendering the class with React. I'd recommend moving the functions out of the classes. In addition, you seem unsure as to how Promises work. They are called asynchronously, the callback is called way after the enclosing function finishes unless you use async/await. I'll refactor this for you, but you really shouldn't be taking on something this difficult without the basics. Try this Promise guide for a start, then learn how async/await make it easy to avoid infinite .thens.

const getProducts = async () => {
    const data = await listProducts();
    // typeof isn't a function
    console.log(typeof data, data);
}

const listProducts = async () => {
    // Create the initDB() function the way I did this
    const db = await initDB();
    const favdrinks = await new Promise((resolve, reject) => {
        db.transaction(async tx => {
            const [tx, results] = await tx.executeSql(
                'SELECT p.favorites FROM drinks p',
                []
            );
            const favdrinks = [];
            console.log("Query completed");
            var len = results.rows.length;
            for (let i = 0; i < len; i++) {
                let row = results.rows.item(i);
                console.log(`Drinks favorited: ${row.favorites}`)
                const { favorites } = row;
                favdrinks.push({
                    favorites
                });
            }
            console.log(favdrinks);
            resolve(favdrinks);
        })
    });
    // Refactor this function as well
    await closeDatabase(db);
    return favdrinks;
}

Putting it all together:

import React, { useState, useEffect } from 'react';
const listProducts = async () => {
    const db = await initDB();
    return new Promise((resolve, reject) => {
        db.transaction(async tx => {
            const [tx, results] = await tx.executeSql(
                'SELECT p.favorites FROM drinks p',
                []
            );
            const favdrinks = [];
            var len = results.rows.length;
            for (let i = 0; i < len; i++) {
                let row = results.rows.item(i);
                const { favorites } = row;
                favdrinks.push({
                    favorites
                });
            }
            resolve(favdrinks);
        })
    });
    await closeDatabase(db);
    return favdrinks;
}

const MyComponent = () => {
    const [products, setProducts] = useState(null);
    useEffect(() => {
        listProducts().then(setProducts);
    }, []); // empty dependency list means never update
    return (
        <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
            <Text>text: {products && JSON.stringify(products)}</Text>
        </View>
    );
}

EDIT: Seeing your comment on another answer, you aren't able to remove the class logic. I think you may be misunderstanding the requirements of whatever framework you are using, but if you're seriously unable to avoid it, you should create the object of the class you're using inside the useEffect callback.

101arrowz
  • 1,825
  • 7
  • 15
  • Thanks for the detailed answer, I will look into it tommorow in more detail. After I posted the question I focused on async and reworked the getProducts function: async getProducts() { return await this.listProduct(); }. I call the method later and with then() save the result into variable. In console.log I could see the variable as an json object but rendering it in text produced nothing, not even error. Are there still steps to take? I will help myself with your take if it doesnt work. – Pedro Gonzalez Dec 26 '20 at 21:25
  • 1
    You may still be trying to render a Promise object, which will fail. Promise is just a wrapper for something that will exist in the future, and you get access to the future value from `.then`. To fix this, you do need to use `useEffect`; it is just about the only way to solve the problem. So you probably will need to take a look at this answer. – 101arrowz Dec 26 '20 at 21:26
  • I solved the rendering using your useEffect approach, but is it possible to do it without hooks? They are a good solution in this scenario but what if I could not use them? I am using the classes because I am using react-navigation which creates screen based on functions and since I am going to be using database in multiple screens, I somehow had to make it possible to call methods manipulating database inside every function and putting 200 rows into every function would be dumb. I am still studying at university so my aproaches and thinking can be wrong, but thats what I concluded. – Pedro Gonzalez Dec 27 '20 at 16:33
  • You can just export a function from a centralized utility file that lists what you need and access it from elsewhere. For example, in `src/util/db.ts` export the listProducts function and call it from elsewhere with `import { listProducts } from 'path/to/util/db.ts'`. You can create your own hook to wrap this functionality into a single function call if you'd like: https://reactjs.org/docs/hooks-custom.html. – 101arrowz Dec 28 '20 at 04:02
  • React Navigation also doesn't require the use of classes as far as I can tell. I've used it before without writing a single class. See the example docs: https://reactnavigation.org/docs/hello-react-navigation. Last thing, academia doesn't really teach software development but rather computer science, so you shouldn't doubt your decisions just because you're still in college. – 101arrowz Dec 28 '20 at 04:10
1

For react-native, you can use the react hooks as well to call a method that contains API calls. Try this below,

import React, {useEffect, useState} from 'react;
import {View, Text} from 'react-native';

const Screen=(props)=> {
       const [prodReturned, setprodReturned] = useState([]);
       useEffect(()=>{
          getProducts();
       },[])

const getProducts=()=> {
    let products=[];
    listProduct().then((data) => {
        setprodReturned(data);
        console.log(typeof(data),data,'this returns object type and correct json form')
    }).catch((err) => {
        console.log(err);   
    })
}


    return (
        <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
            <Text>text: {prodReturned && JSON.stringify(prodReturned)}</Text>
        </View>
     );
  }

  export default Screen;

  const listProduct=()=> {
    return new Promise((resolve) => {
        const favdrinks = [];
        this.initDB().then((db) => {
            db.transaction((tx) => {
                tx.executeSql('SELECT p.favorites FROM drinks p', []).then(([tx, results]) => {
                    console.log("Query completed");
                    var len = results.rows.length;
                    for (let i = 0; i < len; i++) {
                        let row = results.rows.item(i);
                        console.log(`Drinks favorited: ${row.favorites}`)
                        const { favorites } = row;
                        favdrinks.push({
                            favorites
                        });
                    }
                    console.log(favdrinks);
                    resolve(favdrinks);
                });
            }).then((result) => {
                this.closeDatabase(db);
            }).catch((err) => {
                console.log(err);
            });
        }).catch((err) => {
            console.log(err);
        });
    });
}

please comment feedback if this works/not. I will love to solve it if it doesn't work.

Tulip
  • 21
  • 3
  • Thanks, but I need to follow the structure that I laid out because of other libraries. I edited the code in my question. I want to know how to create an instance of class and return object by calling a method inside a function. – Pedro Gonzalez Dec 26 '20 at 19:30
0

In the end I made the method getProducts() async, made it only return Promise containing data, and retrieved it in useEffect(most important part proposed by @101arrowz).

async getProducts() {
    return await this.listProduct(); //listProduct() returns Promise
}

 const a = new App();
 const [products, setProducts] = useState(null);
    useEffect(() => {
        a.getProducts().then(setProducts);
    }, []);

return (
        <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
            <Text>Favorited drinks:{products && JSON.stringify(products)}
            </Text>
        </View>
);
Pedro Gonzalez
  • 95
  • 2
  • 11