reactjs

Why can't I resolve a promise via a ref stored resolve function in React?


I'm trying to create a promisify style hook, the idea is to eventually use it with Redux Toolkit to allow using the new React 19 use hook.

Here's the gyst of how this thing would work:

function usePromise(input: {
    isLoading: true,
    data: null
} | {
    isLoading: false,
    data: string;
}) {


    // Store the resolve function in a ref 
    const resRef = useRef<(data: string) => void>(null);

    // Create a new promise 
    // Store the resolve function in the ref 
    const promiseRef = useRef(
        new Promise((res) => {
            resRef.current = res;

            //res("xxx") // 👈 this will resolve though
        })
    );

    // When input changes, if there is data, resolve the promise
    useEffect(() => {
        if (!input.isLoading) {
            resRef.current?.(input.data);
        }

    }, [input]);

    // Return the promise 
    return promiseRef.current;
}

With usage like:

export function MyComponent() {

    const [value, setValue] = useState<null | string>(null);

    const prom = usePromise(value ? {
        isLoading: false,
        data: value
    } : {
        isLoading: true,
        data: null
    });

    prom.then((v) => alert(v))

    return <div >

        <button onClick={() => setValue("123")}>Click me</button>
    </div>
}

Here, I would expect that when we click the button, the promise would resolve and we see the alert. However, it does not.

What's going on here?

I have a reproduction for this issue here: https://github.com/dwjohnston/react-promise-issue


Solution

  • Your problem is that you are creating a new promise every time the hook runs, setting resRef.current to the resolver function of that last promise. However only the first promise that is passed to the useRef hook is stored in promiseRef.current.

    To fix this, avoid recreating the promise:

    function usePromise(input) {
        const resRef = useRef();
        const promiseRef = useRef();
        if (!promiseRef.current) {
    //  ^^^^^^^^^^^^^^^^^^^^^^^^
            promiseRef.current = new Promise(resolve => {
                resRef.current = resolve;
            });
        }
        useEffect(() => {
            if (!input.isLoading) {
                resRef.current(input.data);
            }
        }, [input]);
        return promiseRef.current;
    }
    

    Alternatively, use a state that is initialised with a callback:

    function usePromise(input) {
        const [{ promise, resolve }] = useState(() => Promise.withResolvers());
        useEffect(() => {
            if (!input.isLoading) {
                resolve(input.data);
            }
        }, [input]);
        return promise;
    }