How to add debouncing to my simple search query? - reactjs

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>
);
}

Related

How to display data from Mongodb in React using Axios

I'm facing difficulty displaying data in React - Here is my code:
import Axios from 'axios';
import { useNavigate } from 'react-router';
export default function ProductCatalog() {
let navigate = useNavigate();
function addProduct() {
navigate('/adding')
}
const [products, setProducts] = useState([{}])
useEffect(() => {
const axiosProd = async () => {
const response = await Axios('http://localhost:3001/getProducts');
setProducts(response.data)
};
axiosProd();
}, []);
const useProducts = products.map((product)=>{
return <div>
<h1>{product.name}</h1>
</div>
})
return(
<>
<button className = "button" onClick={addProduct}>Add New Product</button>
<br></br>
{useProducts}
</>
)
}
I know data is coming in as JSON Objects as when i follow the link of http://localhost:3001/getProducts, I see my data. What am i doing wrong?
You should make a function then outside of the function call the use effect.
To do a get request using axios use axios.get(api)
For example:
// Get All Shoes
const getShoes = () => {
axios.get('/shoes')
.then(res => setShoes(res.data))
.catch(err => console.log(err));
}
Then
useEffect(() => {
getShoes();
}, [])

TypeError: weatherData.map is not a function

I'm trying to map over data from API, but while writing the code to display the data I got this error: TypeError: weatherData.map is not a function
I tried removing useEffect from the code and tried to add curly brackets: const [weatherData, setWeatherData] = useState([{}])
Update: Line 14 log undefined : console.log(weatherData.response)
import axios from 'axios'
import { useEffect, useState } from 'react'
import './App.css'
function App() {
const [search, setSearch] = useState("london")
const [weatherData, setWeatherData] = useState([])
const getWeatherData = async () => {
try {
const weatherData = await axios.get(`https://api.openweathermap.org/data/2.5/weather?q=${search}&appid={APIKEY}`);
console.log(weatherData.response);
if (weatherData) {
setWeatherData(weatherData);
}
} catch (err) {
console.error(err);
}
}
useEffect(() => {
getWeatherData()
}, [getWeatherData])
const handleChange = (e) => {
setSearch(e.target.value)
}
return (
<div className="App">
<div className='inputContainer'>
<input className='searchInput' type="text" onChange={handleChange} />
</div>
{weatherData.map((weather) => {
return (
<div>
<h1>{weather.name}, {weather.country}</h1>
</div>
)
})}
</div>
)
}
export default App
You're having errors in fetching the data as well as rendering it.
Just change the entire App component like this :
import { useEffect, useState } from "react";
import axios from "axios";
function App() {
const [search, setSearch] = useState("London");
const [weatherData, setWeatherData] = useState([]);
const APIKEY = "pass your api key here";
useEffect(() => {
const fetchData = async () => {
const result = await axios(
`https://api.openweathermap.org/data/2.5/weather?q=${search}&appid=${APIKEY}`
);
setWeatherData(result.data);
};
fetchData();
}, [search]);
const handleChange = (e) => {
setSearch(e.target.value);
};
return (
<div className="App">
<div className="inputContainer">
<input className="searchInput" type="text" onChange={handleChange} />
</div>
<h1>
{" "}
{weatherData.name} ,{" "}
{weatherData.sys ? <span>{weatherData.sys.country}</span> : ""}{" "}
</h1>
</div>
);
}
export default App;
this should be working fine just make sure to change : const APIKEY = "pass your api key "; to const APIKEY = "<your API key> ";
this is a demo in codesandbox
Create a promise function:
const getWeatherData = async () => {
try {
const weatherData = await axios.get(`https://api.openweathermap.org/data/2.5/weather?q=${search}&appid={APIKEY}`);
console.log(weatherData.response);
if (weatherData.response.data) {
setWeatherData(weatherData.response.data);
}
} catch (err) {
console.error(err);
}
}
Then call it.

How to pass data from child component to parent component?

