Sylter
Sylter

Reputation: 1623

Dilemma about React Hooks and AJAX call triggered by user by clicking on a button

I am facing a dilemma on how to properly implement the following (basic) functionality.

I have a simple React component with a button, when the user presses the button, an AJAX call is triggered to a REST server and then the result is shown to the user.

My component is something similar to the following:

function MainComponent() {
    const [data, setData] = useState(null);

    async function getData() {
        const response = await fetch(...);
        setData(response);
    }

    return (
        <div>
            <button type="button" onClick={getData} />
            <div>
                {data}
            </div>
        </div>
    );
}

I would like to add a loading spinner since the fectch call can take a while and here is when the doubts start. How do I handle properly the visibility state of the loading spinner?

function MainComponent() {
    const [data, setData] = useState(null);
    const [isLoading, setIsLoading] = useState(false);

    async function getData() {
        setIsLoading(true);
        const response = await fetch(...);
        setIsLoading(false);
        setData(response);
    }

    return (
        <div>
            <button type="button" onClick={getData} />
            { isLoading ?
                    <span>Loading...</span>
                :
                    null
            }
            <div>
                {data}
            </div>
        </div>
    );
}

Something like the code above works, but I have the feeling that it's not correct.

The second call to setIsLoading, the one after waiting for the fetch request to end, as far as I understood, should not work (but it does).

Let me explain why I think it should not work:

  1. Render the component

  2. User presses the button, the call setIsLoading(true) is done and at some point it will trigger a re-render.

  3. That re-render happens, the "pointer" to the function setIsLoading should (or maybe not) change because the useState hook is called again due to the re-render

  4. The call to setIsLoading(false) after the fetch (but also the one to setData) should affect an already old version of the component

Am I wrong? If yes, why? If my understanding of React Hooks is correct, how do I implement this simple use case in a proper way? (Maybe with an useEffect?)

Thank you very much

EDIT:

@Brad Ball proposed to use useEffect, but the problem of a re-render n between the function execution still persists. How am I sure that the re-render after setIsLoading(true); keeps all the pointers to the functions correct? And why and how is it different from the previous call?

function MainComponent() {
    const [data, setData] = useState(null);

    const [request, setRequest] = useState(undefined);
    const [isLoading, setIsLoading] = useState(false);

    useEffect(() => {
        fetchData();
    }, [request]);

    async function fetchData() {
        setIsLoading(true);
        const response = await fetch(request.url, ...);
        setIsLoading(false);
        setData(response);
    }

    function getData() {
        setRequest({
            url: '...',
            body: '...'
        })
    }

    return (
        <div>
            <button type="button" onClick={getData} />
            { isLoading ?
                    <span>Loading...</span>
                :
                    null
            }
            <div>
                {data}
            </div>
        </div>
    );
}

EDIT 2:

Thanks for the answer: the problem is not coding it, it's on how to do it properly. How do I deal with multiple setState in an async function, so a re-render is triggered while the function is still running? Mine was only an example of an use case

Upvotes: 3

Views: 1035

Answers (3)

Will Jenkins
Will Jenkins

Reputation: 9887

Your code looks fine. Using useState ensures that your component references the correct values and setters when they're called.

In terms of worrying about the exact order of re-renders etc, you shouldn't be concerned about what is rendering when. React code should be declaritive - your code describes what the end result should be, not how to get there. In terms of your specific case and concerns, the timing of re-renders is out of your control as state updates (and possible re-renders) are batched for optimisation purposes.

Dan Abramov had this to say about multple setState calls:

In current release, they will be batched together if you are inside a React event handler. React batches all setStates done during a React event handler, and applies them just before exiting its own browser event handler.

I assume something similar applies to the hooks case but don't know this for sure.

Upvotes: 2

Ashish Kamble
Ashish Kamble

Reputation: 2627

You need to use conditional rendering,

return (
        <div>
            <button type="button" onClick={getData} />
            { isLoading ?
                    <span>Loading...</span>
                :
                <div>
                  {data}
                </div>
            }            
        </div>
    );

Upvotes: 0

Brad Ball
Brad Ball

Reputation: 619

You need to use the useEffect hook. It is built in for these reasons. it is similar to componentDidMount in classes

Link to docs here

Upvotes: 0

Related Questions