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.
Related
What I have is a list that was fetched from an api. This list will be filtered based on the input. But at the first render it will render nothing, unless I press space or add anything to the input. Another solution is set the fetched data to the filteredList. But I don't know if it is the right thing to set the fetched data to two arrays.
import React, { useState, useEffect } from "react";
const PersonDetail = ({ person }) => {
return (
<div>
Id: {person.id} <br />
Name: {person.name} <br />
Phone: {person.phone}
</div>
);
};
const App = () => {
const [personsList, setPersonsList] = useState([]);
const [personObj, setPersonObj] = useState({});
const [showPersonDetail, setShowPersonDetail] = useState(false);
const [newPerson, setNewPerson] = useState("");
const [filter, setFilter] = useState("");
const [filteredList, setFilteredList] = useState(personsList);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users")
.then((response) => response.json())
.then((data) => {
setPersonsList(data);
//setFilteredList(data) <-- I have to add this to work
console.log(data);
});
}, []);
const handleClick = ({ person }) => {
setPersonObj(person);
if (!showPersonDetail) {
setShowPersonDetail(!showPersonDetail);
}
};
const handleChange = (event) => {
setNewPerson(event.target.value);
};
const handleSubmit = (event) => {
event.preventDefault();
const tempPersonObj = {
name: newPerson,
phone: "123-456-7890",
id: personsList.length + 1,
};
setPersonsList((personsList) => [...personsList, tempPersonObj]);
//setFilteredList(personsList) <-- to render the list again when add new person
setNewPerson(" ");
};
const handleFilter = (event) => {
setFilter(event.target.value);
const filteredList =
event.target.value.length > 0
? personsList.filter((person) =>
person.name.toLowerCase().includes(event.target.value.toLowerCase())
)
: personsList;
setFilteredList(filteredList);
};
return (
<div>
<h2>List:</h2>
Filter{" "}
<input value={filter} onChange={handleFilter} placeholder="Enter" />
<ul>
{filteredList.map((person) => {
return (
<li key={person.id}>
{person.name} {""}
<button onClick={() => handleClick({ person })}>View</button>
</li>
);
})}
</ul>
<form onSubmit={handleSubmit}>
<input
placeholder="Add Person"
value={newPerson}
onChange={handleChange}
/>
<button type="submit">Add</button>
</form>
{showPersonDetail && <PersonDetail person={personObj} />}
</div>
);
};
export default App;
Your filtered list is actually something derived from the full persons list.
To express this, you should not create two apparently independent states in this situation.
When your asynchronous fetch completes, the filter is probably already set and you are just setting personsList which is not the list you are rendering. You are rendering filteredList which is still empty and you are not updating it anywhere, except when the filter gets changed.
To avoid all of this, you could create the filtered list on each rendering and — if you think this is not efficient enough — memoize the result.
const filteredList = useMemo(() =>
filter.length > 0
? personsList.filter((person) =>
person.name.toLowerCase().includes(filter.toLowerCase())
)
: personsList,
[filter, personsList]
);
When the filter input gets changed, you should just call setFilter(event.target.value).
This way, you will always have a filtered list, independent of when your asynchronous person list fetching completes or when filters get updated.
Side note: Writing const [filteredList, setFilteredList] = useState(personsList); looks nice but is the same as const [filteredList, setFilteredList] = useState([]); because the initial value will be written to the state only once, at that's when the component gets initialized. At that time personsList is just an empty array.
I am currently having trouble thinking of an efficient solution to my problem. Essentially, I have a search bar on the previous page that allows a user to enter in a city location (heropage.js). That then navigates to a concerts list page (selectconcert.js), which portrays all of the concerts from that city using the useParams() hook to take the city name from the url.
The problem I am having is the following: I want users to be able to look up a new city on the selectconcerts.js page if they would like, and clear the useParams() so it doesn't interrupt. Currently since I am using a useEffect hook with the useParams, I am sometimes getting the old location param interrupting with the new search query for city. I also feel like I am duplicating my code by having a fetchConcerts function in my useEffect hook and having a handleLocationSubmit function to process the separate queries (depending if it was searched on the prior page or current page). I dabbled into using Searchparams but please let me know if you may have a more efficient solution.
selectconcert.js (page portraying the list of concerts)
export default function Concerts() {
let navigate = useNavigate()
const [concerts, setConcerts] = useState([]);
const [city, setCity] = useState('');
const [artist, setArtist] = useState('');
const [toggled, setToggled] = useState(false);
const [invalid, setInvalid] = useState(false);
const [page, setPage] = useState(1)
let {user} = useContext(AuthContext)
const yesterday = ( d => new Date(d.setDate(d.getDate()-1)) )(new Date());
let { location } = useParams();
useEffect( () => {
const fetchConcerts = async() => {
const concertResponse = await fetch(`${process.env.REACT_APP_BUDDY_API}/api/selectconcertsforcity/${location}/&p=${page}`)
if(concertResponse.ok) {
const concertData = await concertResponse.json();
if (concertData.concerts.setlist) {
for (let i in concertData.concerts.setlist){
const dateParts = concertData.concerts.setlist[i].eventDate.split("-");
const dateObject = new Date(+dateParts[2], dateParts[1] - 1, +dateParts[0]);
concertData.concerts.setlist[i].eventDate = dateObject
}
setConcerts(concertData.concerts.setlist);
let concList = [concertData.concerts.setlist.filter(concert => ((concert.eventDate)) >= (Date.now()))]
if (concList[0].length === 0){
setConcerts(0)
}
setArtist('');
setInvalid(false)
} else {
if (location !== undefined) {
setInvalid(true)
setConcerts([])
}
}
}
}
fetchConcerts();
}, [location, page]
);
const handleLocationSubmit = async (e) => {
e.preventDefault();
const city_new = city.split(' ')
let final_city = city_new[0]
for (let i = 1; i < city_new.length; i++) {
final_city += '%20'
final_city += city_new[i]
}
const concertResponse = await fetch(`${process.env.REACT_APP_BUDDY_API}/api/selectconcertsforcity/${final_city}/&p=1`)
if(concertResponse.ok) {
const concertData = await concertResponse.json();
if (concertData.concerts.setlist) {
for (let i in concertData.concerts.setlist){
const dateParts = concertData.concerts.setlist[i].eventDate.split("-");
const dateObject = new Date(+dateParts[2], dateParts[1] - 1, +dateParts[0]);
concertData.concerts.setlist[i].eventDate = dateObject
}
setConcerts(concertData.concerts.setlist);
let concList = [concertData.concerts.setlist.filter(concert => ((concert.eventDate)) >= (Date.now()))]
if (concList[0].length === 0){
setConcerts(0)
}
setArtist('');
setInvalid(false)
} else {
console.error('concertData:', concertResponse);
setInvalid(true)
setConcerts([])
}
}
}
.....
return (
<>
<div className='selectconcerts'>
<div>
<Toggle onChange={(e) => setToggled(e.target.checked)} />
<p> Search by {toggled ? "Artist": "City "}</p>
<div className='entry'>
{ toggled ?
<form onSubmit={handleArtistSubmit}>
<input className="form-control" type="text" value={artist} required onChange={(e) => {setArtist(e.target.value)}} onKeyPress={handleKeypress}/>
</form>
:
<form onSubmit={handleLocationSubmit}>
<input className="form-control" type="text" value={city} required onChange={(e) => {setCity(e.target.value)}} onKeyPress={handleKeypress}/>
</form>
}
<div>
<p></p>
</div>
herosection.js (page with the initial city search query option)
function HeroSection() {
let {user} = useContext(AuthContext)
const [city, setCity] = useState('');
let videoimage = require('./Images/video-3.mp4')
let navigate = useNavigate()
const handleImGoingSubmit = async (e) => {
e.preventDefault();
navigate(`/selectconcerts/${city}`)
}
const handleKeypress = e => {
//it triggers enter button by pressing the enter key
if (e.keyCode === 13) {
handleImGoingSubmit();
}
};
return (
<div className='hero-container'>
<video src={videoimage} autoPlay loop muted />
<h1 align="center">ADVENTURE AWAITS</h1>
{user ? (<p align="center">Hello {user.username}, what are you waiting for?</p>):(<p align="center">What are you waiting for?</p>)}
<div className='hero-btns'>
<form onSubmit={handleImGoingSubmit}>
<input className="form-control" type="text" placeholder="Search concerts by city..." value={city} required onChange={(e) => setCity(e.target.value)} onKeyPress={handleKeypress}/>
</form>
</div>
</div>
);
}
export default HeroSection;
You've already a function fetchConcerts and effect for fetching data based on the location. Don't duplicate this logic (DRY Principle). The handleLocationSubmit callback should issue an imperative navigation action to the same route with the new location path parameter.
Example:
export default function Concerts() {
const navigate = useNavigate();
...
const { location } = useParams();
useEffect(() => {
const fetchConcerts = async () => {
const concertResponse = await fetch(`${process.env.REACT_APP_BUDDY_API}/api/selectconcertsforcity/${location}/&p=${page}`);
...
};
fetchConcerts();
}, [location, page]);
const handleLocationSubmit = (e) => {
e.preventDefault();
const location = city.replaceAll(" ", "%20");
// reset back to page 1
setPage(1);
// navigate to update `location` path param and trigger rerender
navigate(`./${location}`);
};
...
I was looking through your entire code. Including the answer to your questions, I will also suggest some improvements to other pieces of code.
For your main question, you can either add a form reset button that uses form.reset() method to get the input field back to "". But, since in your case, your input is a controlled one(uses state as its value and setState as its handler), you can do the following:
<input {...props} /> // Input for selecting city
<button onClick={() => {
setConcerts([]);
setCity("");
setArtist(" ");
setInvalid(false);
setPage(1);
// Maybe you don't want to reset the toggled state for UX purposes
}}>Reset Search</button>
The fact you mention about you repeating your code is correct, so you should extract out the input field for city, related handlers like handleLocationSubmit and the states you use with it outside of the given file, into a new component. Also, depending on how yo extract out your component, the reset form handler might have fewer setter calls. You might also have to adjust the flow of data though, which I can help you with, if you need it.
In the first few lines of the handleLocationSubmit function, you can replace the white spaces with "%20" using this:
const cityString = city.split(" ").join("%20");
Hope this helps, let me know about any doubts in the comments and I will answer them.
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);
I have one edit form. its values are getting auto populated once user click on the edit button. actually all the values are getting populated properly except the dropdown. its still same. how can I change the valye of dropdown using react hook?
below method populating data in the dropdown.
const fetchStatus = async ()=>{
const {data}= await httpClient.get( config.resourceServerUrl+"/sponsors");
data.length>0? setResult(data): setResult([]);
}
below useEffect setting the values in the field once page loaded. before that it calls above method to populate values in dropdown because it comes from database.
useEffect(() => {
fetchStatus();
(async () => {
const { data } = await httpClient.get(config.resourceServerUrl+"/certificates/"+id);
setRequestStatus(data.requestStatus);
setSponsor(data.sponsor);
setAddress(data.address);
setPhaseOfTrial(data.phaseOfTrial);
setAddress2(data.address2);
setZipCOde(data.zipCode);
setCity(data.city);
setprotoColNo(data.protoColNo);
});
below are the react hooks I created.
const [requestStatus, setRequestStatus] = useState();
const [sponsor, setSponsor] = useState();
const [address, setAddress] = useState();
const [phaseOfTrial, setPhaseOfTrial] = useState();
const [address2, setAddress2] = useState();
const [zipCode, setZipCOde] = useState();
const [city, setCity] = useState();
const [protoColNo, setprotoColNo] = useState();
below hook is getting used for selecting the value in the dropdown..
const [sponsor, setSponsor] = useState();
what changes I need to do to select the value in dropdown once page loaded. later user can change.
I am usin react-select and formik in the page. when user will change the value in dropdown other field will also populate.
<div className="row mt-0 ">
<div className="col-sm-6 pb-2 gx-5">
<label
id="sponsorLabel"
className="required form-label"
>
Sponsor
</label>
<Select
id="sponsor"
name="sponsor"
// placeholder="Select the Sponsor"
className="align-items-center justify-content-center"
options={result.map((sponsor:Sponsor)=>
({ label: sponsor.name, value: sponsor.id })
)}
onChange={ (sponsorId:any,e:any)=>{
setFieldValue("sponsor",sponsorId.value)
result.map((sponsor:Sponsor)=>{
if(sponsorId.value===sponsor.id){
setFieldValue("address",sponsor.address);
setFieldValue("address2",sponsor.address2);
setFieldValue("zipCode",sponsor.zipCode);
setFieldValue("city",sponsor.city);
setFieldValue("sponsor",sponsor.name)
}
})
}}
/>
<span className={styles.mandatory}>
<ErrorMessage name="sponsor" />
</span>
</div>
</div>
below is the data to populate list.
const fetchStatus = async ()=>{
const {data}= await httpClient.get( config.resourceServerUrl+"/sponsors");
data.length>0? setResult(data): setResult([]);
}
I have a component on a page that renders a bunch of checkboxes and toggles.
I also have a button called confirm at the bottom to save the changes and make a request to update the back-end.
However I wanted to support a feature that when users haven't made any changes to any of these checkboxes or toggles, the confirm button should be disabled. and when users toggle or check any of these, then the confirm button is enabled and clickable.
so right now what I am doing is
const MyComponent = () => {
const dispatch = useDispatch();
// get the current state from the store
const state = useSelector((state: RootState) => state.settings);
const [isCheckbox1Checked, setCheckbox1] = useState(false);
const [isCheckbox2Checked, setCheckbox2] = useState(false);
const [isCheckbox3Checked, setCheckbox3] = useState(false);
const [isConfirmBtnEnabled, setConfirmBtn] = useState(false);
const [updateBtnEnabled, enableUpdateBtn] = useState(false);
useEffect(() => {
(async () => {
// `getSettingsConfig` is a async thunk action
await dispatch(getSettingsConfig());
setCheckbox1(state.isCheckbox1Checked);
setCheckbox2(state.isCheckbox2Checked);
setCheckbox3(state.isCheckbox3Checked);
})();
}, [
dispatch,
state.isCheckbox1Checked,
state.isCheckbox2Checked,
state.isCheckbox3Checked
// ..
]);
return (
<>
<div className="checkboxes">
<Checkbox1
onCheck={() => {
setCheckbox1(true);
setConfirmBtn(true);
}}
/>
<Checkbox2
onCheck={() => {
setCheckbox2(true);
setConfirmBtn(true);
}}
/>
<Checkbox3
onCheck={() => {
setCheckbox3(true);
setConfirmBtn(true);
}}
/>
</div>
<button disabled={!isConfirmBtnEnabled}>Confirm</button>
</>
);
};
right now it seems to be working out fine but it requires manually spamming setConfirmBtn to every checkbox and toggles I have on this page. I wonder if there is a better way to do it.
Also I thought about using useEffect to call isConfirmBtnEnabled every time any of these state changes. However since the initial state is derived from the store via dispatching an async thunk, the state of these checkboxes and toggles are going to be changed anyways after the page mounts, so that means I cannot use another useEffect to listen on the changes of these state.
You could use useEffect hook to watch the three check boxes and update the button state based on isConfirmBtnEnabled which is updated inside the useEffect hook:
useEffect(()=>{
setConfirmBtn(isCheckbox1Checked || isCheckbox2Checked || isCheckbox3Checked)
},[isCheckbox1Checked,isCheckbox2Checked,isCheckbox3Checked])
Edit :
const MyComponent = () => {
const dispatch = useDispatch();
// get the current state from the store
const state = useSelector((state: RootState) => state.settings);
const [checkboxes, setCheckboxes] = useState({c1:false,c2:false,c3:false});
const [isConfirmBtnEnabled, setConfirmBtn] = useState(false);
const [updateBtnEnabled, enableUpdateBtn] = useState(false);
useEffect(() => {
(async () => {
// `getSettingsConfig` is a async thunk action
await dispatch(getSettingsConfig());
[1,2,3].forEach(i=>{
setCheckboxes({...checkboxes,[`c${i}`]:state[`isCheckbox${1}Checked`]})
})
})();
}, [
dispatch,
state
// ..
]);
useEffect(()=>{
setConfirmBtn(Object.values(checkboxes).reduce((a,c)=>(a=a || c),false))
},[checkboxes])
const _onCheck=(i)=>{
setCheckboxes({...checkboxes,[`c${i}`]:tur})
}
return (
<>
<div className="checkboxes">
<Checkbox1
onCheck={() => _onCheck(1)}
/>
<Checkbox2
onCheck={() => _onCheck(2)}
/>
<Checkbox3
onCheck={() => _onCheck(3)}
/>
</div>
<button disabled={!isConfirmBtnEnabled}>Confirm</button>
</>
);
};