I am currently learning React, and I am trying to build a small weatherapp to practice with apis, axios and react generally. I built an input component where it's duty is getting the data from the API, and I am holding the data in the useState hook and I want to use the data in the main App component? I am able to pass data from parent App component to input component if I take the functionality in the app component, but this time I start to have problems with input text rendering problems. Here is the code:
this is the input component where I search and get the data from the API, and I am trying to pass the weatherData into the main App component and render it there. How is it possible to achieve this?
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const key = process.env.REACT_APP_API_KEY;
function SearchLocation() {
const [text, textChange] = useState('');
const [weatherData, setWeatherData] = useState([]);
const handleText = (e) => {
textChange(e.target.value);
};
const fetchData = async () => {
const { data } = await axios.get(
`https://api.weatherapi.com/v1/current.json`,
{
params: {
key: key,
q: text,
lang: 'en',
},
}
);
setWeatherData(data);
};
useEffect(() => {
try {
fetchData();
} catch (error) {
console.log(error);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [text]);
console.log(weatherData);
return (
<div>
<form>
<input
onChange={handleText}
className="locationInput"
type="text"
value={text}
required
></input>
</form>
</div>
);
}
export default SearchLocation;
EDIT:
After moving the states to main component and passing them to children as props I receive 3 errors, GET 400 error from the API, createError.js:16 Uncaught (in promise) Error: Request failed with status code 400 and textChange is not a function error. Here are how components look like. This is the input component:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const key = process.env.REACT_APP_API_KEY;
function SearchLocation({ weatherData, setWeatherData, text, textChange }) {
const handleText = (e) => {
textChange(e.target.value);
};
const fetchData = async () => {
const { data } = await axios.get(
`https://api.weatherapi.com/v1/current.json`,
{
params: {
key: key,
q: text,
lang: 'en',
},
}
);
setWeatherData(data);
};
useEffect(() => {
try {
fetchData();
} catch (error) {
console.log(error);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [text]);
console.log(weatherData);
return (
<div>
<form>
<input
onChange={handleText}
className="locationInput"
type="text"
value={text}
required
></input>
</form>
</div>
);
}
export default SearchLocation;
this is the parent app component:
import React from 'react';
import { useState } from 'react';
import './App.css';
import './index.css';
import SearchLocation from './components/Input';
function App() {
const [weatherData, setWeatherData] = useState([]);
const [text, textChange] = useState('');
return (
<div className="App">
<SearchLocation
setWeatherData={setWeatherData}
lastData={weatherData}
inputText={text}
/>
</div>
);
}
export default App;
You'll still need to store the state in the parent component. Pass the setter down as a prop. This is a React pattern called Lifting State Up.
Example:
const App = () => {
const [weatherData, setWeatherData] = useState([]);
...
return (
...
<SearchLocation setWeatherData={setWeatherData} />
...
);
};
...
function SearchLocation({ setWeatherData }) {
const [text, textChange] = useState('');
const handleText = (e) => {
textChange(e.target.value);
};
const fetchData = async () => {
const { data } = await axios.get(
"https://api.weatherapi.com/v1/current.json",
{
params: {
key,
q: text,
lang: 'en',
},
}
);
setWeatherData(data);
};
useEffect(() => {
try {
// Only request weather data if `text` is truthy
if (text) {
fetchData();
}
} catch (error) {
console.log(error);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [text]);
return (
<div>
<form>
<input
onChange={handleText}
className="locationInput"
type="text"
value={text}
required
/>
</form>
</div>
);
}
There are two solutions to your problem:-
Firstly you can create the states const [text, textChange] = useState('');
const [weatherData, setWeatherData] = useState([]);, inside your parent component and pass text, textChange, weatherData, setWeatherData as props to your child component.
I would recommend the second way, i.e, implement redux for this and store text, and weatherData into your redux and try to access them from redux.
redux reference:- https://react-redux.js.org/introduction/getting-started

Simple function that retrieves data from an API is not returning the data

I have this React component that used to return an HTML element like this:
const PartsList = () => {
const [data, setData] = useState([]);
useEffect(() => {
const fetchData = async () => {
const result = await axios(
'https://localhost:44376/api/parts',
);
setData(result.data);
};
fetchData();
}, []);
return (
<>
{data.map((item, index) => (
<label key={index} className="inline">
<Field key={index} type="checkbox" name="machineParts" value={item.id} />
{item.name}
</label>
))}
</>
);
}
export default PartsList;
Now, I want it to return only an array of JSON, no HTML.
So I tried modifying the component so that it looks like this:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const [data, setData] = useState([]);
useEffect(() => {
const fetchData = async () => {
const result = await axios(
'https://localhost:44376/api/machines',
);
setData(result.data);
console.log("data as seen in function: ", JSON.stringify(result, null, 2));
};
fetchData();
}, []);
return data;
When I write it out to the console in this function, I see all the needed data.
But when I write it out to the console in the main App.js, I just see undefined.
What could I be doing wrong?
Thanks!
Originally you wanted a component because it had to render HTML.
Now what you actually need is to move everything out to a function.
So you can do this in your main App.js:
import React from 'react';
import axios from 'axios';
const fetchData = async () => {
const result = await axios(
'https://localhost:44376/api/machines',
);
return JSON.stringify(result, null, 2);
};
const App = () => {
const result = await fetchData()
console.log(result)
return <div>Main App<div>
}
export default App
This is how you make a function to return data that you can call to see the console result in your main App component.
This obviously just demonstrates the concept, you can take it further by moving that function out to its own file that you can import into your App.js folder.

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