El escenario es relativamente simple: tenemos un cálculo bajo demanda de larga ejecución que se produce en un servidor remoto. Queremos memorizar el resultado. Aunque estamos recuperando de forma asincrónica desde un recurso remoto, esto no es un efecto secundario porque solo queremos que el resultado de este cálculo se muestre al usuario y definitivamente no queremos hacer esto en cada renderizado.
Problema: parece que React.useMemo no es compatible directamente con async/await de Typescript y devolverá una promesa:
//returns a promise: let myMemoizedResult = React.useMemo(() => myLongAsyncFunction(args), [args]) //also returns a promise: let myMemoizedResult = React.useMemo(() => (async () => await myLongAsyncFunction(args)), [args])
¿Cuál es la forma correcta de esperar el resultado de una función asíncrona y memorizar el resultado usando React.useMemo? He usado promesas regulares con JS simple, pero aún lucho con ellas en este tipo de situaciones.
Probé otros enfoques como memoize-one, pero el problema parece ser que this
contexto cambia debido a la forma en que los componentes de la función React funcionan y rompen la memoización , por lo que estoy tratando de usar React.useMemo.
Tal vez estoy tratando de colocar una clavija cuadrada en un agujero redondo aquí; si ese es el caso, sería bueno saberlo también. Por ahora, probablemente solo voy a implementar mi propia función de memorización.
Editar: creo que parte de eso fue que estaba cometiendo un error tonto diferente con memoize-one, pero todavía estoy interesado en saber la respuesta aquí con React.memo.
Aquí hay un fragmento: la idea no es usar el resultado memorizado directamente en el método de renderizado, sino más bien como algo a lo que hacer referencia de una manera impulsada por eventos, es decir, al hacer clic en el botón Calcular.
export const MyComponent: React.FC = () => { let [arg, setArg] = React.useState('100'); let [result, setResult] = React.useState('Not yet calculated'); //My hang up at the moment is that myExpensiveResultObject is //Promise<T> rather than T let myExpensiveResultObject = React.useMemo( async () => await SomeLongRunningApi(arg), [arg] ); const getResult = () => { setResult(myExpensiveResultObject.interestingProperty); } return ( <div> <p>Get your result:</p> <input value={arg} onChange={e => setArg(e.target.value)}></input> <button onClick={getResult}>Calculate</button> <p>{`Result is ${result}`}</p> </div>); }
Editar: mi respuesta original a continuación parece tener algunos efectos secundarios no deseados debido a la naturaleza asíncrona de la llamada. En su lugar, intentaría pensar en memorizar el cálculo real en el servidor o usar un cierre escrito por mí mismo para verificar si el arg
no ha cambiado. De lo contrario, aún puede utilizar algo como useEffect
como se describe a continuación.
Creo que el problema es que las funciones async
siempre devuelven implícitamente una promesa. Dado que este es el caso, puede await
directamente el resultado para desenvolver la promesa:
const getResult = async () => { const result = await myExpensiveResultObject; setResult(result.interestingProperty); };
Vea un ejemplo de codesandbox aquí .
Sin embargo, creo que un mejor patrón puede ser utilizar un useEffect
que dependa de algún objeto de estado que solo se establece al hacer clic en el botón en este caso, pero parece que useMemo
debería funcionar.
Creo que React menciona específicamente que useMemo no debe usarse para administrar efectos secundarios como llamadas API asíncronas. Deben administrarse en ganchos useEffect
donde se establezcan las dependencias adecuadas para determinar si deben volver a ejecutarse o no.
Lo que realmente desea es volver a renderizar su componente una vez que finaliza la llamada asincrónica. La memorización por sí sola no te ayudará a lograrlo. En su lugar, debe usar el estado de React: mantendrá el valor que devolvió su llamada asíncrona y le permitirá activar una nueva representación.
Además, desencadenar una llamada asíncrona es un efecto secundario, por lo que no debe realizarse durante la fase de procesamiento, ni dentro del cuerpo principal de la función del componente, ni dentro useMemo(...)
que también ocurre durante la fase de procesamiento. En su lugar, todos los efectos secundarios deben activarse dentro useEffect
.
Aquí está la solución completa:
const [result, setResult] = useState() useEffect(() => { let active = true load() return () => { active = false } async function load() { setResult(undefined) // this is optional const res = await someLongRunningApi(arg1, arg2) if (!active) { return } setResult(res) } }, [arg1, arg2])
Aquí llamamos a la función asíncrona dentro useEffect
. Tenga en cuenta que no puede realizar la devolución de llamada completa dentro useEffect
async; es por eso que declaramos una load
de función asíncrona dentro y la llamamos sin esperar.
El efecto se volverá a ejecutar una vez que cambie uno de los arg
: esto es lo que desea en la mayoría de los casos. Así que asegúrese de memorizar los arg
si los vuelve a calcular en el renderizado. Hacer setResult(undefined)
es opcional; en su lugar, es posible que desee mantener el resultado anterior en la pantalla hasta que obtenga el siguiente resultado. O puede hacer algo como setLoading(true)
para que el usuario sepa lo que está pasando.
Usar bandera active
es importante. Sin ella, se está exponiendo a una condición de carrera a punto de ocurrir: la segunda llamada a la función asíncrona puede finalizar antes de que finalice la primera:
setResult()
setResult()
ocurre de nuevo, sobrescribiendo el resultado correcto con uno obsoleto y su componente termina en un estado inconsistente. Evitamos eso al usar la función de limpieza de useEffect
para restablecer el indicador active
:
active#1 = true
, iniciar la primera llamadaactive#1 = false
active#2 = true
, iniciar la segunda llamadasetResult()
setResult()
no sucede ya que active#1
es false