refetch data from API using react hooks - reactjs

I'm a complete beginner in react and I have written a fetch component which returns data from an API using a usefetch function . In my app I can manually change the input to get different data from the API but what I want is to have an input field and a button that when it is clicked it returns the new data from the API . With my code below I can fetch data only once when the component mounts and if i give input nothing happens .
import React , {useState ,useEffect} from 'react';
import useFetch from './fetch'; //fetch api code imported
import SearchIcon from '#material-ui/icons/Search';
import InputBase from '#material-ui/core/InputBase';
import Button from '#material-ui/core/Button';
function City(){
const searchStyle = {
display:"flex",
justifyContent:"flex-start",
position:"absolute",
top:"400px",
left:"40%",
}
const [inputVal , setInputVal] = useState(''); //store input value
const [place,setPlace] = useState('london'); //get london data from api by manually changing value new data is succesfully dislayed
const {loading , pics} = useFetch(place); //fetch data
const [images , setImages] = useState([]); //store fetched imgs
const removeImage = (id) =>{
setImages((oldState)=>oldState.filter((item)=> item.id !== id))
}
useEffect(()=>{
setImages(pics);
} , [pics] )
//load and display fetched images
return (<div className="city-info">
{
!loading ?
(images.length>0 && images.map((pic) =>{
return <div className="info" key = {pic.id}>
<span className="close" onClick= {()=>removeImage(pic.id)} >
<span
className="inner-x">
×
</span>
</span>
<img src = {pic.src.original} alt ="img"/>
<div style = {{position:"absolute" ,margin:"10px"}}>
<strong>From : </strong>
{pic.photographer}
</div>
</div>
})
):<div> Loading </div>
}
<div style = {searchStyle} >
<SearchIcon />
//when input changes store it
<InputBase onChange={(e)=>setInputVal(e.target.value)} placeholder="Enter input" style= {{backgroundColor:"lightgrey"}}/>
//new fetch data based on input by clicking on button nothing happens onclick
<Button onClick= {()=>setPlace(inputVal)} color="primary" variant = "contained" > Find </Button>
</div>
</div>);
}
export default City;
fetch.js my code to connect to api :
import { useState, useEffect } from 'react';
function useFetch(url){
const [loading ,setLoading] = useState(false);
const [query,setQuery] = useState(url);
const [pics,setPics] = useState([]);
const getPics = async()=>{
setLoading(true);
const response = await fetch(
`https://api.pexels.com/v1/search?query=${query}&per_page=4`,
{
method:"GET",
headers:{
Accept:"application/json",
Authorization:key
}
}
);
const result = await response.json();
setPics(result.photos ?? []);
setLoading(false);
}
useEffect(()=>{
getPics();
},[query]);
return {loading , pics ,query ,setQuery , getPics};
}
export default useFetch;
I think that my place value changes when my button is clicked but my fetch function is not reloaded and I just change a value .
I would really appreciate your help .

You can create a new useEffect and then add the place to the useEffect dependencies to create a side effect to call the API again once the value of the place variable changes:
// return the read function as well so you can re-fech the data whenever you need it
const {loading , pics, readData} = useFetch(place);
useEffect(() => {
readData(place);
setImages(pics)
}, [place]);
This will give you fresh data for each button click.

The problem is useFetch is storing the initial url passed into useState:
const [query,setQuery] = useState(url);
When place gets updated, useFetch never uses it and the effect is never going to be re-triggered. I think if you remove this state from useFetch completely, it ought to work as you expect:
import { useState, useEffect } from 'react';
function useFetch(url) {
const [loading, setLoading] = useState(false);
const [pics, setPics] = useState([]);
const getPics = async () => {
setLoading(true);
const response = await fetch(
`https://api.pexels.com/v1/search?query=${query}&per_page=4`,
{
method: "GET",
headers: {
Accept: "application/json",
Authorization: key
}
}
);
const result = await response.json();
setPics(result.photos ?? []);
setLoading(false);
}
useEffect(()=>{
getPics();
}, [url]);
return { loading, pics, getPics };
}
export default useFetch;

Related

Facing problem when fetching data using a fake API using useEffect

