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
Related
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.
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])
Currently, my code re-renders every time the query parameter is updated. Once I remove the query parameter; however, I get a warning stating "React Hook useCallback has a missing dependency: 'query'. Either include it or remove the dependency array react-hooks/exhaustive-deps". I have tried just defining my getData function within the useEffect, but I am using getData as on onclick function outside of the useEffect. What I am trying to accomplish is to initially fetch articles on react hooks and then only fetch new data on submit as opposed to when the query is updated and not have any warnings about query being a missing dependency as well. Any suggestions would help immensely. the code is as follows:
import React, { useState, useEffect, useCallback } from "react"
import axios from "axios"
const Home = () => {
const [data, setData] = useState(null)
const [query, setQuery] = useState("react hooks")
const getData = useCallback(async () => {
const response = await axios.get(
`http://hn.algolia.com/api/v1/search?query=${query}`
)
setData(response.data)
}, [query])
useEffect(() => {
getData()
}, [getData])
const handleChange = event => {
event.preventDefault()
setQuery(event.target.value)
}
return (
<div>
<input type='text' onChange={handleChange} value={query} />
<button type='button' onClick={getData}>
Submit
</button>
{data &&
data.hits.map(item => (
<div key={item.objectID}>
{item.url && (
<>
<a href={item.url}>{item.title}</a>
<div>{item.author}</div>
</>
)}
</div>
))}
</div>
)
}
export default Home
Add a submitting state as a condition for triggering your axios request
const [submitting, setSubmitting] = useState(true)
const [data, setData] = useState(null)
const [query, setQuery] = useState("react hooks")
useEffect(() => {
const getData = async () => {
const response = await axios.get(
`http://hn.algolia.com/api/v1/search?query=${query}`
)
setData(response.data)
setSubmitting(false) // call is finished, set to false
}
// query can change, but don't actually trigger
// request unless submitting is true
if (submitting) { // is true initially, and again when button is clicked
getData()
}
}, [submitting, query])
const handleChange = event => {
event.preventDefault()
setQuery(event.target.value)
}
const getData = () => setSubmitting(true)
If you wanted to useCallback, it could be refactored as such:
const [submitting, setSubmitting] = useState(true)
const [data, setData] = useState(null)
const [query, setQuery] = useState("react hooks")
const getData = useCallback(async () => {
const response = await axios.get(
`http://hn.algolia.com/api/v1/search?query=${query}`
)
setData(response.data)
}, [query])
useEffect(() => {
if (submitting) { // is true initially, and again when button is clicked
getData().then(() => setSubmitting(false))
}
}, [submitting, getData])
const handleChange = event => {
event.preventDefault()
setQuery(event.target.value)
}
and in render
<button type='button' onClick={() => setSubmitting(true)}>
So I am working on a small personal project that is a todo app basically, but with a backend with express and mongo. I use useEffect() to make an axios get request to the /all-todos endpoint that returns all of my todos. Then inside of useEffect()'s dependency array I specify the todos array as dependency, meaning that I want the useEffect() to cause a rerender anytime the tasks array gets modified in any way. Take a look at this:
export default function () {
const [todos, setTodos] = useState([]);
const currentUser = JSON.parse(localStorage.getItem('user'))._id;
useEffect(() => {
function populateTodos () {
axios.get(`http://localhost:8000/api/all-todos/${currentUser}`)
.then(res => setTodos(res.data))
.catch(err => console.log(err));
}
populateTodos();
}, [todos]);
console.log(todos);
return (
<div className="App">
...
</div>
);
}
So I placed that console.log() there to give me a proof that the component gets rerendered, and the problem is that the console.log() gets printed to the console forever, until the browser gets slower and slower.
How can I make it so that the useEffect() gets triggered obly when todos gets changed?
You should execute the hook only if currentUser changes:
export default function () {
const [todos, setTodos] = useState([]);
const [error, setError] = useState(null);
const currentUser = JSON.parse(localStorage.getItem('user'))._id;
useEffect(() => {
function populateTodos () {
axios.get(`http://localhost:8000/api/all-todos/${currentUser}`)
.then(res => setTodos(res.data))
.catch(err => setError(err));
}
populateTodos();
}, [currentUser]);
console.log(todos);
if (error) return (
<div className="App">
There was an error fetching resources: {JSON.stringify(error)}
</div>
)
return (
<div className="App">
...
</div>
);
}
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.