React Hooks - Making an Ajax request
Asked Answered
P

9

26

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>
    )
}
Peso answered 30/10, 2018 at 7:9 Comment(2)
"I have tried many attempts" What did you try? "am unable to get it to work" What happened instead of what you expected?Hagfish
Does this answer your question? what is right way to do API call in react js?Estimate
T
53

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>
    );
}
Tetreault answered 30/10, 2018 at 7:16 Comment(6)
What if I want to reuse the same response in another component? every call to useFetch will refetch the data even if the URL is same. There is no way to share the same useState between 2 components.Auberon
@WalidAmmar I guess you could move the useFetch to a parent component and pass the data down from thereTetreault
Why not use Context instead?Auberon
@PaulFitzgerald, where is your data property coming from? You should be getting ReferenceError: data is not defined.Junior
@peterflanagan, what about a CORS error, how did you avoid that?Junior
@Paul, you have a comment that says // empty array as second argument, yet you are not using an empty array?Plutonium
U
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).

Unsling answered 30/10, 2018 at 7:20 Comment(5)
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: codesandbox.io/s/1qn0pjx0ojBetz
@Betz Seems the rest of the answers still have the exact same issue :)Unsling
@Unsling Thanks for the update. I removed my downvote & upvoted your post. There's no cleanup but it might not be needed here.Betz
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.Shebeen
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?Dike
E
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.

Etui answered 31/10, 2018 at 3:22 Comment(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() { ...}.Indecorum
Hi @AshishRawat which docs are you referring to? Can we have the link, please?Stutzman
E
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

Egesta answered 7/9, 2019 at 4:7 Comment(0)
L
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.

Luciana answered 7/2, 2019 at 15:32 Comment(1)
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.Haemin
L
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.
Lethe answered 8/11, 2018 at 10:37 Comment(2)
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 stateTitanesque
Just updated the snippet. The problem was the async in front of the closure.Lethe
F
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/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

<div id="app"></div>
Fantasm answered 11/11, 2018 at 6:7 Comment(0)
S
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.

Silveira answered 15/1, 2019 at 9:2 Comment(0)
K
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};
};
Kilocalorie answered 5/5, 2023 at 15:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.