25

I have just began playing around with React hooks and am wondering how an AJAX request should look?

I have tried many attempts, but am unable to get it to work, and also don't really know the best way to implement it. Below is my latest attempt:

import React, { useState, useEffect } from 'react';

const App = () => {
    const URL = 'http://api.com';
    const [data, setData] = useState({});

    useEffect(() => {
        const resp = fetch(URL).then(res => {
          console.log(res)
        });
    });

    return (
        <div>
          // display content here
        </div>
    )
}
Yangshun Tay
  • 49,270
  • 33
  • 114
  • 141
peter flanagan
  • 9,195
  • 26
  • 73
  • 127
  • 5
    _"I have tried many attempts"_ What did you try? "am unable to get it to work" What happened instead of what you expected? – Alex Wayne Oct 30 '18 at 07:14
  • Does this answer your question? [what is right way to do API call in react js?](https://stackoverflow.com/questions/38742334/what-is-right-way-to-do-api-call-in-react-js) – sleske May 14 '21 at 07:05

9 Answers9

51

You could create a custom hook called useFetch that will implement the useEffect hook.

If you pass an empty array as the second argument to the useEffect hook will trigger the request on componentDidMount. By passing the url in the array this will trigger this code anytime the url updates.

Here is a demo in code sandbox.

See code below.

import React, { useState, useEffect } from 'react';

const useFetch = (url) => {
  const [data, setData] = useState(null);

  useEffect(() => {
    async function fetchData() {
      const response = await fetch(url);
      const json = await response.json();
      setData(json);
    }
    fetchData();
  }, [url]);

  return data;
};

const App = () => {
    const URL = 'http://www.example.json';
    const result = useFetch(URL);
 
    return (
      <div>
        {JSON.stringify(result)}
      </div>
    );
}
Paul Fitzgerald
  • 11,770
  • 4
  • 42
  • 54
9

Works just fine... Here you go:

import React, { useState, useEffect } from 'react';

const useFetch = url => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  const fetchUser = async () => {
    const response = await fetch(url);
    const data = await response.json();
    const [user] = data.results;
    setData(user);
    setLoading(false);
  };

  useEffect(() => {
    fetchUser();
  }, []);

  return { data, loading };
};

const App = () => {
  const { data, loading } = useFetch('https://api.randomuser.me/');

  return (
    <div className="App">
      {loading ? (
        <div>Loading...</div>
      ) : (
        <React.Fragment>
          <div className="name">
            {data.name.first} {data.name.last}
          </div>
          <img className="cropper" src={data.picture.large} alt="avatar" />
        </React.Fragment>
      )}
    </div>
  );
};

Live Demo:

Edit x908rkw8yq

Edit

Updated based on version change (thanks @mgol for bringing it to my attention in the comments).

halfer
  • 19,824
  • 17
  • 99
  • 186
SakoBu
  • 3,972
  • 1
  • 16
  • 33
  • 1
    You shouldn't pass an async function to useEffect as such a function returns a promise instead of a cleanup function. Newer React will print a warning to the console in such a case: https://codesandbox.io/s/1qn0pjx0oj – mgol Feb 22 '19 at 11:06
  • @mgol Seems the rest of the answers still have the exact same issue :) – SakoBu Feb 23 '19 at 20:12
  • @SakoBu Thanks for the update. I removed my downvote & upvoted your post. There's no cleanup but it might not be needed here. – mgol Feb 24 '19 at 00:28
  • 1
    Downvoted. This answer could do with some explanation of what is happening in the code, so it's more helpful to beginners who find this question. – David Yell Aug 02 '21 at 12:20
  • 1
    December 2021: This CodeSandbox link has a lint error: "React Hook useEffect has a missing dependency: 'fetchUser'. Either include it or remove the dependency array. (react-hooks/exhaustive-deps)" And if you do either fix as the message requests...then the API call fires every render. So is the real fix to move the fetchUser declaration inside the useEffect code? – Andy Dec 19 '21 at 16:15
