so I'm trying to be a clever-a$$ and return a promise from a hook (so I can await the value instead of waiting for the hook to give me the value after its resolved and the hook reruns). I'm attempting something like this, and everything is working until the resolve part. The .then
doesnt ever seem to run, which tells me that the resolve I set isn't firing correctly. Here's the code:
function App() {
const { valPromise } = useSomeHook();
const [state, setState] = React.useState();
React.useEffect(() => {
valPromise.then(r => {
setState(r);
});
}, []);
if (!state) return 'not resolved yet';
return 'resolved: ' + state;
}
function useSomeHook() {
const [state, setState] = React.useState();
const resolve = React.useRef();
const valPromise = React.useRef(new Promise((res) => {
resolve.current = res;
}));
React.useEffect(() => {
getValTimeout({ setState });
}, []);
React.useEffect(() => {
if (!state) return;
resolve.current(state);
}, [state]);
return { valPromise: valPromise.current, state };
}
function getValTimeout({ setState }) {
setTimeout(() => {
setState('the val');
}, 1000);
}
and a working jsfiddle: /
I tried something similar (re-assigning the 'resolve' part of the promise constructor) with plain functions, and it seems to work:
let resolve;
function initPromise() {
return new Promise((res) => {
resolve = res;
});
}
function actionWithTimeout() {
setTimeout(() => {
resolve('the val');
}, 2000);
}
const promise = initPromise();
actionWithTimeout();
promise.then(console.log);
jsfiddle: /
which makes me think something is happening with the useRef or with rendering.
** update **
so it looks like the useRefs are working fine. its the final call to 'res' (or resolve) that doesn't seem to fulfill the promise (promise stays pending). not sure if a reference (the one being returned from the hook) is breaking between renders or something
so I'm trying to be a clever-a$$ and return a promise from a hook (so I can await the value instead of waiting for the hook to give me the value after its resolved and the hook reruns). I'm attempting something like this, and everything is working until the resolve part. The .then
doesnt ever seem to run, which tells me that the resolve I set isn't firing correctly. Here's the code:
function App() {
const { valPromise } = useSomeHook();
const [state, setState] = React.useState();
React.useEffect(() => {
valPromise.then(r => {
setState(r);
});
}, []);
if (!state) return 'not resolved yet';
return 'resolved: ' + state;
}
function useSomeHook() {
const [state, setState] = React.useState();
const resolve = React.useRef();
const valPromise = React.useRef(new Promise((res) => {
resolve.current = res;
}));
React.useEffect(() => {
getValTimeout({ setState });
}, []);
React.useEffect(() => {
if (!state) return;
resolve.current(state);
}, [state]);
return { valPromise: valPromise.current, state };
}
function getValTimeout({ setState }) {
setTimeout(() => {
setState('the val');
}, 1000);
}
and a working jsfiddle: https://jsfiddle/8a4oxse5/
I tried something similar (re-assigning the 'resolve' part of the promise constructor) with plain functions, and it seems to work:
let resolve;
function initPromise() {
return new Promise((res) => {
resolve = res;
});
}
function actionWithTimeout() {
setTimeout(() => {
resolve('the val');
}, 2000);
}
const promise = initPromise();
actionWithTimeout();
promise.then(console.log);
jsfiddle: https://jsfiddle/pa1xL025/
which makes me think something is happening with the useRef or with rendering.
** update **
so it looks like the useRefs are working fine. its the final call to 'res' (or resolve) that doesn't seem to fulfill the promise (promise stays pending). not sure if a reference (the one being returned from the hook) is breaking between renders or something
Share Improve this question edited Oct 24, 2022 at 12:26 Abdul Ahmad asked Oct 21, 2022 at 18:47 Abdul AhmadAbdul Ahmad 10k16 gold badges74 silver badges136 bronze badges 2-
If you're working with Promises, why do you use a callback in
getValTimeout()
? Why don't you make itasync
and reduce your "clever" hook tofunction useSomeHook() { return React.useRef().current ??= getValTimeout(); }
and thenconst valPromise = useSomeHook()
without that unnecessary object. – Thomas Commented Oct 24, 2022 at 8:57 -
this question is a simplified version of a more plex operation i need to do in my actual code. however, even with that said, i always use objects for function params and return values in JS. there are over a dozen benefits to doing this. so my function signatures are always something like:
function doX({ someParam } = {}) ...
and always return a val wrapped in an object. sometimes keeping things 'simple' is good, but i've found over the years that the 'simple' implementation is never enough, and always ends up needing more – Abdul Ahmad Commented Oct 24, 2022 at 11:18
2 Answers
Reset to default 5If you use this code the problem is gone:
const valPromise = React.useRef();
if (!valPromise.current) {
valPromise.current = new Promise((res) => {
resolve.current = res;
})
}
Normally you shouldn't write to ref during render but this case is ok.
Explanation
When you had this initially:
const valPromise = React.useRef(new Promise((res) => {
resolve.current = res;
}));
the promise here is actually recreated on each render and only the result from first render is used.
From the docs:
const playerRef = useRef(new VideoPlayer());
Although the result of new VideoPlayer() is only used for the initial render, you’re still calling this function on every render. This can be wasteful if it’s creating expensive objects.
So in your case that meant the resolve.current
would be updated on each render.
But the valPromise
remains the initial one.
Also since the expression passed to useRef
runs during rendering one shouldn't do there anything that you would not do during rendering, including side effects - which writing to resolve.current
was.
As Giorgi said, the useRef runs every render, but all results after the first run are discarded, which can cause the issue I was having above. So for those interested, I made the promise implementation into a standalone hook to abstract away the plexity:
this hook has been published to NPM if interested: https://www.npmjs./package/usepromisevalue
export function usePromiseValue() {
const resolve = React.useRef();
const promise = React.useRef(new Promise(_resolve => {
// - useRef is actually called on every render, but the
// subsequent result is discarded
// - however, this can cause the `resolve.current` to be overwritten
// which will make the initial promise unresolvable
// - this condition takes care of ensuring we always resolve the
// first promise
if (!resolve.current) resolve.current = _resolve;
}));
return {
promise: promise.current,
resolve: resolve.current,
};
}
(*** NOTE: see the bottom of this answer for the same hook but with the ability to update the promise/resolve bo so you can resolve multiple promises instead of just 1)
keep in mind, this will only resolve the promise 1 time. if you want the promise to react to state changes and be able to fire off another async operation and resolve a new promise, you'll need a more involved implementation that will return a new promise + resolve bo.
Usage; taking the code from my question above, but tweaking:
function useSomeHook() {
const [state, setState] = React.useState();
const { promise, resolve } = usePromiseValue();
React.useEffect(() => {
getValTimeout({ setState });
}, []);
React.useEffect(() => {
if (!state) return;
resolve(state);
}, [state]);
return {
valPromise: promise,
state,
};
}
*** UPDATE *** here's the updated promise hook that will handle giving you a new promise/resolve bo when dependecies change:
function usePromiseValue({
deps = [],
promiseUpdateTimeout = 200,
} = {}) {
const [count, setCount] = React.useState(0);
const [mountRender, setMountRender] = React.useState(true);
const resolve = React.useRef();
const promise = React.useRef(new Promise(_resolve => {
// - useRef is actually called on every render, but the
// subsequent result is discarded
// - however, this can cause the `resolve.current` to be overwritten
// which will make the initial promise unresolvable
// - this condition takes care of ensuring we always resolve the
// first promise
if (!resolve.current) resolve.current = _resolve;
}));
React.useEffect(() => {
setMountRender(false);
}, []);
React.useEffect(() => {
// - dont run this hook on mount, otherwise
// the promise will update and not be resolveable
if (mountRender) return;
setTimeout(() => {
setCount(count + 1);
}, promiseUpdateTimeout);
// - dont update the promise/resolve bo right away,
// otherwise the current promise will not resolve
// - instead, wait a short period (100-300 ms after state updates)
// to give the current promise time to resolve before updating
}, [...deps]);
React.useEffect(() => {
// - dont run this hook on mount, otherwise
// the promise will update and not be resolveable
if (mountRender) return;
promise.current = new Promise(r => resolve.current = r);
}, [count]);
return {
promise: promise.current,
resolve: resolve.current,
};
}
https://jsfiddle/ahmadabdul3/atgcxhod/5/