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.
Related
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
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.
Here is a very basic search by title query on a public API. In my actual app my API calls are under services.js files so I'm trying to do this where my API call is not inside the react component.
https://codesandbox.io/s/elastic-pond-pghylu?file=/src/App.js
import * as React from "react";
import axios from "axios";
// services.js
const fetchPhotos = async (query) => {
const { data } = await axios.get(
`https://jsonplaceholder.typicode.com/photos?title_like=${query}`
);
return data;
};
export default function App() {
const [photos, setPhotos] = React.useState([]);
const [searchTerm, setSearchTerm] = React.useState("");
const fetch = React.useCallback(async () => {
const data = await fetchPhotos(searchTerm);
setPhotos(data);
}, [searchTerm]);
React.useEffect(() => {
fetch();
}, [fetch]);
return (
<div>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<div>
{photos?.map((photo) => (
<div>{JSON.stringify(photo.title)}</div>
))}
</div>
</div>
);
}
The problem with my code is this (too many api calls while im typing):
My attempt to fix this
I tried cancelToken. This cancels my previous request. I was able to implement this but in my actual API the get request is so fast that it still manages to finish the request. So I'm trying to do this without cancelToken.
I recently came across debouncing and it seems to do what I need i'm just struggling to get it to work
for example, I tried this: (I wrapped debounce onto my fetchPhotos function)
import {debounce} from 'lodash';
// services.js
const fetchPhotos = debounce(async (query) => {
const { data } = await axios.get(
`https://jsonplaceholder.typicode.com/photos?title_like=${query}`
);
return data;
}, 500);
however now fetchphotos returns undefined always?
You can make use of useCallback so that the debounced (fetchPhotos) will have same function across re-renders
import * as React from "react";
import axios from "axios";
import { debounce } from "lodash";
// services.js
export default function App() {
const [photos, setPhotos] = React.useState([]);
const [searchTerm, setSearchTerm] = React.useState("");
async function fetchData(searchTerm) {
const data = await axios.get(
`https://jsonplaceholder.typicode.com/photos?title_like=${searchTerm}`
);
setPhotos(data.data);
}
const debounced = React.useCallback(debounce(fetchData, 500), []);
React.useEffect(() => {
// for the first render load
fetchData("");
}, []);
return (
<div>
<input
type="text"
value={searchTerm}
onChange={(e) => {
setSearchTerm(e.target.value);
debounced(e.target.value, 1000);
}}
/>
<div>
{photos?.map((photo) => (
<div key={photo.id}>{JSON.stringify(photo.title)}</div>
))}
</div>
</div>
);
}
It's continuing to be rendered. I want to stop it.
Because of the problem, photos and text received from Api keep changing randomly.
I think useEffect is the problem. Please let me know because I am a beginner.
This is useFetch.jsx
import { useState, useEffect } from "react";
function useFetch(url) {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
async function fetchUrl() {
const response = await fetch(url);
const json = await response.json();
setData(json);
setLoading(false);
}
useEffect(() => {
fetchUrl();
});
return [data, loading];
}
export default useFetch;
This is Main.jsx
import React from "react";
import styled from "styled-components";
import useFetch from "./useFetch";
import "./font.css";
const url = "https://www.thecocktaildb.com/api/json/v1/1/random.php";
const Main = () => {
const [data, loading] = useFetch(url);
return (
<Wrapper>
<Header>My Cocktail Recipe</Header>
{loading ? (
"Loading..."
) : (
<>
{data.drinks.map(
({ idDrink, strDrink, strAlcoholic, strGlass, strDrinkThumb }) => (
<Container>
<img src={`${strDrinkThumb}`} alt="" />
<div key={`${idDrink}`}>{`${strDrink}`}</div>
</Container>
)
)}
</>
)}
<Search type="text" placeholder="검색하세요" val />
</Wrapper>
);
};
export default Main;
You must update useFetch.jsx:
import { useState, useEffect } from "react";
function useFetch(url) {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
async function fetchUrl() {
const response = await fetch(url);
const json = await response.json();
setData(json);
setLoading(false);
}
useEffect(() => {
fetchUrl();
}, []); //<--- Here
return [data, loading];
}
export default useFetch;
The problem is that the useEffect hook receives two arguments and you forgot the second argument, which is the effect's dependencies array.
useEffect(() => {
fetchUrl();
}, []);
return [data, loading];
}
Add Array Thanks Nick Parsons
I need to get data from server on changes in search input but I don't want to send a new request on every new character there so I'm trying use debounce from use-debounce package https://github.com/xnimorz/use-debounce. But my code below causes only endless requests before even any changes in search input happens.
App.js
import React, { useState, useEffect } from "react";
import axios from "axios";
import moment from "moment";
import { useDebounce } from "use-debounce";
import { Layout } from "./../Layout";
import { List } from "./../List";
import { Loader } from "./../Loader";
import { Header } from "./../Header";
import { Search } from "./../Search";
import { Licenses } from "./../Licenses";
import { Pagination } from "./../Pagination";
import "./App.css";
const PER_PAGE = 20;
export const App = () => {
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [hasError, setHasError] = useState(false);
const [nameSearch, setNameSearch] = useState("");
const [license, setLicense] = useState({});
const [currentPage, setCurrentPage] = useState(1);
const [total, setTotal] = useState(0);
const debouncedNameSearch = useDebounce(nameSearch, 2000);
const fetchData = async () => {
setHasError(false);
setIsLoading(true);
try {
const prevMonth = moment()
.subtract(30, "days")
.format("YYYY-MM-DD");
const licenseKey = (license && license.key) || "";
const url = `https://api.github.com/search/repositories?q=${nameSearch}+in:name+language:javascript+created:${prevMonth}${
licenseKey ? `+license:${licenseKey}` : ""
}&sort=stars&order=desc&page=${currentPage}&per_page=${PER_PAGE}`;
const response = await axios(url);
setData(response.data.items);
setTotal(response.data.total_count);
} catch (error) {
setHasError(true);
setData([]);
}
setIsLoading(false);
};
useEffect(() => {
fetchData();
}, [license, nameSearch, currentPage]);
return (
<Layout>
<Header>
<Search
handleNameSearchChange={setNameSearch}
nameSearch={nameSearch}
/>
<Licenses license={license} handleLicenseChange={setLicense} />
</Header>
<main>
{hasError && <div>Error...</div>}
{isLoading && <Loader />}
{data && !isLoading && !hasError && (
<>
<List data={data} />
<Pagination
currentPage={currentPage}
total={total}
itemsPerPage={PER_PAGE}
handlePageChange={setCurrentPage}
/>
</>
)}
</main>
</Layout>
);
};
Search.js
import React from "react";
import PropTypes from "prop-types";
export const Search = ({ handleNameSearchChange, nameSearch }) => (
<div className="flex-grow-1 mx-lg-3 mb-4 mb-lg-0">
<input
type="text"
name="search"
placeholder="Enter name..."
onChange={e => handleNameSearchChange(e.target.value)}
className="form-control"
value={nameSearch}
/>
</div>
);
Search.propTypes = {
nameSearch: PropTypes.string,
handleNameSearchChange: PropTypes.func
};
How to make debounce work properly?
You never refer to debouncedNameSearch.
I think the issue is with your useEffect:
useEffect(() => {
fetchData();
}, [license, nameSearch, currentPage]);
The first issue is that it will fire every time nameSearch changes, so you should change it to use debouncedNameSearch:
useEffect(() => {
fetchData();
}, [license, debouncedNameSearch, currentPage]);
You are also firing the request on initial render when debouncedNameSearch is an empty string, so you could wrap the call to fetchData in a conditional to prevent the request firing when debouncedNameSearch === "":
useEffect(() => {
if(debouncedNameSearch) {
fetchData();
}
}, [license, debouncedNameSearch, currentPage]);
Also, your request is using nameSearch when it should be using debouncedNameSearch:
const url = `https://api.github.com/search/repositories?q=${nameSearch}...
Change to:
const url = `https://api.github.com/search/repositories?q=${debouncedNameSearch}...
And it's recommended that any function that is declared inside a component and called inside a useEffect should either be declared inside the useEffect, or set as a dependency of that useEffect:
Read the docs: is it safe to omit functions from the list of dependencies?
So you can either do something like this:
useEffect(() => {
// Declare fetchData inside useEffect
const fetchData = async () => {...};
if (debouncedNameSearch) {
// Call it inside useEffect too
fetchData();
}
}, [
// Don't forget to add the function's dependencies
license,
debouncedNameSearch,
currentPage,
setHasError,
setIsLoading,
setData,
setTotal
]);
Or you can make the function itself a dependency of the useEffect, but you should wrap the function in a useCallback to make sure its state dependencies are up to date (as per the documentation linked above):
const fetchData = useCallback(
async () => {
// Function defined here
},
[ // function dependencies
setHasError,
setIsLoading,
license,
debouncedNameSearch,
currentPage,
setData,
setTotal,
setHasError
]
);
useEffect(() => {
if(debouncedNameSearch) {
fetchData();
}
}, [license, debouncedNameSearch, currentPage, fetchData]); // Add as dependency