6

Great answers so far, but I'll add a custom hook for when you want to trigger a request, because you can do that too.

function useTriggerableEndpoint(fn) {
  const [res, setRes] = useState({ data: null, error: null, loading: null });
  const [req, setReq] = useState();

  useEffect(
    async () => {
      if (!req) return;
      try {
        setRes({ data: null, error: null, loading: true });
        const { data } = await axios(req);
        setRes({ data, error: null, loading: false });
      } catch (error) {
        setRes({ data: null, error, loading: false });
      }
    },
    [req]
  );

  return [res, (...args) => setReq(fn(...args))];
}

You can create a function using this hook for a specific API method like so if you wish, but be aware that this abstraction isn't strictly required and can be quite dangerous (a loose function with a hook is not a good idea in case it is used outside of the context of a React component function).

const todosApi = "https://jsonplaceholder.typicode.com/todos";

function postTodoEndpoint() {
  return useTriggerableEndpoint(data => ({
    url: todosApi,
    method: "POST",
    data
  }));
}

Finally, from within your function component

const [newTodo, postNewTodo] = postTodoEndpoint();

function createTodo(title, body, userId) {
  postNewTodo({
    title,
    body,
    userId
  });
}

And then just point createTodo to an onSubmit or onClick handler. newTodo will have your data, loading and error statuses. Sandbox code right here.

cwharris
  • 17,835
  • 4
  • 44
  • 64
horyd
  • 1,364
  • 10
  • 12
  • 2
    The way you're calling ajax request inside `useEffect` is wrong. According to doc, `useEffect` function must be pure as it also does cleanup automatically. So, the correct way is: `useEffect(() => { fetchData() })` where fetchData can be written as: `async function fetchData() { ...}`. – Ashish Rawat Mar 01 '19 at 12:35
  • Hi @AshishRawat which docs are you referring to? Can we have the link, please? – Rohman HM Oct 26 '19 at 15:57
5

use-http is a little react useFetch hook used like: https://use-http.com

import useFetch from 'use-http'

