Home > OS >  How to fetch data from a custom React hook (API) with onClick and display it in a Div using TypeScri
How to fetch data from a custom React hook (API) with onClick and display it in a Div using TypeScri

Time:10-05

I'm new to TypeScript and even React Hooks. I'm trying to fetch data continuously from a custom hook called useFetch(), which takes in a string (the URL) parameter:

import { useEffect, useState } from "react";

export interface Payload {
  data: null | string,
  loading: boolean
}

const useFetch = (url: string) => {
  const [state, setState] = useState<Payload>({data: null, loading: true})

  useEffect(() => {
    setState(state => ({ data: state.data, loading: true }));
    fetch(url)
      .then(x => x.text())
      .then(y => {
        setState({ data: y, loading: false });
      });
  }, [url, setState])

  return state;
  }

export default useFetch;

I import that hook into App():

import React, { useState, useEffect } from "react";
import useFetch from './utils/useFetch'
import {Payload} from './utils/useFetch';

const App = () => {
  const [quote, setquote] = useState<Payload>({data: null, loading: true})
  const handleClick = () => setquote(data)

  const data = useFetch(
    "/api/rand"
  );

  return (
    <div>
      <h1>Quotes</h1>
      <button onClick={handleClick}>Get Quote</button>
      <div>{quote.data}</div>
    </div>
  )
}

export default App;

On the first time the app loads, it works. I get data (a quote) when I click the button.

Preview

However, when I click it multiple times, the API isn't called again, and new data doesn't come through. I believe I'm supposed to use the useEffect() react hook and maintain the state (maybe I'm mistaken) but all my attempts so far have been to no avail. Any help would be much appreciated. If I figure it out, I'll definitely answer this question. Thanks!

CodePudding user response:

It seems like you are mixing two ideas a little bit here.

Do you want to fetch data continuously? (Continuously meaning all the time, as in, periodically, at an interval)

Or do you want to fetch data upon mount of the component (currently happens) AND when the user taps the "Get Quote" button (not working currently)?

I will try to help you with both.

Continuously / periodically

Try changing the setup a bit. Inside the useFetch do something like:

import { useEffect, useState } from "react";

export interface Payload {
  data: null | string,
  loading: boolean
}

const useFetch = (url: string) => {
  const [state, setState] = useState<Payload>({data: null, loading: true})

  useEffect(() => {
    const interval = setInterval(() => {
        setState(state => ({ data: state.data, loading: true }));
        fetch(url)
          .then(x => x.text())
          .then(y => {
            setState({ data: y, loading: false });
          });
     }, 2000); // Something like 2s

     return () => {
        clearInterval(interval); // clear the interval when component unmounts
     }
  }, [url])

  return state;
  }

export default useFetch;

This will fetch the endpoint every 2s and update the state variable, this will then be reflected in the App (or any place that uses the hook for that matter).

Therefore you do not need the quote state anymore in the App. Also, you don't need a button in the UI anymore.

App will looks something like:

import React, { useState, useEffect } from "react";
import useFetch from './utils/useFetch'
import {Payload} from './utils/useFetch';

const App = () => {
  const quote = useFetch(
    "/api/rand"
  );

  return (
    <div>
      <h1>Quotes</h1>
      <div>{quote.data}</div>
    </div>
  )
}

export default App;

Fetch data upon mount of the component (currently happens) AND when the user taps the "Get Quote" button

Then the useFetch hook isn't really needed/suited in my opinion. A hook can be used for this application, but transforming it into a more simple function would make more sense to me. I suggest omitting the useFetch hook. The App component would look something like:

import React, { useState, useEffect, useCallback } from "react";

const App = () => {
  const [quote, setQuote] = useState<Payload>({data: null, loading: true})

  const handleGetQuote = useCallback(() => {
    setQuote(state => ({ data: state.data, loading: true }));
    fetch("/api/rand")
      .then(x => x.text())
      .then(y => {
        setQuote({ data: y, loading: false });
      });
  }, []);

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

  return (
    <div>
      <h1>Quotes</h1>
      <button onClick={handleGetQuote}>Get Quote</button>
      <div>{quote.data}</div>
    </div>
  )
}

export default App;

CodePudding user response:

Here's another possibility: I added a reload function to the return values of useFetch. Also, since the useFetch hook already contains its own state, I removed the useState from App

const useFetch = (url: string) => {
  const [state, setState] = useState<Payload>({data: null, loading: true})
  const [reloadFlag, setReloadFlag] = useState(0)

  useEffect(() => {
    if(reloadFlag !== 0){ // remove this if you want the hook to fetch data initially, not just after reload has been clicked
      setState(state => ({ data: state.data, loading: true }));
      fetch(url)
        .then(x => x.text())
        .then(y => {
          setState({ data: y, loading: false });
        });
    }
  }, [url, setState, reloadFlag])

  return [state, ()=>setReloadFlag((curFlag)=>curFlag   1)];
  }

const App = () => {

  const [data, reload] = useFetch(
    "/api/rand"
  );

  return (
    <div>
      <h1>Quotes</h1>
      <button onClick={reload}>Get Quote</button>
      <div>{quote.data}</div>
    </div>
  )
}

CodePudding user response:

The problem here is, that you are not refetching the quotes.

const useFetch = (url:string) => {
  const [loading, setLoading] = useState(false);
  const [quote, setQuote] = useState('');

  const refetch = useCallback(() => {
    setLoading(true);
    fetch(url)
      .then((x) => x.text())
      .then((y) => {
        //setQuote(y);
        setQuote(String(Math.random()));
        setLoading(false);
      });
  }, [url]);

  return { quote, refetch, loading };
};

const App = () => {
  const { quote, refetch, loading } = useFetch('/api/rand');

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

  return (
    <div>
      <h1>Quotes</h1>
      <button onClick={refetch}>Get Quote</button>
      {loading ? ' loading...' : ''}
      <div>{quote}</div>
    </div>
  );
};

See this example https://stackblitz.com/edit/react-ts-kdyptk?file=index.tsx

CodePudding user response:

Just change the code as below:

const useFetch = (url: string) => {
  const [state, setState] = useState<Payload>({data: null, loading: true})

  setState(state => ({ data: state.data, loading: true }));
    fetch(url)
      .then(x => x.text())
      .then(y => {
        setState({ data: y, loading: false });
      });

  return state;
  }

export default useFetch;
  • Related