How to send request on click React Hooks way?

ReactjsReact Hooks

Reactjs Problem Overview


How to send http request on button click with react hooks? Or, for that matter, how to do any side effect on button click?

What i see so far is to have something "indirect" like:

export default = () => {
  const [sendRequest, setSendRequest] = useState(false);

  useEffect(() => {
    if(sendRequest){
       //send the request
       setSendRequest(false);
    }
  },
  [sendRequest]);

  return (
    <input type="button" disabled={sendRequest} onClick={() => setSendRequest(true)}
  );
}

Is that the proper way or is there some other pattern?

Reactjs Solutions


Solution 1 - Reactjs

export default () => {
  const [isSending, setIsSending] = useState(false)
  const sendRequest = useCallback(async () => {
    // don't send again while we are sending
    if (isSending) return
    // update state
    setIsSending(true)
    // send the actual request
    await API.sendRequest()
    // once the request is sent, update state again
    setIsSending(false)
  }, [isSending]) // update the callback if the state changes

  return (
    <input type="button" disabled={isSending} onClick={sendRequest} />
  )
}

this is what it would boil down to when you want to send a request on click and disabling the button while it is sending

update:

@tkd_aj pointed out that this might give a warning: "Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function."

Effectively, what happens is that the request is still processing, while in the meantime your component unmounts. It then tries to setIsSending (a setState) on an unmounted component.

export default () => {
  const [isSending, setIsSending] = useState(false)
  const isMounted = useRef(true)
  
  // set isMounted to false when we unmount the component
  useEffect(() => {
    return () => {
      isMounted.current = false
    }
  }, [])

  const sendRequest = useCallback(async () => {
    // don't send again while we are sending
    if (isSending) return
    // update state
    setIsSending(true)
    // send the actual request
    await API.sendRequest()
    // once the request is sent, update state again
    if (isMounted.current) // only update if we are still mounted
      setIsSending(false)
  }, [isSending]) // update the callback if the state changes

  return (
    <input type="button" disabled={isSending} onClick={sendRequest} />
  )
}

Solution 2 - Reactjs

You don't need an effect to send a request on button click, instead what you need is just a handler method which you can optimise using useCallback method

const App = (props) => {
   //define you app state here
   const fetchRequest = useCallback(() => {
       // Api request here
   }, [add dependent variables here]);

  return (
    <input type="button" disabled={sendRequest} onClick={fetchRequest}
  );
}

Tracking request using variable with useEffect is not a correct pattern because you may set state to call api using useEffect, but an additional render due to some other change will cause the request to go in a loop

Solution 3 - Reactjs

In functional programming, any async function should be considered as a side effect.

When dealing with side effects you need to separate the logic of starting the side effect and the logic of the result of that side effect (similar to redux saga).

Basically, the button responsibility is only triggering the side effect, and the side effect responsibility is to update the dom.

Also since react is dealing with components you need to make sure your component still mounted before any setState or after every await this depends on your own preferences.

to solve this issue we can create a custom hook useIsMounted this hook will make it easy for us to check if the component is still mounted

/**
 * check if the component still mounted
 */
export const useIsMounted = () => {
  const mountedRef = useRef(false);
  const isMounted = useCallback(() => mountedRef.current, []);

  useEffect(() => {
    mountedRef.current = true;
    return () => {
      mountedRef.current = false;
    };
  });

  return isMounted;
};

Then your code should look like this

export const MyComponent = ()=> {
  const isMounted = useIsMounted();
  const [isDoMyAsyncThing, setIsDoMyAsyncThing] = useState(false);

  // do my async thing
  const doMyAsyncThing = useCallback(async () => {
     // do my stuff
  },[])

  /**
   * do my async thing effect
  */
  useEffect(() => {
    if (isDoMyAsyncThing) {
      const effect = async () => {
        await doMyAsyncThing();
        if (!isMounted()) return;
        setIsDoMyAsyncThing(false);
      };
      effect();
    }
  }, [isDoMyAsyncThing, isMounted, doMyAsyncThing]);

  return (
     <div> 
        <button disabled={isDoMyAsyncThing} onClick={()=> setIsDoMyAsyncThing(true)}>
          Do My Thing {isDoMyAsyncThing && "Loading..."}
        </button>;
     </div>
  )
}

Note: It's always better to separate the logic of your side effect from the logic that triggers the effect (the useEffect)

UPDATE:

Instead of all the above complexity just use useAsync and useAsyncFn from the react-use library, It's much cleaner and straightforward.