function Todos() {
  const [todos, setTodos] = useState([])
  const { request, response } = useFetch('https://example.com')

  // componentDidMount
  useEffect(() => { initializeTodos() }, [])

  async function initializeTodos() {
    const initialTodos = await request.get('/todos')
    if (response.ok) setTodos(initialTodos)
  }

  async function addTodo() {
    const newTodo = await request.post('/todos', {
      title: 'no way',
    })
    if (response.ok) setTodos([...todos, newTodo])
  }

  return (
    <>
      <button onClick={addTodo}>Add Todo</button>
      {request.error && 'Error!'}
      {request.loading && 'Loading...'}
      {todos.map(todo => (
        <div key={todo.id}>{todo.title}</div>
      )}
    </>
  )
}

or, if you don't want to manage the state yourself, you can do

function Todos() {
  // the dependency array at the end means `onMount` (GET by default)
  const { loading, error, data } = useFetch('/todos', [])

  return (
    <>
      {error && 'Error!'}
      {loading && 'Loading...'}
      {data && data.map(todo => (
        <div key={todo.id}>{todo.title}</div>
      )}
    </>
  )
}

Live Demo

Edit Basic Example

Alex Cory
  • 10,635
  • 10
  • 52
  • 62
2

I'd recommend you to use react-request-hook as it covers a lot of use cases (multiple request at same time, cancelable requests on unmounting and managed request states). It is written in typescript, so you can take advantage of this if your project uses typescript as well, and if it doesn't, depending on your IDE you might see the type hints, and the library also provides some helpers to allow you to safely type the payload that you expect as result from a request.

It's well tested (100% code coverage) and you might use it simple as that:

function UserProfile(props) {
  const [user, getUser] = useResource((id) => {
    url: `/user/${id}`,
    method: 'GET'
  })

  useEffect(() => getUser(props.userId), []);

  if (user.isLoading) return <Spinner />;
  return (
    <User 
      name={user.data.name}
      age={user.data.age}
      email={user.data.email}
    >  
  )
}

image example

Author disclaimer: We've been using this implementation in production. There's a bunch of hooks to deal with promises but there are also edge cases not being covered or not enough test implemented. react-request-hook is battle tested even before its official release. Its main goal is to be well tested and safe to use as we're dealing with one of the most critical aspects of our apps.

  • Excellent library Matheus! One question: how do you mock axios for testing an app using react-request-hook? I haven't managed to use axios-mock-adapter with it. – Nicolas Bonnefon Apr 13 '19 at 21:05
1

Here's something which I think will work:

import React, { useState, useEffect } from 'react';

const App = () => {
    const URL = 'http://api.com';
    const [data, setData] = useState({})

    useEffect(function () {
      const getData = async () => {
        const resp = await fetch(URL);
        const data = await resp.json();

        setData(data);
      }
      getData();
    }, []);

    return (
      <div>
        { data.something ? data.something : 'still loading' }
      </div>
    )
}

There are couple of important bits:

  • The function that you pass to useEffect acts as a componentDidMount which means that it may be executed many times. That's why we are adding an empty array as a second argument, which means "This effect has no dependencies, so run it only once".
  • Your App component still renders something even tho the data is not here yet. So you have to handle the case where the data is not loaded but the component is rendered. There's no change in that by the way. We are doing that even now.
Krasimir
  • 13,306
  • 3
  • 40
  • 55
  • This approach is not appropriate. See the following warning you'll get: ```index.js:1 Warning: An effect function must not return anything besides a function, which is used for clean-up. It looks like you wrote useEffect(async () => ...) or returned a Promise. Instead, write the async function inside your effect and call it immediately: useEffect(() => { async function fetchData() { // You can await here const response = await MyAPI.getData(someId); // ... } fetchData(); }, [someId]); // Or [] if effect doesn't need props or state``` – Jason Rice Aug 27 '20 at 16:27
  • Just updated the snippet. The problem was the `async` in front of the closure. – Krasimir Aug 28 '20 at 07:51
1

Traditionally, you would write the Ajax call in the componentDidMount lifecycle of class components and use setState to display the returned data when the request has returned.

With hooks, you would use useEffect and passing in an empty array as the second argument to make the callback run once on mount of the component.

Here's an example which fetches a random user profile from an API and renders the name.

function AjaxExample() {
  const [user, setUser] = React.useState(null);
  React.useEffect(() => {
    fetch('https://randomuser.me/api/')
      .then(results => results.json())
      .then(data => {
        setUser(data.results[0]);
      });
  }, []); // Pass empty array to only run once on mount.
  
  return <div>
    {user ? user.name.first : 'Loading...'}
  </div>;
}

ReactDOM.render(<AjaxExample/>, document.getElementById('app'));
<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>

<div id="app"></div>
Yangshun Tay
  • 49,270
  • 33
  • 114
  • 141
1

I find many wrong usages of useEffect in the answers above.

An async function shouldn't be passed into useEffect.

Let's see the signature of useEffect:

useEffect(didUpdate, inputs);

You can do side effects in didUpdate function, and return a dispose function. The dispose function is very important, you can use that function to cancel a request, clear a timer etc.

Any async function will return a promise, but not a function, so the dispose function actually takes no effects.

So pass in an async function absolutely can handle your side effects, but is an anti-pattern of Hooks API.

Yuanqiu Li
  • 1,600
  • 1
  • 9
  • 12
0

Here's perhaps a slightly refined modification to the accepted answer. (Includes error handling and passing options to fetch)

const useFetch = (url, options = {}) => {
  const [response, setResponse] = React.useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch(url, options)
      .then(res => res.json())
      .then(setResponse)
      .catch(setError);
  }, [url, ...Object.values(options)]);

  return {response, error};
};
Matt
  • 5,315
  • 1
  • 30
  • 57