Search code examples
javascriptnode.jspromiseapollo

Javascript Promise: Return 'In Progress' Response Until Promise Resolves?


This isn't really an Apollo question, it's a Javascript promises question, but uses an example from Apollo, because that's the only time I recall seeing it.

Apollo has a React hook that looks like this:

const { loading, error, data } = useQuery(GET_DOGS);

I understand how it returns error -- if the promise resolver throws an error, you get an error back.

I understand how it returns data -- when the promise resolver completes, it returns the data.

But how does it return loading and then later return data? I've coded quite a few node.js promise resolvers and haven't yet seen a pattern that could return loading while the operation is in process, and then later return the data.

What Javascript pattern makes this possible?


Solution

  • They'd use a state variable that starts true and is switched to false when they're done, vaguely like this:

    function useQuery(/*...*/) {
        const [loading, setLoading] = useState(true);
        const [error, setError] = useState(null);
        const [data, setData] = useState(null);
    
        useEffect(() => {
            let cancelled = false;
            goGetTheStuff()
            .then(data => {
                if (!cancelled) {
                    setData(data);
                    setLoading(false);
                }
            })
            .catch(error => {
                if (!cancelled) {
                    setError(error);
                    setLoading(false);
                }
            });
            return () => {
                cancelled = true;
            };
        }, []);
    
        return {loading, error, data};
    }
    

    Live Example:

    const {useState, useEffect} = React;
    
    function goGetTheStuff() {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                if (Math.random() < 0.7) {
                    // Emulate success
                    resolve({data: "here"});
                } else {
                    // Emulate failure
                    reject(new Error("Couldn't get the data"));
                }
            }, 800);
        });
    }
    
    function useQuery(/*...*/) {
        const [loading, setLoading] = useState(true);
        const [error, setError] = useState(null);
        const [data, setData] = useState(null);
    
        useEffect(() => {
            let cancelled = false;
            goGetTheStuff()
            .then(data => {
                if (!cancelled) {
                    setData(data);
                    setLoading(false);
                }
            })
            .catch(error => {
                if (!cancelled) {
                    setError(error);
                    setLoading(false);
                }
            });
            return () => {
                cancelled = true;
            };
        }, []);
    
        return {loading, error, data};
    }
    
    function Example() {
        const {loading, error, data} = useQuery();
        return (
            <div>
                <div>loading: {JSON.stringify(loading)}</div>
                <div>data: {data && JSON.stringify(data)}</div>
                <div>error: {error && error.message}</div>
            </div>
        );
    }
    
    ReactDOM.render(<Example/>, document.getElementById("root"));
    <div>70% of the time when you run this, the async operation succeeds; 30% of the time, it fails. Run repeatedly if you want to see both scenarios.</div>
    <hr>
    <div id="root"></div>
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js"></script>