I'm trying to build a game deals watcher and I been working on my browse page. I want to add a filter based on price and title in one fetch request. However, I'm not sure how to accomplish that in my case. Here's my Browse.jsx file:
import React, { useState, useEffect } from 'react';
function Browse({currentUser}) {
const [gameDealsList, setGameDealsList] = useState([]);
const [gameTitle, setTitle] = useState('')
// const [minPrice, setMinPrice] = useState('')
// const [maxPrice, setMaxPrice] = useState('')
const defaultURL = `https://www.cheapshark.com/api/1.0/deals?`
useEffect(()=>{
fetch(defaultURL)
.then((r)=>r.json())
.then((gameList)=> setGameDealsList(gameList))
},[])
console.log(gameDealsList)
function handleRedirect(e, dealID){
e.preventDefault();
window.open(`https://www.cheapshark.com/redirect?pageSize=10&dealID=${dealID}`, '_blank');
return null;
}
return(
<div className="container-fluid">
<h1>Browse</h1>
<h4>Filter:</h4>
<input placeholder='Title' value={gameTitle} onChange={(e)=>setTitle(e.target.value)}></input>
<span>Price Range $:</span>
<input
type="range"
className="price-filter"
min="0"
value="50"
max="100"
/>
<br/><br/>
{gameDealsList.map((game) =>
<div className="container" key={game.dealID}>
<div className="row">
<div className="col">
<img src={game.thumb} className="img-thumbnail" alt='thumbnail'/>
</div>
<div className="col">
<strong><p>{game.title}</p></strong>
</div>
<div className="col">
<span><s>${game.normalPrice}</s></span><br/>
<span>${game.salePrice}</span><br/>
<span>{Math.round(game.savings)}% Off</span>
</div>
<div className="col">
<button onClick={(e)=>handleRedirect(e, game.dealID)}>Visit Store</button>
</div>
<div className="col">
{currentUser ? <button>Add to wishlist</button> : null}
</div>
</div><br/>
</div>
)}
</div>
)
}
export default Browse;
Right now, I'm only fetching deals without any filters. However, the API allows me to set filters. For instance, if I want to search deals based on video game title I can just add &title={random title}. Also, I can type in &upperPrice={maximum price} to set up max price of deals. So, I would like to figure out ways to implement these filters in my fetch request without writing multiple fetch requests.
You can try this approach. Only 1 comment in the code to be worried about.
Another thing to worry is to add a Debounce to a fetch function, because without that requests will be sent every time variables in depsArray changed, so if i try to type Nights - 6 requests will be sent while im still typing.
In order to have everything working well:
Create some utils.js file in order to keep some shared helper functions. For debounce in our case.
utils.js
export function debounce(func, wait) {
let timeout;
return function (...args) {
const context = this;
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = null;
func.apply(context, args);
}, wait);
};
}
Import and wrap the function we want to debounce with useCallback and actual debounce:
import { debounce } from "./utils";
/* ... */
const fetchDeals = useCallback((queryObject) => {
const url = new URL(`https://www.cheapshark.com/api/1.0/deals`);
for (const [key, value] of Object.entries(queryObject)) {
if (value) url.searchParams.append(key, value);
}
console.log(url);
return fetch(url)
.then((r) => r.json())
.then((gameList) => setGameDealsList(gameList));
}, []);
const fetchDealsDebounced = useMemo(() => {
// So API call will not be triggered until 400ms passed since last
// action that may trigger api call
return debounce(fetchDeals, 400);
}, [fetchDeals]);
// Initial call will still be executed but only once, even if we have
// 3 items in depsArray (due to debounce)
useEffect(() => {
// Name object keys according to what API expects
fetchDealsDebounced({ title: gameTitle, upperPrice: maxPrice });
}, [fetchDealsDebounced, gameTitle, maxPrice]);
You should be able to append the query parameters directed by the API to your default query string
fetch(defaultURL + new URLSearchParams({
lowerPrice: minPrice,
upperPrice: maxPrice,
title: gameTitle,
}).then()...
As far as how to control this with only one request you could refactor useEffect like this.
useEffect(() => {
const queryParams = {
lowerPrice: minPrice,
upperPrice: maxPrice,
title: gameTitle,
};
if (minPrice === '') delete queryParams.lowerPrice;
if (maxPrice === '') delete queryParams.upperPrice;
if (gameTitle === '') delete queryParams.title;
fetch(defaultURL + new URLSearchParams(queryParams).then()...
}, [maxPrice, minPrice, gameTitle]);
Related
I'm trying to send a delete request to delete an item from an API.
The API request is fine when clicking on the button. But Item get's deleted only after refreshing the browser!
I'm not too sure if I should add any parameter to SetHamsterDeleted for it to work?
This is what my code looks like.
import React, {useState} from "react";
const Hamster = (props) => {
const [hamsterDeleted, setHamsterDeleted] = useState("")
async function deleteHamster(id) {
const response = await fetch(`/hamsters/${id}`, { method: "DELETE" });
setHamsterDeleted()
}
return (
<div>
<p className={props.hamster ? "" : "hide"}>
{hamsterDeleted}
</p>
<button onClick={() => deleteHamster(props.hamster.id)}>Delete</button>
<h2>{props.hamster.name}</h2>
<p>Ålder:{props.hamster.age}</p>
<p>Favorit mat:{props.hamster.favFood}</p>
<p>Matcher:{props.hamster.games}</p>
<img src={'./img/' + props.hamster.imgName} alt="hamster"/>
</div>
)
};
export default Hamster;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
Imagine you have a parent component (say HamstersList) that returns/renders list of these Hamster components - it would be preferable to declare that deleteHamster method in it, so it could either: a) pass some prop like hidden into every Hamster or b) refetch list of all Hamsters from the API after one got "deleted" c) remove "deleted" hamster from an array that was stored locally in that parent List component.
But since you are trying to archive this inside of Hamster itself, few changes might help you:
change state line to const [hamsterDeleted, setHamsterDeleted] = useState(false)
call setHamsterDeleted(true) inside of deleteHamster method after awaited fetch.
a small tweak of "conditional rendering" inside of return, to actually render nothing when current Hamster has hamsterDeleted set to true:
return hamsterDeleted ? null : (<div>*all your hamster's content here*</div>)
What do you want to do in the case the hamster is deleted? If you don't want to return anything, you can just return null.
I'm not too sure if I should add any parameter to SetHamsterDeleted for it to work?
Yes, I'd make this a boolean instead. Here's an example:
import React, { useState } from "react";
const Hamster = (props) => {
const [hamsterDeleted, setHamsterDeleted] = useState(false);
async function deleteHamster(id) {
const response = await fetch(`/hamsters/${id}`, { method: "DELETE" });
setHamsterDeleted(true);
}
if (hamsterDeleted) return null;
return (
<div>
<p className={props.hamster ? "" : "hide"}>
{hamsterDeleted}
</p>
<button onClick={() => deleteHamster(props.hamster.id)}>Delete</button>
<h2>{props.hamster.name}</h2>
<p>Ålder:{props.hamster.age}</p>
<p>Favorit mat:{props.hamster.favFood}</p>
<p>Matcher:{props.hamster.games}</p>
<img src={'./img/' + props.hamster.imgName} alt="hamster"/>
</div>
);
};
HOWEVER! Having each individual hamster keep track of its deleted state doesn't sound right (of course I don't know all your requirements but it seems odd). I'm guessing that you've got a parent component which is fetching all the hamsters - that would be a better place to keep track of what has been deleted, and what hasn't. That way, if the hamster is deleted, you could just not render that hamster. Something more like this:
const Hamsters = () => {
const [hamsers, setHamsters] = useState([]);
// Load the hamsters when the component loads
useEffect(() => {
const loadHamsters = async () => {
const { data } = await fetch(`/hamsters`, { method: "GET" });
setHamsters(data);
}
loadHamsters();
}, []);
// Shared handler to delete a hamster
const handleDelete = async (id) => {
await fetch(`/hamsters/${id}`, { method: "DELETE" });
setHamsters(prev => prev.filter(h => h.id !== id));
}
return (
<>
{hamsters.map(hamster => (
<Hamster
key={hamster.id}
hamster={hamster}
onDelete={handleDelete}
/>
))}
</>
);
}
Now you can just make the Hamster component a presentational component that only cares about rendering a hamster, eg:
const Hamster = ({ hamster, onDelete }) => {
const handleDelete = () => onDelete(hamster.id);
return (
<div>
<button onClick={handleDelete}>Delete</button>
<h2>{hamster.name}</h2>
<p>Ålder:{hamster.age}</p>
<p>Favorit mat:{hamster.favFood}</p>
<p>Matcher:{hamster.games}</p>
<img src={'./img/' + hamster.imgName} alt="hamster"/>
</div>
);
};
I have a custom hook in my React application which uses a GET request to fetch some data from the MongoDB Database. In one of my components, I'm reusing the hook twice, each using different functions that make asynchronous API calls.
While I was looking at the database logs, I realized each of my GET requests were being called twice instead of once. As in, each of my hooks were called twice, making the number of API calls to be four instead of two. I'm not sure why that happens; I'm guessing the async calls result in re-renders that aren't concurrent, or there's somewhere in my component which is causing the re-render; not sure.
Here's what shows up on my MongoDB logs when I load a component:
I've tried passing an empty array to limit the amount of time it runs, however that prevents fetching on reload. Is there a way to adjust the custom hook to have the API call run only once for each hook?
Here is the custom hook which I'm using:
const useFetchMongoField = (user, id, fetchFunction) => {
const [hasFetched, setHasFetched] = useState(false);
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
if (!user) return;
try {
let result = await fetchFunction(user.email, id);
setData(result);
setHasFetched(true);
} catch (error) {
setError(error.message);
}
};
if (data === null) {
fetchData();
}
}, [user, id, fetchFunction, data]);
return { data, hasFetched, error };
};
This is one of the components where I'm re-using the custom hook twice. In this example, getPercentageRead and getNotes are the functions that are being called twice on MongoDB (two getPercentageRead calls and two getNotes calls), even though I tend to use each of them once.
const Book = ({ location }) => {
const { user } = useAuth0();
const isbn = queryString.parse(location.search).id;
const { data: book, hasFetched: fetchedBook } = useFetchGoogleBook(isbn);
const { data: read, hasFetched: fetchedPercentageRead } = useFetchMongoField(
user,
isbn,
getPercentageRead
);
const { data: notes, hasFetched: fetchedNotes } = useFetchMongoField(
user,
isbn,
getNotes
);
if (isbn === null) {
return <RedirectHome />;
}
return (
<Layout>
<Header header="Book" subheader="In your library" />
{fetchedBook && fetchedPercentageRead && (
<BookContainer
cover={book.cover}
title={book.title}
author={book.author}
date={book.date}
desc={book.desc}
category={book.category}
length={book.length}
avgRating={book.avgRating}
ratings={book.ratings}
language={book.language}
isbn={book.isbn}
username={user.email}
deleteButton={true}
redirectAfterDelete={"/"}
>
<ReadingProgress
percentage={read}
isbn={book.isbn}
user={user.email}
/>
</BookContainer>
)}
{!fetchedBook && (
<Wrapper minHeight="50vh">
<Loading
minHeight="30vh"
src={LoadingIcon}
alt="Loading icon"
className="rotating"
/>
</Wrapper>
)}
<Header header="Notes" subheader="All your notes on this book">
<AddNoteButton
to="/add-note"
state={{
isbn: isbn,
user: user,
}}
>
<AddIcon color="#6b6b6b" />
Add Note
</AddNoteButton>
</Header>
{fetchedNotes && (
<NoteContainer>
{notes.map((note) => {
return (
<NoteBlock
title={note.noteTitle}
date={note.date}
key={note._noteID}
noteID={note._noteID}
bookID={isbn}
/>
);
})}
{notes.length === 0 && (
<NoNotesMessage>
You don't have any notes for this book yet.
</NoNotesMessage>
)}
</NoteContainer>
)}
</Layout>
);
};
The way you have written your fetch functionality in your custom hook useFetchMongoField you have no flag to indicate that a request was already issued and you are currently just waiting for the response. So whenever any property in your useEffect dependency array changes, your request will be issued a second time, or a third time, or more. As long as no response came back.
You can just set a bool flag when you start to send a request, and check that flag in your useEffect before sending a request.
It may be the case that user and isbn are not set initially, and when they are set they each will trigger a re-render, and will trigger a re-evalution of your hook and will trigger your useEffect.
I was able to fix this issue.
The problem was I was assuming the user object was remaining the same across renders, but some of its properties did in fact change. I was only interested in checking the email property of this object which doesn't change, so I only passed user?.email to the dependency array which solved the problem.
Using https://shrtco.de/docs/ for fetching my input data but it's continuously fetching it and adding to my displayLink array with different short url versions. I can add another link and it gets added to it but how do i stop it from duplicating or filter out a single originial link?
I get output like this screenshot of logged output & it keeps on adding to that displayLinks state with more same link but diff versions.
import { useForm } from "react-hook-form";
import axios from 'axios';
import Loading from '../../images/Ripple-1s-200px.svg'
const Shorten = () => {
// get built in props of react hook form i.e. register,handleSubmit & errors / watch is for devs
const { register, handleSubmit, formState: {errors} } = useForm();
//1. set user original values to pass as params to url
const [link, setLink] = useState('');
//2. set loader initial values to false
const [loading, setLoading] = useState(false);
//3. pass the fetched short link object into an array so we can map
const [displayLinks, setDisplayLinks] = useState([]);
//fetch the shortened url link using async method to show loading
useEffect(() => {
let unmounted = false;
async function makeGetRequest() {
try {
let res = await axios.get('https://api.shrtco.de/v2/shorten', { params: { url: link } });
//hid loader if u get response from api call
if (!unmounted && res.data.result.original_link !== displayLinks.original_link) {
setLoading(false);
//add the data to allLinks array to map
return setDisplayLinks(displayLinks => [...displayLinks, res.data.result]);
}
}
catch (error) {
console.log(error, "inital mount request with no data");
}
}
//invoke the makeGetRequest here
makeGetRequest();
return () => {
unmounted = true;
}
//passing dependency to re render on change of state value
}, [displayLinks, link]);
// onSubmit form log data into a variable
const onSubmit = (data, event) => {
event.preventDefault();
//puttin data in a variable to pass as url parameter if valid
setLink(data.userLink);
//add loading here after data is set to state
setLoading(!false);
}
return (
<div>
<form onSubmit={handleSubmit(onSubmit)}>
<label></label>
<input
{...register("userLink", {required: "Please add a link"})}
type="url"
id="userLink"
/>
{errors.userLink && <span>{errors.userLink.message}</span>}
<input type="submit" />
</form>
{
loading ?
<div className="loader" id="loader">
<img src={Loading} alt="Loading" />
</div>
: <div>
{
displayLinks.map((el) => {
return (
<div key={el.code}>
<div>
<h5>{el.original_link}</h5>
</div>
<div>
<h5>{el.full_short_link}</h5>
<button>Copy</button>
</div>
</div>
)
})
}
</div>
}
</div>
)
}
export default Shorten;
I am fairly new to react to please bare with me. I am creating a todo application that just keeps tracks of tasks.
Issue:
the state called [tasks, setTasks] = useState([]) does update when i try to edit a task within the tasks state.
For example lets say tasks contains ["make bed", "fold cloths"] and I wanted to update "fold cloths" to "fold clothes and put away". When I use setTasks the state does not update.
The modification of the state happens in the modifyTask function.
Here is my code:
import React, { useEffect, useState } from "react";
// Imported Components below
import CurrentTasks from "./CurrentTasks";
function InputTask() {
// Initial array for storing list items
// Tracking state of input box
const [task, setTask] = useState("");
const [tasks, setTasks] = useState(
JSON.parse(localStorage.getItem("tasks") || "[]")
);
function deleteTask(index) {
function checkIndex(task, i) {
if (i !== index) {
return task;
}
}
setTasks(prev => prev.filter(checkIndex));
}
function modifyTask(index) {
let editedTask = prompt("Please edit task here..", tasks[index]);
function editTask(task, i) {
if (i === index) {
task = editedTask;
}
console.log(task);
return task;
}
setTasks(prev => prev.filter(editTask));
}
function handleAddTask() {
setTasks(prev => prev.concat(task));
}
useEffect(() => {
// Saves every time tasks state changes
localStorage.setItem("tasks", JSON.stringify(tasks));
console.log("Tasks: " + tasks);
setTask("");
}, [tasks]);
return (
<div className="container">
<div className="input-group mt-4">
<input
type="text"
value={task}
onChange={e => setTask(e.target.value)}
className="form-control form-control-lg"
placeholder="Write task here..."
/>
<div className="input-group-append">
<button onClick={handleAddTask} className="btn btn-outline-secondary">
Add
</button>
</div>
</div>
<CurrentTasks
allTasks={tasks}
deleteTask={deleteTask}
modifyTask={modifyTask}
/>
</div>
);
}
export default InputTask;
What I think is happening
-> My theory is that the state only updates when the number of elements within the array changes. If thats the case is there an elegant way of doing so?
You just need to change your modify function to use map instead of filter, as filter doesn't change the array outside of removing elements. Map takes the returned values and creates a new array from them.
function modifyTask(index) {
let editedTask = prompt("Please edit task here..", tasks[index]);
function editTask(task, i) {
if (i === index) {
task = editedTask;
}
console.log(task);
return task;
}
setTasks(prev => prev.map(editTask));
}
I added the Nutrition file
const Nutrition = () => {
return(
<div>
<p>Label</p>
<p>Quantity</p>
<p>Unit</p>
</div>
)
}
export default Nutrition
I'm trying to map something in React but I'm getting this error map is not function. I'm trying to fetch an Api and now I'm trying to map another component to it, but the error is still there. Could someone help me or give me a hint
const ApiNutrition = () => {
const [nutritions, setNutritions] = useState([])
useEffect( () => {
getNutritions();
}, [])
const getNutritions = async () => {
const response = await fetch(`https://api.edamam.com/api/nutrition-data?app_id=${API_ID}&app_key=${API_KEY}&ingr=1%20large%20apple`)
const data = await response.json();
setNutritions(data.totalNutrientsKCal)
console.log(data.totalNutrientsKCal);
}
return(
<div>
<form className="container text-center">
<input classname="form-control" type="text" placeholder="CALORIES"/>
<button classname="form-control" type="submit">Submit</button>
</form>
{nutritions.map(nutrition => (
<Nutrition />
))}
</div>
)
}
export default ApiNutrition
From your code, I can see you have two places where you are setting the value of nutritions. One is while defining with useState(), and the other is after API call with setNutritions.
The error you are getting is map is not a function, it means somehow type of nutritions is not an array.
while defining with useState you are providing [] as default value so it means the error is with the API, the response you are getting from API which you are passing to setNutritions is not an array.
You can debug the API response type by typeof data.totalNutrientsKCal inside console.log