I'm currently working on a project with the Pokemon API and i'm facing a problem.
I want to change the value parameter in the async function getPokemonTypes(), but the value I receive in handleSelect() is not working.
On the console.log(value), the value changes every time I select a different option.
Could someone tell me what I'm doing wrong?
import React from 'react'
import { useState, useEffect } from "react";
import { Link } from 'react-router-dom'
async function getPokemonTypes(value) {
const response = await fetch(`https://pokeapi.co/api/v2/type/${value}`)
const data = await response.json()
console.log(data)
return data
}
async function getPokemonInfo(pokemonId) {
const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${pokemonId}`)
const data = await response.json()
return data
}
export const PokemonTypesCard = () => {
const [pokemonTypesCard, setPokemonTypesCard] = useState({})
const [pokemonIdNumber, setPokemonIdNumber] = useState([])
const [value, setValue] = useState('normal')
const handleSelect = (value) => {
setValue(value)
}
console.log(value)
useEffect(() => {
async function fetchData() {
const pokemonTypesCard = await getPokemonTypes(value)
const pokemonIdText = pokemonTypesCard.pokemon.map((item) => {
return item.pokemon.name
})
const data = pokemonIdText.map(async (pokemonId) => {
return (
await getPokemonInfo(pokemonId)
)
})
const pokemonIdNumber = await Promise.all(data)
setPokemonIdNumber(pokemonIdNumber)
setPokemonTypesCard(pokemonTypesCard)
}
fetchData()
}, [])
return (
<section>
<div>
<label htmlFor='pokemon-types'>Choose a pokemon type</label>
<form>
<select onChange={(event) => handleSelect(event.target.value)}
value={value}>
<option value='normal'>Normal</option>
<option value='fighting'>Fighting</option>
<option value='flying'>Flying</option>
<option value='poison'>Poison</option>
<option value='ground'>Ground</option>
<option value='rock'>Rock</option>
<option value='bug'>Bug</option>
<option value='ghost'>Ghost</option>
<option value='steel'>Steel</option>
<option value='fire'>Fire</option>
<option value='water'>Water</option>
<option value='grass'>Grass</option>
<option value='electric'>Electric</option>
<option value='psychic'>Psychic</option>
<option value='ice'>Ice</option>
<option value='dragon'>Dragon</option>
<option value='dark'>Dark</option>
<option value='fairy'>Fairy</option>
<option value='shadow'>Shadow</option>
</select>
</form>
</div>
{<div>
<ul>
{!pokemonIdNumber ? '' : pokemonIdNumber.map((item, index) =>
<li key={index}>
<Link to={`/pokemon/${item.id}`}>
<img
src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${item.id}.png`}
alt={item.name}
/>
</Link>
<p>{item.name}</p>
</li>
)}
</ul>
</div>}
</section>
);
}
You need to add type to the dependnecies array of useEffect:
useEffect(() => {
async function fetchData() {
const pokemonTypesCard = await getPokemonTypes(value);
const pokemonIdText = pokemonTypesCard.pokemon.map((item); => {
return item.pokemon.name;
});
const data = pokemonIdText.map(async (pokemonId) => {
return (
await getPokemonInfo(pokemonId)
);
});
const pokemonIdNumber = await Promise.all(data);
setPokemonIdNumber(pokemonIdNumber);
setPokemonTypesCard(pokemonTypesCard);
}
fetchData();
}, [value]); // <= HERE
Keep in mind this code has some issues, as you might end up seeing data for a type that doesn't match the one in the URL if something like this happens:
You select fire and getPokemonTypes('fire') is called.
You select to ice and getPokemonTypes('ice') is called.
getPokemonTypes('ice') finishes loading and the rest of the fetchData function executes.
getPokemonTypes('fire') finishes loading and the rest of the fetchData function executes.
The selected option is now ice but see data from fire.
The proper way to do it would be like this:
useEffect(() => {
let shouldUpdate = true;
async function fetchData() {
const pokemonTypesCard = await getPokemonTypes(value);
if (!shouldUpdate) return;
const pokemonIdText = pokemonTypesCard.pokemon.map((item) => {
return item.pokemon.name;
});
const data = pokemonIdText.map((pokemonId) => {
return getPokemonInfo(pokemonId);
});
const pokemonIdNumber = await Promise.all(data);
if (!shouldUpdate) return;
setPokemonIdNumber(pokemonIdNumber);
setPokemonTypesCard(pokemonTypesCard);
}
fetchData();
// This cleanup function will be called if the `useEffect`'s
// dependencies change, so if you run it twice in a row for
// different types, when the result of the first one arrives,
// it will be discarded:
return () => {
shouldUpdate = false;
};
}, [value]);
Also, you have an error here:
const data = pokemonIdText.map(async (pokemonId) => {
return (
await getPokemonInfo(pokemonId)
);
});
const pokemonIdNumber = await Promise.all(data);
You want to store all those promises in the data array and await them all together with Promise.all below, but you are instead awaiting them one by one. It should be like this:
const data = pokemonIdText.map((pokemonId) => {
return getPokemonInfo(pokemonId);
});
const pokemonIdNumber = await Promise.all(data);
Related
Question : "detectLanguageKey" is getting updated only after selecting the language from the dropdown twice.
When I select the option from the dropdown first time, detectLanguageKey is still "", and gets updated only after selecting the option second time.
Can you please explain why ? I have tried using async await and callbacks as well.
import React, { useState, useEffect } from "react";
import axios from "axios";
function SearchBar() {
const [inputText, setInputText] = useState("");
const [detectLanguageKey, setdetectedLanguageKey] = useState("");
const [selectedLanguageKey, setLanguageKey] = useState("");
const [languagesList, setLanguagesList] = useState([]);
const [resultText, setResultText] = useState("");
const getLanguageSource = () => {
axios
.post(`https://libretranslate.de/detect`, {
q: inputText,
})
.then((response) => {
setdetectedLanguageKey(response.data[0].language);
});
};
useEffect(() => {
axios.get("https://libretranslate.de/languages").then((res) => {
setLanguagesList(res.data);
console.log("languagesList", languagesList);
});
}, [inputText]);
const languageKey = (selectedLanguage) => {
setLanguageKey(selectedLanguage.target.value);
};
const translateText = async () => {
await getLanguageSource();
let data = {
q: inputText,
source: detectLanguageKey,
target: selectedLanguageKey,
};
axios.post(`https://libretranslate.de/translate`, data).then((response) => {
setResultText(response.data.translatedText);
});
};
return (
<div>
<textarea
rows="10"
cols="80"
onChange={(e) => setInputText(e.target.value)}
placeholder="Type text to translate.."
></textarea>
<textarea
rows="10"
cols="80"
placeholder="Your translated text will be here..."
value={resultText}
disabled={true}
></textarea>
{languagesList.length > 0 && (
<select onChange={languageKey} name="selectedLanguageKey">
<option>Please Select Language..</option>
{languagesList.map((lang) => {
return <option value={lang.code}>{lang.name}</option>;
})}
</select>
)}
<button
class="submit-btn"
onClick={(e) => {
translateText();
}}
>
Submit
</button>
</div>
);
}
Change translateText function to this
const translateText = async () => {
const detectedLanguageKey = await getLanguageSource();
const data = {
q: inputText,
source: detectedLanguageKey,
target: selectedLanguageKey,
};
axios.post(`https://libretranslate.de/translate`, data).then((response) => {
setResultText(response.data.translatedText);
});
};
Change getLanguageSource function to this
const getLanguageSource = async () => {
const response = await axios
.post(`https://libretranslate.de/detect`, {
q: inputText,
})
return response.data[0].language;
};
Remove inputText from the dependency array of the useEffect.
Remove const [detectLanguageKey, setdetectedLanguageKey] = useState("");
There were a few problems in your code.
First, inside translateText, you are awaiting a function that does not return a promise i.e. getLanguageSource.
Secondly, even if getLanguageSource returned a promise, you are expecting setdetectedLanguageKey inside getLanguageSource to take effect immediately. State updates are not instantaneous.
So, I'm trying to dynamically create the options of a select dropdown, I make the fetch of an api with the states of my country, but I don't know how to access the content inside each object..
As you can see below, the data is being pulled from the API, that is, the fetch worked, but I don't know how to create the options that will be inside the Select with each object..
import { EmailIcon, LocationIcon } from './assets/FormSvgIcons'
import { useEffect, useState } from 'react';
const SettingsForm = () => {
const [stateList, setStateList] = useState([]);
const [userLocation, setUserLocation] = useState('');
const handleLocation = () => {
setUserLocation(e.target.value);
}
useEffect(() => {
let initialStates = [];
fetch('https://servicodados.ibge.gov.br/api/v1/localidades/estados/')
.then(response => {
return response.json();
}).then(data => {
initialStates = data.map((states) => {
return states
});
console.log(initialStates);
setStateList({states: initialStates});
});
}, []);
const createDropdownOptions = () => {
const createOptions = stateList.map((state, i) => {
Object.keys(state).map(singleState => (
<option value={i}>{singleState.sigla}</option>
))
});
return createOptions;
}
return (
<form>
<div className="user-country">
<label className="white-label">
Local
</label>
<div className="input-icon-wrapper">
<div className="icon-input w-embed">
<LocationIcon />
</div>
<select
className="select-field white-select w-select"
id="locationField"
name="locationField"
onChange={handleLocation}
>
{createDropdownOptions()}
</select>
</div>
</div>
</form>
)
I know that the error is in the createDropdownOptions function because it is responsible for creating the options, but I don't know how to do it, any light?
I see your problem, your logic is correct, but it is poorly implemented, once you have filtered the data, it is only rendering a new component:
import { EmailIcon, LocationIcon } from "./assets/FormSvgIcons";
import React, { useEffect, useState } from "react";
export default function SettingsForm() {
const [stateList, setStateList] = useState([]);
useEffect(() => {
fetch("https://servicodados.ibge.gov.br/api/v1/localidades/estados/")
.then((response) => {
return response.json();
})
.then((data) => {
console.log(data);
setStateList(data);
});
}, []);
return (
<form>
<div className="user-country">
<label className="white-label">Local</label>
<div className="input-icon-wrapper">
<div className="icon-input w-embed">
<LocationIcon />
</div>
<select
className="select-field white-select w-select"
id="locationField"
name="locationField"
onChange={handleLocation}
>
{stateList.map((state) => {
return <CreateDropdownOptions state={state} />;
})}
</select>
</div>
</div>
</form>
);
}
function CreateDropdownOptions({ state }) {
return (
<option key={state.id} value={state.sigla}>
{state.sigla}
</option>
);
}
I recommend using a component for each option, this will make it easier if you later need to do some action on the
First you could simplify your useEffect to the code below. As you are making a map where the callback returns the same object for each iteration, better you use data as it's, because the output would be the same.
useEffect(() => {
fetch("https://servicodados.ibge.gov.br/api/v1/localidades/estados/")
.then((response) => {
return response.json();
})
.then((data) => {
console.log(data);
setStateList(data);
});
}, []);
Then change createDropdownOptions to the code below. You can change the value or what's displayed to nome:
const createDropdownOptions = () => {
const createOptions = stateList.map((state) => (
<option key={state.id} value={state.sigla}>
{state.sigla}
</option>
));
return createOptions;
};
And finnaly you would need to pass the event to handleLocation:
const handleLocation = (e) => {
setUserLocation(e.target.value);
}
Don't overthink. Tips:
Keep your fetching logic as simple as possible.
Prefer Async Await instead of then chaining for readability.
Honor your state initialization. If you said it is an Array, don't set it as an object.
If you have an array, you can easily map it into jsx and generate your options.
You did very well, and got really close. Take a look at the changes I've done to get it working:
import { useEffect, useState } from 'react';
export const SettingsForm = () => {
const [stateList, setStateList] = useState([]);
const [userLocation, setUserLocation] = useState('');
const handleLocation = () => {
setUserLocation(e.target.value);
};
useEffect(() => {
const loadOptions = async () => {
const data = await fetch(
'https://servicodados.ibge.gov.br/api/v1/localidades/estados/'
).then((response) => {
return response.json();
});
setStateList(data);
};
loadOptions();
}, []);
return (
<form>
<div className="user-country">
<label className="white-label">Local</label>
<div className="input-icon-wrapper">
<div className="icon-input w-embed"></div>
<select
className="select-field white-select w-select"
id="locationField"
name="locationField"
onChange={handleLocation}
>
{stateList.map((state) => {
return (
<option key={state.nome} value={state.nome}>
{state.sigla}
</option>
);
})}
</select>
</div>
</div>
</form>
);
};
Hope it helps! keep up the good work and feel free to reach out in case you're still stuck!
As the title suggests, I'm having issues updating state with a select menu. I'm not sure if the trouble is coming from the fact I'm trying to update it from multiple sources?
getSurvivorPerks fetches an array of objects. On page load a random 4 are selected to be displayed, and these four are randomized on each handlesubmit. I would like to be able to manually select the individual perks for perk1, 2, etc with a select menu. As of now, this just results in perk1 getting set to Null. The data does display appropriately in the select menu.
export default function SurvivorRandomizer() {
const [survivorPerk1, setSurvivorPerk1] = useState({});
const [survivorPerk2, setSurvivorPerk2] = useState({});
const [survivorPerk3, setSurvivorPerk3] = useState({});
const [survivorPerk4, setSurvivorPerk4] = useState({});
const [perkList, setPerkList] = useState([]);
const [loading, setLoading] = useState(true);
const { user } = useUser();
useEffect(() => {
const fetchData = async () => {
const data = await getSurvivorPerks();
let perks = randomPerks(data);
setPerkList(data);
setSurvivorPerk1(perks[0]);
setSurvivorPerk2(perks[1]);
setSurvivorPerk3(perks[2]);
setSurvivorPerk4(perks[3]);
setLoading(false);
};
fetchData();
}, []);
const handleSubmit = () => {
let perks = randomPerks(perkList);
setSurvivorPerk1(perks[0]);
setSurvivorPerk2(perks[1]);
setSurvivorPerk3(perks[2]);
setSurvivorPerk4(perks[3]);
};
if (loading) return <h1>loading...</h1>;
return (
<>
<div className="perk-row-1">
<div className="perk-card">
<PerkCard {...survivorPerk1} />
<select value={perkList.perk} onChange={(e) => setSurvivorPerk1(e.target.value)}>
<option>Select...</option>
{perkList.map((perk) => (
<option key={uuid()} value={perk}>
{perk.name}
</option>
))}
</select>
</div>
The issue here (had my blinders on) is that the value cannot be an object.
const handlePerkSelect = async (perk, id) => {
let setPerk = await getSurvivorPerkById(id);
perk(setPerk);
};
-------------
<select onChange={(e) => handlePerkSelect(setSurvivorPerk1, e.target.value)}>
<option>Select...</option>
{perkList.map((perk) => (
<option key={uuid()} value={perk.ID}>
{perk.name}
</option>
))}
</select>
This was my solution. There's definitely a better way to do it that doesn't involve making more fetch requests, and I'll likely refactor, but on the off chance this helps someone, I figured I'd share what the issue was.
Get All values of checkboxes from dynamic values
import * as React from "react";
import Checkbox from "#mui/material/Checkbox";
import FormControlLabel from "#mui/material/FormControlLabel";
import axios from "axios";
export default function Checkboxes() {
const [users, setUsers] = React.useState([]);
const [isChecked, setIsChecked] = React.useState(() =>
users.map((i) => false)
);
React.useEffect(() => {
getUsers();
}, []);
const getUsers = async () => {
try {
const response = await axios.get(
"https://jsonplaceholder.typicode.com/users"
);
setUsers(response.data);
} catch (error) {
console.error(error);
}
};
const isCheckboxChecked = (index, checked) => {
setIsChecked((isChecked) => {
return isChecked.map((c, i) => {
if (i === index) return checked;
return c;
});
});
};
console.log(isChecked);
return (
<div>
{users.map((checkbox, index) => {
return (
<FormControlLabel
key={index}
control={
<Checkbox
checked={isChecked[index]}
onChange={(e) => isCheckboxChecked(index, e.target.checked)}
/>
}
label={checkbox.name}
/>
);
})}
<pre>{JSON.stringify(isChecked, null, 4)}</pre>
</div>
);
}
i'm trying to do like this.
https://codesandbox.io/s/69640376-material-ui-react-multiple-checkbox-using-tabs-8jogw?file=/demo.js
but this has static data.
this is what i have done so far
https://codesandbox.io/s/festive-euclid-w3dzpq?file=/src/App.js
I've checked your code and found that you should issue in your "isCheckboxChecked" method.
const isCheckboxChecked = (index, checked) => {
isChecked[index] = checked;
setIsChecked([...isChecked]);
};
Another issue was you should wait till you get your response from API, so we are only setting checkboxes when we have users.
React.useEffect(() => {
if (users.length) setIsChecked(users.map((i) => false));
}, [users]);
Hope this helps😁
I've already updated your sandbox.
https://codesandbox.io/s/festive-euclid-w3dzpq?file=/src/App.js
If you need the same functionality as the first sandbox, then you need to update somehow your "checked" array after you fetch the users.
Currently this array is not updated, so you never see true/false values
Try this
const getUsers = async () => {
try {
const response = await axios.get(
"https://jsonplaceholder.typicode.com/users"
);
await setUsers(response.data);
setIsChecked(() => response.data.map((i) => false));
} catch (error) {
console.error(error);
}
};
sandbox
If you don't want to use a function, you can instead use an array
const [isChecked, setIsChecked] = React.useState([]);
React.useEffect(() => {
getUsers();
}, []);
const getUsers = async () => {
try {
const response = await axios.get(
"https://jsonplaceholder.typicode.com/users"
);
await setUsers(response.data);
setIsChecked(Array.from({length: response.data.length}, i => i = false))
} catch (error) {
console.error(error);
}
};
In order to clear the "uncontrolled state" warnings/errors, change this line
<Checkbox
checked={isChecked[index] == null ? false : isChecked[index]}
onChange={(e) => isCheckboxChecked(index, e.target.checked)}
/>
Checkbox must have an initial controlled state of true or false and not null/undefined.
If user type id in input, I'd like to fetch post by specific id typed by user. If not, I'd like to fetch whole array of posts, and then get some id from fetched data. I know it doesn't make sense, but it just only for tests.
It doesn't work, cause useState works asynchronously. I tried few solutions, but all of them looked very ugly.
LIVE DEMO
I received an error:
Cannot read properties of undefined (reading 'id')
Cause setPosts hasn't set yet.
What is the best, clear way to handle this case?
import { useState } from "react";
export default function App() {
const [id, setId] = useState("");
const [post, setPost] = useState(null);
const [posts, setPosts] = useState([]);
const fetchPost = async (id) => {
const res = await axios.get(
`https://jsonplaceholder.typicode.com/posts/${id}`
);
setPost(res.data);
};
const fetchPosts = async () => {
const res = await axios.get(`https://jsonplaceholder.typicode.com/posts`);
setPosts(res.data);
};
const onSubmit = async (e) => {
e.preventDefault();
if (id) {
await fetchPost(id);
} else {
await fetchPosts();
await fetchPost(posts[0].id);
}
};
return (
<div className="App">
<form onSubmit={onSubmit}>
<input type="text" onChange={(e) => setId(e.target.value)} />
<button type="submit">submit</button>
</form>
</div>
);
}
You can treat fetchPosts as a side-effect and wrap fetchPost(posts[0].id) in a useEffect dependant on posts.
Or just use the result directly in onSubmit() (presuming you don't need posts for something else).
const fetchPosts = async () => {
const res = await axios.get(`https://jsonplaceholder.typicode.com/posts`);
// setPosts(res.data); // this state is transitory and not used directly by the render
return res.data;
};
const onSubmit = async (e) => {
e.preventDefault();
if (id) {
await fetchPost(id);
} else {
const posts = await fetchPosts(); // Only used as part of submit event?
await fetchPost(posts[0].id);
}
};
return (
<div className="App">
<form onSubmit={onSubmit}>
<input type="text" onChange={(e) => setId(e.target.value)} />
<button type="submit">submit</button>
</form>
<div>{(post && post.title) || "No post yet"}</div>
</div>
);
Just like you said useState works asynchronously , if you want to do something after mutating it you will have to use useEffect and set posts as its arguments , now whenever the posts get mutated your funcion will be run and the first index of array will be sent to the fetchPost(id),
import axios from "axios";
import "./styles.css";
import { useEffect, useState } from "react";
export default function App() {
const [id, setId] = useState("");
const [post, setPost] = useState(null);
const [posts, setPosts] = useState([]);
useEffect(() => {
if (posts.length) {
fetchPost(posts[0].id);
}
}, [posts]);
const fetchPost = async (id) => {
console.log(`fetching ${id}`);
const res = await axios.get(
`https://jsonplaceholder.typicode.com/posts/${id}`
);
console.log(res.data);
setPost(res.data);
};
const fetchPosts = async () => {
console.log(`fetching all posts`);
const res = await axios.get(`https://jsonplaceholder.typicode.com/posts`);
setPosts(res.data);
};
const onSubmit = async (e) => {
e.preventDefault();
if (id) {
await fetchPost(id);
} else {
await fetchPosts();
// res = await fetchPost(posts[0].id); we dont need it here it defined in useffect function
}
};
const setDefaultId = (e) => {
setId(e.target.value);
};
return (
<div className="App">
<form onSubmit={onSubmit}>
<input type="text" onChange={(e) => setDefaultId(e)} />
<button type="submit">submit</button>
</form>
</div>
);
}
Also consider never to update state directly in your return function it will cause performance issues
The problem is in the method "fetchPost". Inside this method, you have two variables with the same name. "id" from the state hook, and "id" from the function parameter.
You can solve the problem changing one of those variables names.
One more thing, if "id" doesn't have value, your way to get the first post won't work because the await method won't wait to the change of the state.
I have edit a bit the code to solve both problems.
import { useState } from 'react';
import axios from 'axios';
export default function App() {
const [id, setId] = useState('');
const [post, setPost] = useState(null);
const fetchPost = async (idProp) => {
const res = await axios.get(
`https://jsonplaceholder.typicode.com/posts/${idProp}`,
);
setPost(res.data);
};
const fetchPosts = async () => {
const res = await axios.get('https://jsonplaceholder.typicode.com/posts');
await fetchPost(res.data[0].id);
};
const onSubmit = async (e) => {
e.preventDefault();
if (id) {
await fetchPost(id);
} else {
await fetchPosts();
}
};
return (
<div className="App">
<form onSubmit={onSubmit}>
<input type="text" onChange={(e) => {
setId(e.target.value);
}} />
<button type="submit">submit</button>
</form>
</div>
);
}
I hop I've helped you.