typescript - Devolver un objeto json desde una promesa resuelta y asignarlo después de que se cargue
Tengo un método en una clase que devuelve un objeto JSON y luego, fuera de la clase en una función, creo una instancia de esta clase, llamo a uno de los métodos y espero leer los valores JSON devueltos. Pero lo que sucede es que la variable asignada al método se llama desde el principio antes de que el método siquiera recoja los datos, resultando en undefined. Intenté usar cortocircuito (&&) que me había funcionado antes, pero esta vez no. También intenté usar hooks en lugar de let y devolver eso, lo que finalmente funcionó para mí, pero por alguna razón, se estaba ejecutando en bucle como si lo hubiera puesto en while(1). Todos los métodos fueron tomados de un tutorial de SQLite y modificados.
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);
});
});
}
}
¿Cómo me aseguro de llamar al método y asignar el valor de retorno a una variable una vez que esté preparado para hacerlo?
Respuesta #1
Los componentes de React deben renderizarse de manera sincrónica. Así es como React fue diseñado; no puede y nunca cambiará. Cualquier forma de renderizar de manera asincrónica que escuches es una abstracción que almacena en caché los resultados de renderización antiguos y los devuelve de manera sincrónica. Por esta razón, nunca podrás hacer lo que tu código indica.
Sin embargo, sospecho que en realidad no te importa si la función se llama antes de que se carguen los datos (lo que sugiere tu código), sino que deseas que se llame y se muestre un menú de carga hasta que la operación asíncrona se complete y te dé su resultado. Este es un problema común en React y se resuelve teniendo un estado inicial vacío que luego estableces.
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>
);
}
Para hacer esto más 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>
);
}
Solo hay un problema con esto: cada vez que se llama a MyComponent, se cargan los datos. Puede que quieras esto en algunas circunstancias, pero la gran mayoría del tiempo, solo quieres recargar los productos cuando algo cambia. Aquí es donde entra el hook useEffect. Acepta una función que solo se llamará si la lista de dependencias ha cambiado.
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]);
Si añades el minuto actual a la lista de dependencias, la lista de productos se actualizará como máximo cada minuto.
useEffect(() => {
listProducts().then(setProducts);
}, [
// Time in milliseconds rounded down to the minute
Math.floor(Date.now() / (60 * 1000))
]);
Si solo quieres llamar a la función una vez, haz que la lista de dependencias esté vacía. Si quieres llamarla después de cada renderizado, no especifiques ninguna lista de dependencias en absoluto (es decir, useEffect(callback)). Lee más aquí.
Algunas otras cosas: tu clase App es inútil. Puede que estés acostumbrado a la programación orientada a objetos en lenguajes como Java y C#, pero el JavaScript moderno evita las clases tanto como sea razonable. Además, no necesitas extender React.Component porque nunca estás renderizando la clase con React. Te recomendaría mover las funciones fuera de las clases. Además, parece que no estás seguro de cómo funcionan las Promesas. Se llaman de forma asíncrona, la función de callback se llama mucho después de que la función que las encierra haya terminado, a menos que uses async/await. Haré una refactorización de esto para ti, pero realmente no deberías asumir algo tan difícil sin tener los conceptos básicos. Prueba esta guía de Promesas para empezar, y luego aprende cómo async/await facilita evitar infinitos .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;
}
Uniéndolo todo:
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>
);
}
EDITAR: Viendo tu comentario en otra respuesta, no puedes eliminar la lógica de la clase. Creo que puedes estar malinterpretando los requisitos de cualquier framework que estés usando, pero si realmente no puedes evitarlo, deberías crear el objeto de la clase que estás usando dentro del callback de useEffect.
Respuesta #2
Para react-native, también puedes usar los hooks de react para llamar a un método que contenga llamadas a la API. Prueba esto a continuación,
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);
});
});
}
por favor comenta tus opiniones si esto funciona o no. Me encantaría resolverlo si no funciona.
