Reputation: 10021
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.net/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.net/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
Upvotes: 5
Views: 2705
Reputation: 10021
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 complexity:
this hook has been published to NPM if interested: https://www.npmjs.com/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 combo 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 combo.
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 combo 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 combo 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.net/ahmadabdul3/atgcxhod/5/
Upvotes: 2
Reputation: 28654
If 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.
Upvotes: 5