Example:

import {useAsyncFn} from 'react-use';

const Demo = ({url}) => {

  const [state, doFetch] = useAsyncFn(async () => {
    const response = await fetch(url);
    const result = await response.text();
    return result
  }, [url]);

  return (
    <div>
      {state.loading
        ? <div>Loading...</div>
        : state.error
          ? <div>Error: {state.error.message}</div>
          : <div>Value: {state.value}</div>
      }
      <button onClick={() => doFetch()}>Start loading</button>
    </div>
  );
};

Solution 4 - Reactjs

You can fetch data as an effect of some state changing like you have done in your question, but you can also get the data directly in the click handler like you are used to in a class component.

Example

const { useState } = React;

function getData() {
  return new Promise(resolve => setTimeout(() => resolve(Math.random()), 1000))
}

function App() {
  const [data, setData] = useState(0)

  function onClick() {
    getData().then(setData)
  }

  return (
    <div>
      <button onClick={onClick}>Get data</button>
      <div>{data}</div>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));

<script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>

<div id="root"></div>

Solution 5 - Reactjs

You can define the boolean in the state as you did and once you trigger the request set it to true and when you receive the response set it back to false:

const [requestSent, setRequestSent] = useState(false);

const sendRequest = () => {
  setRequestSent(true);
  fetch().then(() => setRequestSent(false));
};

Working example

Solution 6 - Reactjs

You can create a custom hook useApi and return a function execute which when called will invoke the api (typically through some onClick).

useApi hook:

export type ApiMethod = "GET" | "POST";

export type ApiState = "idle" | "loading" | "done";

const fetcher = async (
    url: string,
    method: ApiMethod,
    payload?: string
  ): Promise<any> => {
    const requestHeaders = new Headers();
    requestHeaders.set("Content-Type", "application/json");
  
    console.log("fetching data...");
    const res = await fetch(url, {
      body: payload ? JSON.stringify(payload) : undefined,
      headers: requestHeaders,
      method,
    });
  
    const resobj = await res.json();
    return resobj;
  };

export function useApi(
  url: string,
  method: ApiMethod,
  payload?: any
): {
  apiState: ApiState;
  data: unknown;
  execute: () => void;
} {
  const [apiState, setApiState] = useState<ApiState>("idle");

  const [data, setData] = useState<unknown>(null);
  const [toCallApi, setApiExecution] = useState(false);

  const execute = () => {
    console.log("executing now");
    setApiExecution(true);
  };


  const fetchApi = useCallback(() => {
    console.log("fetchApi called");
    fetcher(url, method, payload)
      .then((res) => {
        const data = res.data;
        setData({ ...data });
        return;
      })
      .catch((e: Error) => {
        setData(null);
        console.log(e.message);
      })
      .finally(() => {
        setApiState("done");
      });
  }, [method, payload, url]);

  // call api
  useEffect(() => {
    if (toCallApi &&  apiState === "idle") {
      console.log("calling api");
      setApiState("loading");
      fetchApi();
    }
  }, [apiState, fetchApi, toCallApi]);

  return {
    apiState,
    data,
    execute,
  };
}

using useApi in some component:

const SomeComponent = () =>{

const { apiState, data, execute } = useApi(
      "api/url",
      "POST",
      {
        foo: "bar",
      }
    );

}

if (apiState == "done") {
      console.log("execution complete",data);
}

return (
 <button
   onClick={() => {
            execute();
          }}>
Click me
</button>
);


Solution 7 - Reactjs

For this you can use callback hook in ReactJS and it is the best option for this purpose as useEffect is not a correct pattern because may be you set state to make an api call using useEffect, but an additional render due to some other change will cause the request to go in a loop.

 <const Component= (props) => {
       //define you app state here
       const getRequest = useCallback(() => {
           // Api request here
       }, [dependency]);
    
      return (
        <input type="button" disabled={sendRequest} onClick={getRequest}
      );
    }

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questiondee zgView Question on Stackoverflow
Solution 1 - ReactjsDoXicKView Answer on Stackoverflow
Solution 2 - ReactjsShubham KhatriView Answer on Stackoverflow
Solution 3 - ReactjsFareed AlnamroutiView Answer on Stackoverflow
Solution 4 - ReactjsTholleView Answer on Stackoverflow
Solution 5 - ReactjsTeneffView Answer on Stackoverflow
Solution 6 - ReactjsGorvGoylView Answer on Stackoverflow
Solution 7 - ReactjsJamal AshrafView Answer on Stackoverflow