I'm trying to display an array with 151 pokemons sorted by their pokedex positions (ex: 1 - bulbasaur, 2- ivysaur...), but every time I reload the page it brings me a different array, with the positions shuffled.
App.js:
import './App.css';
import { useEffect, useState } from 'react';
import Pokemon from './components/Pokemon';
const App = () => {
const [pokemonData, setPokemonData] = useState([]);
const fetchPokemons = () => {
for (let index = 1; index < 152; index += 1) {
new Array(151).fill(
fetch(`https://pokeapi.co/api/v2/pokemon/${index}`)
.then((data) => data.json())
.then((result) =>
setPokemonData((prevState) => [...prevState, result])
)
);
}
};
useEffect(() => {
fetchPokemons();
}, []);
return (
<>
{pokemonData && <Pokemon data={pokemonData} />}
</>
);
};
export default App;
Pokemon.js:
const Pokemon = ({ data }) => {
return (
<section>
{data.map((pokemon) => (
<div key={pokemon.name}>
<img src={pokemon.sprites.front_default} alt={pokemon.name} />
<span>{pokemon.name}</span>
<span>
{pokemon.types.length >= 2
? `${pokemon.types[0].type.name} ${pokemon.types[1].type.name}`
: pokemon.types[0].type.name}
</span>
</div>
))}
</section>
);
};
export default Pokemon;
If you want to guarantee the order, you'll need something like
const fetchPokemons = async () => {
const promises = [];
for (let index = 1; index < 152; index += 1) {
promises.push(fetch(`https://pokeapi.co/api/v2/pokemon/${index}`).then((data) => data.json()));
}
const results = await Promise.all(promises);
setPokemonData(results);
};
This will take a while as it loads all of the pokémon before showing any of them - if you don't want that, then there really are two options: rework things to do each request sequentially, or alternately switch to an array where some of the slots may be null while things are still being loaded (which will require changing your rendering code some too).
You are making 153 calls to api, which is not great I would highly recommend that you change into single api call to get all pokemons, to achieve this you can do it like this:
const [pokemonData, setPokemonData] = useState<any>([]);
const fetchPokemons = async () => {
const data = await fetch(`https://pokeapi.co/api/v2/pokemon?offset=0&limit=153"`);
const pokemons = await data.json();
setPokemonData(pokemons.results);
};
useEffect(() => {
(async () => {
await fetchPokemons();
})();
}, []);
Also this will guarantee that you get data always in the same way. You will not face any race conditions and you won't any unnecessary api calls.
Related
I need to add sorting to fetched data (ascending/descending).
I get all the data from API endpoint. I map every object in that array to be displayed in separate component card. But once I choose to sort data from Descending name I get a quick change of components were they are sorted from Z to A but it just instantly converts back to initial fetched state (from A to Z).
Could you please tell me where the problem is? I don't know why but it feels like sorted array doesn't get saved in state "data" which I use to map all the cards.
import { useState } from 'react';
import { useEffect } from 'react';
import './styles/main.scss';
import Card from './components/Card/Card';
import { v4 as uuidv4 } from 'uuid';
function App() {
const [data, setData] = useState([]);
const [sortType, setSortType] = useState('default');
useEffect(() => {
fetchData();
sortData();
}, [sortType]);
const fetchData = async () => {
const response = await fetch(
'https://restcountries.com/v2/all?fields=name,region,area'
);
const data = await response.json();
setData(data);
};
function sortData() {
let sortedData;
if (sortType === 'descending') {
sortedData = [...data].sort((a, b) => {
return b.name.localeCompare(a.name);
});
} else if (sortType === 'ascending') {
sortedData = [...data].sort((a, b) => {
return a.name.localeCompare(b.name);
});
} else {
return data;
}
setData(sortedData);
}
return (
<div className='content'>
<header className='content__header'>
<h1>Header placeholder</h1>
</header>
<div className='wrapper'>
<div className='wrapper__sort-buttons'>
<select
defaultValue='default'
onChange={(e) => setSortType(e.target.value)}
>
<option disabled value='default'>
Sort by
</option>
<option value='ascending'>Ascending</option>
<option value='descending'>Descending</option>
</select>
</div>
<ul className='wrapper__list'>
{data.map((country) => {
country.key = uuidv4();
return (
<li key={country.key}>
<Card
name={country.name}
region={country.region}
area={country.area}
/>
</li>
);
})}
</ul>
</div>
</div>
);
}
export default App;
This is what I get just for a quick moment:
And then it just goes back to initial state:
It appears the way you're using useEffect is causing your component to refetch the data each time you change the sort type. This could be causing a race condition due to multiple places updating your data state at different times.
I would move the sorting logic into a useMemo and only fetch the data in useEffect on initial load:
import { useEffect, useMemo, useState } from "react";
import './styles/main.scss';
import Card from './components/Card/Card';
import { v4 as uuidv4 } from "uuid";
function App() {
const [data, setData] = useState([]);
const [sortType, setSortType] = useState("default");
// Move sort logic here...
const sortedData = useMemo(() => {
let result = data;
if (sortType === "descending") {
result = [...data].sort((a, b) => {
return b.name.localeCompare(a.name);
});
} else if (sortType === "ascending") {
result = [...data].sort((a, b) => {
return a.name.localeCompare(b.name);
});
}
return result;
}, [data, sortType]);
// Only fetch data once on component mount...
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
const response = await fetch(
"https://restcountries.com/v2/all?fields=name,region,area"
);
const data = await response.json();
setData(data);
};
return (
<div className="content">
<header className="content__header">
<h1>Header placeholder</h1>
</header>
<div className="wrapper">
<div className="wrapper__sort-buttons">
<select
defaultValue="default"
onChange={(e) => setSortType(e.target.value)}
>
<option disabled value="default">
Sort by
</option>
<option value="ascending">Ascending</option>
<option value="descending">Descending</option>
</select>
</div>
<ul className="wrapper__list">
{/* Use sortedData here instead of data... */}
{sortedData.map((country) => {
country.key = uuidv4();
return (
<li key={country.key}>
<Card
name={country.name}
region={country.region}
area={country.area}
/>
</li>
);
})}
</ul>
</div>
</div>
);
}
export default App;
Here's a basic example in a Codesandbox (I commented out your styles/card component): https://codesandbox.io/s/goofy-tdd-8lio9?file=/src/App.js
This might be happening for the reason that set state function is asynchronous in nature and the order in which setData is being called is different than you expect.
So, for the initial call with sortType 'default', you are not noticing any change as you are returning the data as it is. But once you change it to 'descending', setData() from sortData() is called earlier than that from fetchData() so as you have already data in your state, you see a change in data in UI for few moments, but then setData() from the function fetchData is called and replaces your data with the one you got from the API call which is unsorted or in ascending order.
POSSIBLE SOLUTION
DON'T set the state inside fetchData method, rather just set it once inside the sortData method, as you are needing it anyhow.
So your code will look something like this:
// we will call sortData inside fetchData so remove it from here
useEffect(() => {
fetchData();
}, [sortType]);
const fetchData = async () => {
const response = await fetch(
'https://restcountries.com/v2/all?fields=name,region,area'
);
const data = await response.json();
// using API response data as an input to sortData function
sortData(data)
};
// using data from parameter instead of state
function sortData(data) {
let sortedData;
if (sortType === 'descending') {
sortedData = [...data].sort((a, b) => {
return b.name.localeCompare(a.name);
});
} else if (sortType === 'ascending') {
sortedData = [...data].sort((a, b) => {
return a.name.localeCompare(b.name);
});
} else {
return data;
}
setData(sortedData);
}
IMPROVEMENT
Your API call is not depending upon the SORTING ORDER, so you don't need to call the API again and again, just call it once, and then sort the data on the value changed from dropdown.
// call the API on initial load only
useEffect(() => {
fetchData();
}, []);
// and on sortType change you can handle it like this:
useEffect(() => {
sortData(data);
}, [sortType]);
// and using this approach you can use the exact same code for both functions implementation that you posted in your question above.
I have an array of country codes and I need to have the name.
I am trying to access the countries data from the state (axios call) and from there filter by country code, and from that new array, extract the common name of the country.
(I am using the restcountries.com api).
-If I create a new state to map from, I get the too many re-renders.
-Right now, Although the border countries info is there, I can't access it, I get the "Cannot read properties of undefined" error, that usually is tied to a lifecycle issue, therefore I am using a condition on when to access the information.
Still I am not able to get it stable and return the name that I need.
Can someone please take a look and tell me what am I doing wrong?
Thanks in advance
import axios from "axios";
const BorderCountries = (props) => {
const [countriesList, setCountriesList] = useState([]);
useEffect(() => {
axios
.get(`https://restcountries.com/v3.1/all`)
.then((countries) => setCountriesList(countries.data))
.catch((error) => console.log(`${error}`));
}, []);
const getCountryName = () => {
const codes = props.data;
const borderCountries = [];
codes.map((code) => {
const borderCountry = countriesList.filter((country) =>
country.cca3.includes(code)
);
borderCountries.push(borderCountry);
});
// console.log(borderCountries);
if (props.data.length === borderCountries.length) {
const borderName = borderCountries.map((border) =>
console.log(border[0].name.common)
);
return borderName
}
};
return (
<div>
<h3>Border Countries:</h3>
{getCountryName()}
</div>
);
};
export default BorderCountries;
const getCountryName = () => {
const codes = props.data;
if(countriesList.length === 0) return <></>;
const borderCountries = [];
codes.map((code) => {
const borderCountry = countriesList.filter((country) =>
country.cca3.includes(code)
);
borderCountries.push(borderCountry);
});
// console.log(borderCountries);
if (props.data.length === borderCountries.length) {
const borderName = borderCountries.map((border) =>
console.log(border[0].name.common)
);
return borderName
}
};
Try this, you forgot to wait for the call to finish.
Below is my App.js component-
import { useEffect, useState } from 'react'
import Cart from './Cart'
import Navbar from './Navbar'
import firebase from './firebase'
function App() {
const [products, setProducts] = useState([])
useEffect(()=>{
const ref = firebase.firestore().collection('products');
ref.onSnapshot((snapshot) => {
const items = [];
snapshot.forEach((doc) =>{
const data = doc.data();
items.push(data)
data['id'] = doc.id
})
setProducts(items);
})
},[]);
const handleIncreaseQuantity = (product) =>{
const index = products.indexOf(product)
products[index].qty += 1;
setProducts([...products])
}
const handleDecreaseQuantity = (product) =>{
const index = products.indexOf(product)
if(products[index].qty > 0){
products[index].qty -= 1;
setProducts([...products])
}
}
const handleDeleteQuantity = (product) =>{
// const productId = product.id
// const newProducts = products.filter((item) => (item.id !== productId))
// setProducts([...newProducts])
// console.log(newProducts)
// console.log(product.prototype)
firebase.firestore().collection('products').doc(product.id).remove()
console.log(`product id: ${product.id}`)
}
const getCartCount = () =>{
var count = 0;
products.forEach((item) => (
count += item.qty
))
return count;
}
const getCartTotal = () =>{
var count = 0;
products.map((product) =>(
count += product.qty*product.price
))
return count;
}
return (
<div className="App">
<Navbar count = {getCartCount()}/>
<Cart
products = {products}
onIncreaseQty = {handleIncreaseQuantity}
onDecreaseQty = {handleDecreaseQuantity}
onDeleteQty = {handleDeleteQuantity}
/>
<div className="cart-total">
TOTAL: {getCartTotal()}
</div>
</div>
);
}
export default App;
I want my app to function in a way that whenever I click the delete icon(see the image at the last), the item is deleted from the database. I just don't know how to do it. I am using .remove() (see handleDeleteQuantity() function) but it's throwing an error that .remove is not a function. Or maybe I don't know exactly what code goes in handleDeleteQuantity() to remove the item.
The .remove() method is used to delete data from realtime database. Firestore's documentation says, "To delete a document, use the delete() method." Also try awaiting that method in an async function or just handle the promise using .then and .catch.
const handleDeleteQuantity = (product) => {
firebase.firestore().collection('products').doc(product.id).delete().then(() => {
console.log(`product id: ${product.id}`)
}).catch((e) => console.log(e))
}
Deleting a document does not delete its subcollections!
i don't know how make this guys, i can't update my state with the api array, and if i put it in useEffect i have an error cause i am not sending any data, help me please is my first time using stackoverflow
import React, { useEffect, useState } from "react";
import getTeam from "../Helpers/getTeam";
const selectTeams = [
"Barcelona",
"Real Madrid",
"Juventus",
"Milan",
"Liverpool",
"Arsenal",
];
const Select = () => {
const [team, setTeam] = useState(null);
const [loading, setLoading] = useState(null);
const handleOption = async (e) => {
setLoading(true);
let teamsJson = await getTeam(e.target.value);
let arr = [];
Object.keys(teamsJson).map((teamjs, i) => {
return arr.push(teamsJson[teamjs]);
});
console.log(arr);
console.log(team);
setTeam(arr);
setLoading(false);
};
return (
<div
style={{ background: "skyblue", textAlign: "center", padding: "20px" }}
>
<h1>Equipos Disponibles</h1>
<div>
<select onChange={handleOption}>
<option>Elige tu equipo</option>
{selectTeams.map((selectTeam, i) => {
return <option key={i}>{selectTeam}</option>;
})}
</select>
</div>
{loading ? <h1>suave</h1> : (
team !== null ? (
team.map((newTeam, i) => {
return (
<div>
the items are here
</div>
)
})
) : null
)}
</div>
);
};
export default Select;
i let you my api file down
const getTeam = async (teamName) => {
const url = `https://www.thesportsdb.com/api/v1/json/1/searchteams.php?t=${teamName}`;
const res = await fetch(url);
const team = await res.json();
return team;
};
export default getTeam;
i wanna update my const team with the response of my api call, but it doesn't update it, i dont know what do, please help me
The teamsJson value is an object with a single key and value of some array
{ teams: [...] }
So you are updating your state with a nested array when you push the value into another array.
let arr = [];
Object.keys(teamsJson).map((teamjs, i) => {
return arr.push(teamsJson[teamjs]);
});
Based upon how you want to map your team state array I assume you just want the raw inner array from teamJson.
const { teams } = await getTeam(e.target.value);
setTeam(teams);
Then when you are mapping you can access any of the properties you need.
team.map((newTeam, i) => {
return <div key={i}>{newTeam.idTeam}</div>;
})
I've just tested it & it seems to works just fine.
The only 2 issues seem to be that:
You don't use team anywhere (apart from a console.log statement).
At the moment when you console.log(team); the constant team will (yet) be null for the first time (because it still keeps the initial state).
Here's what I see in React dev tools after picking a random team in the <select>:
I've been having trouble using React's useContext hook. I'm trying to update a state I got from my context, but I can't figure out how. I manage to change the object's property value I wanted to but I end up adding another object everytime I run this function. This is some of my code:
A method inside my "CartItem" component.
const addToQuantity = () => {
cartValue.forEach((item) => {
let boolean = Object.values(item).includes(props.name);
console.log(boolean);
if (boolean) {
setCartValue((currentState) => [...currentState, item.quantity++])
} else {
return null;
}
});
};
The "Cart Component" which renders the "CartItem"
const { cart, catalogue } = useContext(ShoppingContext);
const [catalogueValue] = catalogue;
const [cartValue, setCartValue] = cart;
const quantiFyCartItems = () => {
let arr = catalogueValue.map((item) => item.name);
let resultArr = [];
arr.forEach((item) => {
resultArr.push(
cartValue.filter((element) => item === element.name).length
);
});
return resultArr;
};
return (
<div>
{cartValue.map((item, idx) => (
<div key={idx}>
<CartItem
name={item.name}
price={item.price}
quantity={item.quantity}
id={item.id}
/>
<button onClick={quantiFyCartItems}>test</button>
</div>
))}
</div>
);
};
So how do I preserve the previous objects from my cartValue array and still modify a single property value inside an object in such an array?
edit: Here's the ShoppingContext component!
import React, { useState, createContext, useEffect } from "react";
import axios from "axios";
export const ShoppingContext = createContext();
const PRODUCTS_ENDPOINT =
"https://shielded-wildwood-82973.herokuapp.com/products.json";
const VOUCHER_ENDPOINT =
"https://shielded-wildwood-82973.herokuapp.com/vouchers.json";
export const ShoppingProvider = (props) => {
const [catalogue, setCatalogue] = useState([]);
const [cart, setCart] = useState([]);
const [vouchers, setVouchers] = useState([]);
useEffect(() => {
getCatalogueFromApi();
getVoucherFromApi();
}, []);
const getCatalogueFromApi = () => {
axios
.get(PRODUCTS_ENDPOINT)
.then((response) => setCatalogue(response.data.products))
.catch((error) => console.log(error));
};
const getVoucherFromApi = () => {
axios
.get(VOUCHER_ENDPOINT)
.then((response) => setVouchers(response.data.vouchers))
.catch((error) => console.log(error));
};
return (
<ShoppingContext.Provider
value={{
catalogue: [catalogue, setCatalogue],
cart: [cart, setCart],
vouchers: [vouchers, setVouchers],
}}
>
{props.children}
</ShoppingContext.Provider>
);
};
edit2: Thanks to Diesel's suggestion on using map, I came up with this code which is doing the trick!
const newCartValue = cartValue.map((item) => {
const boolean = Object.values(item).includes(props.name);
if (boolean && item.quantity < item.available) {
item.quantity++;
}
return item;
});
removeFromStock();
setCartValue(() => [...newCartValue]);
};```
I'm assuming that you have access to both the value and the ability to set state here:
const addToQuantity = () => {
cartValue.forEach((item) => {
let boolean = Object.values(item).includes(props.name);
console.log(boolean);
if (boolean) {
setCartValue((currentState) => [...currentState, item.quantity++])
} else {
return null;
}
});
};
Now... if you do [...currentState, item.quantity++] you will always add a new item. You're not changing anything. You're also running setCartValue on each item, which isn't necessary. I'm not sure how many can change, but it looks like you want to change values. This is what map is great for.
const addToQuantity = () => {
setCartValue((previousCartValue) => {
const newCartValue = previousCartValue.map((item) => {
const boolean = Object.values(item).includes(props.name);
console.log(boolean);
if (boolean) {
return item.quantity++;
} else {
return null;
}
});
return newCartValue;
});
};
You take all your values, do the modification you want, then you can set that as the new state. Plus it makes a new array, which is nice, as it doesn't mutate your data.
Also, if you know only one item will ever match your criteria, consider the .findIndex method as it short circuits when it finds something (it will stop there), then modify that index.