I am a beginner in react and trying a basic example where I am fetching data based on what I am passing in input field , the value in input field goes as id to the API URL , Everything is working fine but
this fails for a particular case . Suppose I entered 3 in input field and then click the button , First time the data shows as expected , but if I again press the get data button the UI just stuck showing Loading...
I think it is related to dependency array in useEffect.
import React,{useState,useEffect} from 'react'
const FetchData = () => {
const [data,setData]=useState({})
const [value,setValue]=useState(1)
const [id,setId]=useState(1)
const [loading,setLoading]=useState(true)
const idUpdater=()=>{
setLoading(true)
setId(value)
}
useEffect(()=>{
const fetchData= async()=>{
const data=await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`)
const response=await data.json()
setData(response)
setLoading(false)
}
setTimeout(fetchData,2000)
},[id])
return (
<div>
<input type="text" value={value} onChange={(e)=>setValue(e.target.value)} />
<button onClick={idUpdater}>Get Data</button>
{loading?<h1>Loading...</h1>:<h1>{data.title}</h1>}
</div>
)
}
export default FetchData
I tried removing the dependency array , but it didn't work
Based on the condition you have given the code in useEffect gets only rendered when the id is different. When you press the button again the id remains same thus the code inside useEffect won't run and the loading state set in idUpdater will remain true.
A better approach to this would be calling fetch on initial mount and reusing that function on button press, as:
import React, { useState, useEffect } from "react";
const FetchData = () => {
const [data, setData] = useState({});
const [value, setValue] = useState(1);
const [loading, setLoading] = useState(true);
const idUpdater = () => {
setLoading(true);
fetchData(value);
};
const fetchData = async (pageNo) => {
const data = await fetch(
`https://jsonplaceholder.typicode.com/posts/${pageNo}`
);
const response = await data.json();
setData(response);
setLoading(false);
};
useEffect(() => {
fetchData(1);
}, []);
return (
<div>
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<button onClick={idUpdater}>Get Data</button>
{loading ? <h1>Loading...</h1> : <h1>{data.title}</h1>}
</div>
);
};
export default FetchData;
Check demo
useEffect(()=>{
const fetchData= async()=>{
const data=await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`)
const response=await data.json()
setData(response)
setLoading(false)
}
setTimeout(fetchData,2000)
},[id])
The dependencies inside the square brackets means, this useEffect will be triggered everytime the id changes.
In your case, when re-clicking the Get data button, setLoading(true) runs make your ui runs into Loading... state, the setId(value) also runs but the value does not change (still 3) therefore the useEffect is not triggered

get the state of a function component for my url in getStaticProps

i want to fetch data from an input so i create a controlled component like this
import React, { useState } from "react";
import axios from 'axios'
type Props = {data:any};
function Locality({data}: Props) {
const [city, setCity] = useState("13000");
return(
<input
list="city"
type="text"
placeholder={`Code postale, France`}
onChange={(e) => {
setCity(e.target.value) ;
}}
/>
)
}
i want to fetch an url according to the city set in the state but i don't know how to give this state to my fetch below:
export async function getStaticProps(){
const getDataUrl:string = `https://geo.api.gouv.fr/communes?codePostal=${city}&format=geojson`
const result = await axios.get(getDataUrl)
const data = result.data
return {
props:{
data : data.data[0].attributes
}
}
}
any idea ?
nextjs getStaticProps is for getting build-time data on the server. React.useState is for managing run-time state, on the client
If fetching your data relies on some user interaction, try doing this on the client with useEffect and useState
function Locality({ data }) {
const [city, setCity] = useState("13007");
const [features, setFeatures] = useState({});
const fetchData = async () => {
const url = `https://geo.api.gouv.fr/communes?codePostal=${city}&format=geojson`;
const result = await axios.get(url);
const data = result.data.features;
setFeatures(data);
};
useEffect(() => {
fetchData();
}, [city]);
return (
<>
<input
value={city}
type="text"
placeholder={`Code postale, France`}
onChange={(e) => {
setCity(e.target.value);
}}
/>
<pre>{JSON.stringify(features, null, 2)}</pre>
</>
);
}
You might also want to investigate nextjs getServerSideProps
As pointed out by #ksav, you would need to use some local state, but also, an effect to fetch the data.
Here is an example how to do that: (untested, for the idea)
import React, { useState } from "react";
import axios from 'axios'
type Props = {data:any};
function Locality({data}: Props) {
const [city, setCity] = useState("13000");
// That will contain the result of the fetch
const [isFetching, setIsFetching] = useState(false)
const [result, setResult] = useState(null);
const [error, setError] = useState(null);
useEffect(function fetchResults() {
const getDataUrl:string = `https://geo.api.gouv.fr/communes?codePostal=${city}&format=geojson`
setIsFetching(true)
const result = axios.get(getDataUrl).then(res => {
const data = result.data
setResult(data.data[0].attributes)
}).catch(setError).finally(() => {
setIsFetching(false)
})
}
return(
<input
list="city"
type="text"
placeholder={`Code postale, France`}
onChange={(e) => {
setCity(e.target.value) ;
}}
/>{isFetching && 'Loading...'}
{!error && !isFetching &&
<div>Result for {city}: {result}</div>}
)
}
If you use the above effect that I or #ksav suggested, I suggest you look for XHR cancelation and effect debouncing, to avoid bugs that will occur as the user types in the box and many requests are sent at the same time. For example if I type '75000', it will send 5 requests, and if the 3rd request is the slowest, result can finally contain the result for 750, not 75000.

useState variable is called before useEffect API call

From what I understand useEffect hook runs last as a sideEffect. I am attempting to console log data.main.temp. I can understand that it doesn't know what that is yet, because it is fetching the data from the api in the useEffect hook which runs after.
How would I be able to access or console log data.main.temp AFTER the api call? (I feel like setTimout is the cheating way?)
import React, { useState, useEffect } from "react";
import Button from "../UI/Button";
import styles from "./Weather.module.css";
import moment from "moment";
import Card from "../UI/Card";
export default function Weather() {
//State Management//
const [lat, setLat] = useState([]);
const [long, setLong] = useState([]);
const [data, setData] = useState([]);
//openWeather API key
const key = "xxxxxxxxxxxxxxxxxxxxxxxxxx";
useEffect(() => {
const fetchData = async () => {
//get coordinates//
navigator.geolocation.getCurrentPosition(function (position) {
setLat(position.coords.latitude);
setLong(position.coords.longitude);
});
//fetch openWeather api//
await fetch(`https://api.openweathermap.org/data/2.5/weather/?lat=${lat}&lon=${long}&units=metric&APPID=${key}`)
.then((res) => res.json())
.then((result) => {
setData(result);
console.log(result);
});
};
fetchData();
}, [lat, long]);
//Examples of what I want, they run too early before api//
console.log(data.main.temp);
const Farenheit = data.main.temp * 1.8 + 32;
return (
<Card>
{typeof data.main != "undefined" ? (
<div className={`${styles.weatherContainer} ${styles.clouds}`}>
<h2>Weather</h2>
<p>{data.name}</p>
<p>{data.main.temp * 1.8 + 32} °F</p>
<p>{data.weather[0].description}</p>
<hr></hr>
<h2>Date</h2>
<p>{moment().format("dddd")}</p>
<p>{moment().format("LL")}</p>
</div>
) : (
<div></div>
)}
</Card>
);
}
You're right, the effect function is run after the first render which means you need to wait somehow until your api call is done. One common way to do so is to introduce another state flag which indicate whether the data is available or not.
Another thing which does not follow react good practices is the fact that you're effect function does more than one thing.
I also added trivial error handling and cleaned up mixed promises and async await
here is your refactored code
import React, { useState, useEffect } from "react";
import Button from "../UI/Button";
import styles from "./Weather.module.css";
import moment from "moment";
import Card from "../UI/Card";
//openWeather API key
const key = "xxxxxxxxxxxxxxxxxxxxxxxxxx";
export default function Weather() {
//State Management//
const [lat, setLat] = useState();
const [long, setLong] = useState();
const [data, setData] = useState();
const [error, setError] = useState();
const [loading, setLoading] = useState(false);
useEffect(() => {
navigator.geolocation.getCurrentPosition((position) => {
setLat(position.coords.latitude);
setLong(position.coords.longitude);
});
}, []);
useEffect(() => {
const fetchData = async () => {
if (lat && long && key) {
try {
setLoading(true);
const response = await fetch(
`https://api.openweathermap.org/data/2.5/weather/?lat=${lat}&lon=${long}&units=metric&APPID=${key}`
);
const data = await response.json();
setData(data);
setLoading(false);
} catch (err) {
setError(err);
setLoading(false);
}
}
};
fetchData();
}, [lat, long]);
if (error) {
return <div>some error occurred...</div>;
}
return (
<Card>
{loading || !data ? (
<div>loading...</div>
) : (
<div className={`${styles.weatherContainer} ${styles.clouds}`}>
<h2>Weather</h2>
<p>{data.name}</p>
<p>{data.main.temp * 1.8 + 32} °F</p>
<p>{data.weather[0].description}</p>
<hr></hr>
<h2>Date</h2>
<p>{moment().format("dddd")}</p>
<p>{moment().format("LL")}</p>
</div>
)}
</Card>
);
}
You can use another useEffect, which depends on changing the data state
useEfect(() => {
if (data) {
// do something with data
}
}, [data])
You can create a simple function and call it in your API call response and pass in the data directly from the api response, that way you will have access to the data immediately there's a response.
E.g
...
.then((result) => {
setData(result);
getDataValue(result) // this function will be called when the response comes in and you can use the value for anything
console.log(result);
});
METHOD 2:
You can use a useEffect hook to monitor changes in the data state, so that whenever there's an update on that state, you can use the value to do whatever you want. This is my less preferred option.
useEffect(() => {
//this hook will run whenever data changes, the initial value of data will however be what the initial value of the state is
console.log(data) //initial value = [] , next value => response from API
},[data])

multiple useEffect in a component doesn't work

When I call API from single useEffect, it works perfectly. But when I am trying to call another API from another useEffect in the same component its shows a error.
If it's possible, please have a look at my project on codesandbox.
import React, { useEffect, useState } from 'react';
import { Container, Row, Col } from 'react-bootstrap';
const TeacherDashboard = () => {
// console.log(props)
const [appointmentList, setAppointmentList] = useState([]);
const [viewProfile, setViewProfile] = useState([]);
console.log(viewProfile);
useEffect(() => {
async function loadData(){
const response = await fetch('http://localhost:4200/appointments')
const data = await response.json();
setAppointmentList(data)
}
loadData()
}, [appointmentList])
useEffect(() => {
async function proData() {
const response = await fetch('http://localhost:4200/schedule')
const data = await response.json();
setViewProfile(data)
}
proData()
}, [viewProfile])
return (
<Container>
<Row>
<Col>
{
appointmentList.map(app =>
<div style={{border: '1px solid blue'}}>
<li>Name : {app.name} </li>
<li>Id : {app.s_id} </li>
<li>Sec : {app.sec} </li>
<li>Email : {app.email} </li>
<li>Date & Time : {app.dateTime} </li>
</div>
)
}
</Col>
</Row>
</Container>
);
};
export default TeacherDashboard;
I am not sure the purpose of setting both appointmentList and viewProfile states as the part of the dependency arrays of both useEffect hooks. Both of them will eventually result in an infinite loop as you are directly updating the respective states in the useEffect hooks.
From what I can see, you only need to make both requests once, thus you should be using an empty array as the dependency array, such that both requests will be called only when the component is mounted. This is how it can be done:
useEffect(() => {
async function proData() {
const response = await fetch('http://localhost:4200/schedule')
const data = await response.json();
setViewProfile(data)
}
proData();
async function loadData(){
const response = await fetch('http://localhost:4200/appointments')
const data = await response.json();
setAppointmentList(data)
}
loadData();
}, []);

How to refetch data when field change with react hooks

My component fetches data by calling an hook-file which contains logic for requesting via API.
By default it will call the API without any extra parameter.
In GUI I also show an input where use can enter text.
Each time he writes a letter I want to refetch data. But Im not really sure how to do this with react and hooks.
I declared "useEffect". And I see that the content of the input changes. But what more? I cannot call the hook-function from there because I then get this error:
"React Hook "useFetch" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function react-hooks/rules-of-hooks"
This is the code:
hooks.js
import { useState, useEffect } from "react";
function useFetch(url) {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchUrl() {
const response = await fetch(url);
const json = await response.json();
setData(json);
setLoading(false);
}
fetchUrl();
}, [url]);
return [data, loading];
}
export { useFetch };
mycomponent.js
import React, { useState, useEffect } from 'react';
import { useFetch } from "../hooks";
const MyComponent = () => {
useEffect(() => {
console.log('rendered!');
console.log('searchTerm!',searchTerm);
});
const [searchTerm, setSearchTerm] = useState('');
const [data, loading] = useFetch(
"http://localhost:8000/endpoint?${searchTerm}"
);
return (
<>
<h1>Users</h1>
<p>
<input type="text" placeholder="Search" id="searchQuery" onChange={(e) => setSearchTerm(e.target.value)} />
</p>
{loading ? (
"Loading..."
) : (
<div>
{data.users.map((obj) => (
<div key={`${obj.id}`}>
{`${obj.firstName}`} {`${obj.lastName}`}
</div>
))}
</div>
)}
</>
);
}
export default MyComponent;
Create a function to handle your onChange event and call your fetch function from it. Something like this:
mycomponent.js
import React, { useState, useEffect } from 'react';
import { useFetch } from "../hooks";
const MyComponent = () => {
useEffect(() => {
console.log('rendered!');
console.log('searchTerm!',searchTerm);
});
const [searchTerm, setSearchTerm] = useState('');
const handleChange = e => {
setSearchTerm(e.target.value)
useFetch(
"http://localhost:8000/endpoint?${searchTerm}"
);
}
const [data, loading] = useFetch(
"http://localhost:8000/endpoint?${searchTerm}"
);
return (
<>
<h1>Users</h1>
<p>
<input type="text" placeholder="Search" id="searchQuery" onChange={(e) => handleChange(e)} />
</p>
{loading ? (
"Loading..."
) : (
<div>
{data.users.map((obj) => (
<div key={`${obj.id}`}>
{`${obj.firstName}`} {`${obj.lastName}`}
</div>
))}
</div>
)}
</>
);
}
export default MyComponent;
Your code works for me as per your requirement, type 1 or 2 in text box you will have different results.
So basically API get called once with default value of "searchTerm" and then it get called for each time by onChange.
try this at your local -
import React, { useState, useEffect } from "react";
function useFetch(url) {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchUrl() {
const response = await fetch(url);
const json = await response.json();
setData(json);
setLoading(false);
}
fetchUrl();
}, [url]);
return [data, loading];
}
export { useFetch };
const MyComponent = () => {
useEffect(() => {
console.log("rendered!");
console.log("searchTerm!", searchTerm);
});
const [searchTerm, setSearchTerm] = useState("");
const [data, loading] = useFetch(
`https://reqres.in/api/users?page=${searchTerm}`
);
return (
<>
<h1>Users</h1>
<p>
<input
type="text"
placeholder="Search"
id="searchQuery"
onChange={e => setSearchTerm(e.target.value)}
/>
</p>
{loading ? (
"Loading..."
) : (
<div>
{data.data.map(obj => (
<div key={`${obj.id}`}>
{`${obj.first_name}`} {`${obj.last_name}`}
</div>
))}
</div>
)}
</>
);
};
export default MyComponent;
The way your useFetch hook is setup it will only run once on load. You need to have it setup in a way you can trigger it from an effect function that runs only when searchTerm changes.
this is how you handle searching in react properly. It is better to have default searchTerm defined when user lands on your page, because otherwise they will see empty page or seening "loading" text which is not a good user experience.
const [data, setData] = useState([]);
const [searchTerm, setSearchTerm] = useState("defaultTerm")
In the first render of page, we should be showing the results of "defaultTerm" search to the user. However, if you do not set up a guard, in each keystroke, your app is going to make api requests which will slow down your app.
To avoid fetching data in each keystroke, we set up "setTimeout" for maybe 500 ms. then each time user types in different search term we have to make sure we clean up previous setTimeout function, so our app will not have memory leak.
useEffect(() => {
async function fetchUrl() {
const response = await fetch(url);
const json = await response.json();
setData(json);
}
// this is during initial rendering. we have default term but no data yet
if(searchTerm && !data){
fetchUrl();
}else{
//setTimeout returns an id
const timerId=setTimeout(()=>{
if(searchTerm){
fetchUrl}
},500)
// this where we do clean up
return ()=>{clearTimeout(timerId)}
}
}, [url]);
return [data, loading];
}
inside useEffect we are allowed to return only a function which is responsible for cleaning up. So right before we call useEffect again, we stop the last setTimeout.

Resources