How to change multiple elements in useState array - javascript

I have an issue with my Hangman app im creating.I'm stuck at the stage when im trying to program the letters reveal while clicking on them.
Hangman.js
import al from '../alphabet'
import {HangmanContext} from '../context/HangmanContext'
const Hangman = (props) =>{
const {password,displayGame,displayForm} = useContext(HangmanContext)
const [subject,setSubject] = password
const [gameDisplay,setGameDisplay] = displayGame
const [createSubjectDisplay,setCreateSubjectDisplay] = displayForm
let passwordArr = Array.from(subject)
const [hiddenPassword,changeHiddenPassword] = useState([''])
useEffect(() => {
let passwordArr = Array.from(subject)
console.log(passwordArr)
changeHiddenPassword(passwordArr.map(item => '_'))
},[subject]);
const chooseLetter = (e) => {
let letter = e.target.innerHTML
passwordArr.forEach((s,i) => {
if (s == letter){
console.log(hiddenPassword)
changeHiddenPassword(hiddenPassword.map((item,j) => {
if(j == i){
item = subject[i]
}
return item
}))
}
console.log(hiddenPassword)
})
}
const letters = al.map((letter) =>
<span onClick={chooseLetter}>{letter}</span>
)
return(
<div style={{display:gameDisplay}}>
<div className='password'>
{
passwordArr.map((item,i) => <span>{hiddenPassword[i]}</span>)
}
</div>
<div className='wrapper'>
<div className='pass_input'>Enter password:<input type='text' maxLength='30'/></div>
<div className='alphabet'> {letters} </div>
</div>
</div>
)
}
export default Hangman
In my chooseLetter function im trying to change hiddenPassword state that contains initially as many "_" as is the password length(which i have fixed in another state ) to the point where it has revealed clicked letter.It works fine when the password contain letters that doesn't repeat. When letters in the password are repeating, revealing them only works for the last of them and i want them to reveal all.

Related

How do I change the increment/decrement number based on response data?

The backend calls to increment likes depicted by handleLike() and handleLike() functions successfully return the response of containing the number that's been incremented/decremented.
Unfortunately, this is the only way I can see photos.likes incremented/decrement in real time.
My question is: How do I make it so that I able to only increment/decrement the likes amount by 1 for the specific image's UserID that's being clicked on instead of ALL of them. Also, is there a way to avoid using frontend logic to accomplish this since the increment/decrement is happening on the server side?
I've hit a wall on this and not sure how to overcome it.
const [currentUserClicks, setCurrentUserClicks] = useState(1);
const [onChangeLikes, setonChangeLikes] = useState(null);
const handleLikesBasedOnUserId = (likedPhotoUserId) => {
if(currentUserClicks > 1) {
setCurrentUserClicks(currentUserClicks - 1);
handleDisLike(likedPhotoUserId); // sends data to server to decrement DB column
setonChangeLikes(false);
} else {
setCurrentUserClicks(currentUserClicks + 1);
handleLike(likedPhotoUserId); // sends data to server to increment DB column
setonChangeLikes(true);
}
};
return(
{
data.map((photos, index) => {
return <>
<div key={index}>
<img src={photos.url} alt="Photo" className="gallery-img" onClick={() => handleLikesBasedOnUserId(photos.UserID)}/>
<h5 className="likes">Likes: {!onChangeLikes ? photos.likes - 1: photos.likes + 1}</h5>
</div>
</>
})
}
);
In these cases you need to move the state locally to the mapped elements:
const { useState, useEffect } = React;
const App = () => data.map((pic) => <Pic key={pic.id} data={pic} />);
const Pic = ({ data }) => {
const [likes, setLikes] = useState(data.likes);
const handleClick = (type) => {
if (type === 'increment') {
if (likes - data.likes <= 0) {
// handleLike(id); // sends data to server to increment DB column
setLikes((l) => l + 1);
} else {
// handleDisLike(id); // sends data to server to decrement DB column
setLikes((l) => l - 1);
}
}
if (type === 'decrement') {
if (likes - data.likes >= 0) {
// handleDisLike(id); // sends data to server to decrement DB column
setLikes((l) => l - 1);
} else {
// handleLike(id); // sends data to server to increment DB column
setLikes((l) => l + 1);
}
}
};
return (
<div>
<img src={data.url} alt="Photo" className="gallery-img" />
<div className="container">
<h5 onClick={() => handleClick('decrement')}>-</h5>
<h5 className="likes">
Likes:
{likes}
</h5>
<h5 onClick={() => handleClick('increment')}>+</h5>
</div>
</div>
);
};
const data = [
{
url: 'https://upload.wikimedia.org/wikipedia/commons/a/a6/Pink_lady_and_cross_section.jpg',
likes: 10,
id: 'apple1',
},
{
url: 'https://upload.wikimedia.org/wikipedia/commons/2/22/Malus_domestica_a1.jpg',
likes: 25,
id: 'apple2',
},
];
const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<App/>)
.gallery-img {
width: 100px;
}
.container {
display: flex;
}
h5 {
cursor: pointer;
margin: 10px;
}
<div id="root"></div>
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
I was not sure what you were trying to do since you did not have any like/dislike button, so I was not sure of your logic:
(first click --> like | second click --> reset | third click ?)
So I added two like/dislike clickables and tweaked a bit the logic to permit only one like/dislike vote, and reset if trying to increase/decrease further, as it works here on StackOverflow.
Consider that you should handle this logic serverside by the way, and you should make a call on component mount to check if the user has already voted or not, in a real world scenario, or it's enough a refresh to be able to vote multiple times.

Rendering elements from an array of objects cause them to flicker on the screen and disappear

What is the problem?
I have a functional component that should render a list of player stats based on what team the user selects. The functionality of getting the data works and when I console log the state array using a useEffect I get an array with data inside it, but when I try to render the objects in html so you can see them on the screen sometimes they flicker on and then disappear, sometimes nothing happens at all.
What I've tried
I've tried using both a state array and just an ordinary variable array to see if that makes any difference. I've tried using .forEach and just a for loop to see if that would work. I've messed around with how I store the data and just trying to use a simple array instead of an object nothing so far has managed to get it rendered. As a note it is clear that the component does render as the div (className = Player-Stats) that contains the .map function is visible when inspected.
Thanks for any help and suggestions, I've spent days on this one functionality because the NHL api stores it's data super weirdly and you need to do all kinds of things to get the data you want. I didn't want to spam this question with tons of my code so if you need anything else like the parent components please ask and I can provide them.
Code Snippets
Landing Page
import { useState } from 'react';
import '../CSS/LandingPage.css';
import Instruction from './Instruction';
import LeagueLeaders from './LeagueLeaders';
import NavBar from './NavBar';
import TeamSelector from './TeamSelector';
import TeamStandings from './TeamStandings';
function LandingPage() {
const [teamSelected, setTeamSelected] = useState(false);
const [listOfTeams, setListOfTeams] = useState([]);
return (
<div className = 'Landing-Page-Container'>
<NavBar/>
<div className = 'Stats-Standings-Container'>
<div className = 'Team-Select-Container'>
<TeamSelector toggleStats = {setTeamSelected} setListTeams = {setListOfTeams}/>
</div>
<div className = 'Stats-Container'>
<LeagueLeaders showStats = {teamSelected} getListTeams = {listOfTeams} />
</div>
<div className = 'Standings-Container'>
<TeamStandings/>
</div>
</div>
</div>
);
}
export default LandingPage;
LeagueLeaders code
import { useState } from 'react';
import {FaChevronLeft, FaChevronRight} from 'react-icons/fa';
import '../CSS/LeagueLeaders.css';
import Instruction from './Instruction';
import LeaderStats from './LeaderStats.js';
function LeagueLeaders({showStats, getListTeams}){
var title = ['Skaters', 'Goalies', 'Defencemen'];
var [titleNo, setTitleNo] = useState(0);
var goalieOptions = ['GAA', 'SV%', 'SHUTOUTS'];
var nonGoalieOptions = ['POINTS', 'GOALS', 'ASSISTS'];
function selectPosition(task){
if(task === '+' && titleNo <2){
setTitleNo(titleNo+1);
}else if (task === '+' && titleNo == 2){
setTitleNo(0);
}else if(task === '-' && titleNo >0){
setTitleNo(titleNo-1);
}else{
setTitleNo(2);
}
}
return(
<div className = 'Leaders-Container'>
<div className = 'Leaders-Title'>
<FaChevronLeft className = 'toggleArrow' size = {24} color = 'white' onClick={() => selectPosition('-')}/>
<h1>{title[titleNo]}</h1>
<FaChevronRight className = 'toggleArrow' size = {24} color = 'white' onClick={() => selectPosition('+')}/>
</div>
<div className = 'Leaders-Selection-Container'>
<div className = 'Stat-Select-1'>
<p>{titleNo == 1 ? goalieOptions[0]: nonGoalieOptions[0]}</p>
</div>
<div className = 'Stat-Select-2'>
<p>{titleNo == 1 ? goalieOptions[1]: nonGoalieOptions[1]}</p>
</div>
<div className = 'Stat-Select-3'>
<p>{titleNo == 1 ? goalieOptions[2]: nonGoalieOptions[2]}</p>
</div>
</div>
<div className = 'Leaders-Stats-Container'>
{showStats ? <LeaderStats playerPos = {titleNo} teams = {getListTeams}/> : <Instruction/>}
</div>
</div>
);
}
export default LeagueLeaders;
TeamSelector component code
import '../CSS/TeamSelector.css';
import { useEffect, useState } from "react";
import teamDetail from "../Assets/teamDetail";
function TeamSelector( {toggleStats, setListTeams}) {
const [listOfTeams, setListOfTeams] = useState([]);
const [listOfURL, setListOfURL] = useState([]);
const [selectedTeams, setSelectedTeams] = useState([]);
useEffect(()=>{
console.log(selectedTeams);
setListTeams(selectedTeams);
}, [selectedTeams])
function handleClick(e){
const selectedTeamsCopy = [...selectedTeams];
if(selectedTeams.includes(e.currentTarget.id)){
if(selectedTeams.length <= 1){
toggleStats(false);
selectedTeamsCopy.splice(selectedTeamsCopy.indexOf(e.currentTarget.id, 1), 1);
setSelectedTeams(selectedTeamsCopy);
}else{
selectedTeamsCopy.splice(selectedTeamsCopy.indexOf(e.currentTarget.id, 1), 1);
setSelectedTeams(selectedTeamsCopy);
}
}else {
if(selectedTeams.length === 0){
toggleStats(true);
selectedTeamsCopy.push(e.currentTarget.id);
setSelectedTeams(selectedTeamsCopy);
}else{
selectedTeamsCopy.push(e.currentTarget.id);
setSelectedTeams(selectedTeamsCopy);
}
}
if(e.target.style.opacity === '1'){
e.target.style.opacity = '25%';
}else {
e.target.style.opacity = '100%';
}
}
return (
<div className = 'Team-Logo-Container'>
{teamDetail.map((Teams)=>(
<div>
<img onClick={(e) => handleClick(e)} key = {Teams.ID} id = {Teams.ID} alt = {Teams.Name +' Logo'} src = {Teams.URL} className = 'logo'/>
</div>
))}
</div>
);
}
export default TeamSelector;
Array of objects layout
[{ ID: "8480003", Name: "Jesper Boqvist", Points: "1", … }, { ID: "8475193", Name: "Tomas Tatar", Points: "10", … }, etc. etc.]
Rendering the array
return(
<div className = 'Player-Stats'>
{triggerStats ? listOfStats.map((d)=>{
return <p className = 'Stats' key={d.ID}>{d.ID}</p>}
) : <p className = 'Stats'> Sorry theres no available data</p>}
</div>
)
LeaderStats component script (Where the rendering issue is)
import { useEffect, useState, useRef } from "react";
import '../CSS/LeaderStats.css';
function LeaderStats({playerPos, teams}){
const isInitialMount = useRef(true);
const [listOfStats, setListOfStats] = useState([]);
const [triggerStats, setTriggerStats] = useState(false);
//If it's not the first render and the listOfStats state has changed it will render the stats of the players
useEffect(()=>{
if(!isInitialMount.current){
console.log(listOfStats);
setTriggerStats(true);
}
}, [listOfStats])
//When teams prop changes, run the function to get player data from API
useEffect(()=>{
if (isInitialMount.current) {
isInitialMount.current = false;
}else{
if(teams.length !== 0){
getPlayerIDs(teams);
}else{
setTriggerStats(false);
}
}
},[teams])
//This function runs all the axios calls and gathers data from multiple endpoints and saves it to an array
function getPlayerIDs(teamID){
const axios = require('axios');
var playerList=[];
var tempObj;
teamID.forEach(d =>
axios.get(`https://statsapi.web.nhl.com/api/v1/teams/${parseInt(d)}/roster`).then(res => {
//If user has selected the forward position filter roster by that player position and get the stats and save to temp array.
if(playerPos === 0){
res.data.roster.filter(obj => obj.position.type === 'Forward').map(e=>
axios.get(`https://statsapi.web.nhl.com/api/v1/people/${e.person.id}/stats?stats=statsSingleSeason&season=20212022`).then(res =>{
if(typeof res.data.stats[0].splits[0] !== 'undefined'){
if(playerPos !== 1 ){
tempObj = { ID: `${e.person.id}`, Name: `${e.person.fullName}` ,Points: `${res.data.stats[0].splits[0].stat.points}`, Goals: `${res.data.stats[0].splits[0].stat.goals}`, Assists: `${res.data.stats[0].splits[0].stat.assists}`};
playerList.push(tempObj);
}
}
})
);
}
//If user has selected the goalie position filter roster by that player position and get the stats and save to temp array.
else if(playerPos === 1){
res.data.roster.filter(obj => obj.position.type === 'Goalie').map(e=>
axios.get(`https://statsapi.web.nhl.com/api/v1/people/${e.person.id}/stats?stats=statsSingleSeason&season=20212022`).then(res =>{
if(typeof res.data.stats[0].splits[0] !== 'undefined'){
if(playerPos !== 1 ){
tempObj = { ID: `${e.person.id}`, Name: `${e.person.fullName}`, GAA: `${res.data.stats[0].splits[0].stat.goalAgainstAverage}`, SavePercentage: `${res.data.stats[0].splits[0].stat.savePercentage}`, Shutouts: `${res.data.stats[0].splits[0].stat.shutouts}`};
playerList.push(tempObj);
}
}
})
);
}
//If user has selected the defense position filter roster by that player position and get the stats and save to temp array.
else if(playerPos === 2){
res.data.roster.filter(obj => obj.position.type === 'Defenseman').map(e=>
axios.get(`https://statsapi.web.nhl.com/api/v1/people/${e.person.id}/stats?stats=statsSingleSeason&season=20212022`).then(res =>{
if(typeof res.data.stats[0].splits[0] !== 'undefined'){
if(playerPos !== 1 ){
tempObj = { ID: `${e.person.id}`, Name: `${e.person.fullName}` ,Points: `${res.data.stats[0].splits[0].stat.points}`, Goals: `${res.data.stats[0].splits[0].stat.goals}`, Assists: `${res.data.stats[0].splits[0].stat.assists}`};
playerList.push(tempObj);
}
}
})
);
}
})
);
//Set the state to the temp array that will be used to render the stats
setListOfStats(playerList);
}
return(
<div className = 'Player-Stats'>
{triggerStats ? listOfStats.map((d)=>{
return <p className = 'Stats' key={d.ID}>{d.ID}</p>}
) : <p className = 'Stats'> Sorry theres no available data</p>}
</div>
)
}
export default LeaderStats;
I have managed to figure out the issue so I'll post it here for those of you who in the future might find yourself in the same position as me without being able to find an answer. Turns out the way I was setting setListOfStats(playerList); made the state change without me realising it and by the time it got to rendering the .map there was nothing actually there as console.log takes a snapshot of the state at the time of the console.log. The solution (which I had experimented with before a few times but couldn't get working) was to remove the temprorary array of playerList completely and change the following code playerList.push(tempObj); to setListOfStats(listOfStats => [...listOfStats, tempObj]) thise line of code essentially sets the list of stats as I did before but it captures the previous state as well. This comes up with another issue of when you want to remove some data from the list but that's not related to this question. Hopefully someone can make use of this answer in the future.

Can't get the images from an API to show up

I'm using an API to get information for a database sort of thing. I want the images to be displayed to the right of the text but the images aren't showing up at all. I tried multiple different keys and still nothing. Here is what it currently looks like:
The images are not showing up as you can see.
Here is the JS (its pulling the data from here https://api.tvmaze.com/shows/347/episodes):
// DATABASE const sunnyList = document.getElementById('sunnyList'); let sunnyInfo = [];
searchBar.addEventListener('keyup', (e) => { const searchTarget = e.target.value.toLowerCase(); const filteredSunny = sunnyInfo.filter(sunny => {
return sunny.name.toLowerCase().includes(searchTarget) || sunny.airdate.toLowerCase().includes(searchTarget) || sunny.airtime.includes(searchTarget) });
displayInfo(filteredSunny); });
const loadLayout = async () => {
try {
const res = await fetch('https://api.tvmaze.com/shows/347/episodes');
sunnyInfo = await res.json();
displayInfo(sunnyInfo);
} catch (err) {
console.error(err);
} };
const displayInfo = (sunny) => {
const htmlString = sunny
.map((sunny) => {
return `
<li class="character">
<div class="detail">
<h2>${sunny.name}</h2>
<p>Season ${sunny.season} Episode ${sunny.number}</p>
<p>${sunny.airdate}</p>
<p>${sunny.airtime}</p>
<p>${sunny.rating.average}</p>
</div>
<img src="${sunny.image}"></img>
</li>
`;
})
.join('');
sunnyList.innerHTML = htmlString; };
loadLayout();
I've tried sunny.image.medium and sunny.image.original but it still doesn't show up.
Any help is appreciated :)
The image is not a url string, but an object with the following shape:
{
medium: string,
original: string
}
where both strings contain the actual image URLs.
For your use case medium probably makes more sense, so you can do this:
<img src="${sunny.image?.medium}"></img>
Edit
Added optional chaining because some items do not have image property.
The problem your are facing is that not all objects have images.
Please try this code:
const displayInfo = (sunny) => {
const htmlString = sunny
.map((sunny) => {
const img = sunny.image ? sunny.image.medium : "https://picsum.photos/200/300"
return `
<li class="character">
<div class="detail">
<h2>${sunny.name}</h2>
<p>Season ${sunny.season} Episode ${sunny.number}</p>
<p>${sunny.airdate}</p>
<p>${sunny.airtime}</p>
<p>${sunny.rating.average}</p>
</div>
<img src=${img} />
</li>
`;
})
.join('');
sunnyList.innerHTML = htmlString; };

Why when i am searching for something else is deleting the previous contents

Why when you are searching for something else is deleting the previous contents ?For example first you search for egg and show the contents but then when you search for beef the program deletes the egg and shows only beef.Code :
const searchBtn = document.getElementById('search-btn');
const mealList = document.getElementById('meal');
const mealDetailsContent = document.querySelector('.meal-details-content');
const recipeCloseBtn = document.getElementById('recipe-close-btn');
// event listeners
searchBtn.addEventListener('click', getMealList);
mealList.addEventListener('click', getMealRecipe);
recipeCloseBtn.addEventListener('click', () => {
mealDetailsContent.parentElement.classList.remove('showRecipe');
});
// get meal list that matches with the ingredients
function getMealList(){
let searchInputTxt = document.getElementById('search-input').value.trim();
fetch(`https://www.themealdb.com/api/json/v1/1/filter.php?i=${searchInputTxt}`)
.then(response => response.json())
.then(data => {
let html = "";
if(data.meals){
data.meals.forEach(meal => {
html += `
<div class = "meal-item" data-id = "${meal.idMeal}">
<div class = "meal-img">
<img src = "${meal.strMealThumb}" alt = "food">
</div>
<div class = "meal-name">
<h3>${meal.strMeal}</h3>
Get Recipe
</div>
</div>
`;
});
mealList.classList.remove('notFound');
} else{
html = "Sorry, we didn't find any meal!";
mealList.classList.add('notFound');
}
mealList.innerHTML = html;
});
}
It's because you are replacing the contents in the mealList element every time.
A simple workaround would be to retrieve the the innerHTML values before you update it.
Something like
let html = mealList.innerHTML;
rather than starting off empty every time you call the function should do the trick.

Conditional onclick funtion firing without click?

I have set up a conditional element on click on a button I've made within react. but default prop runs onload without clicking the button how can I fix this issue?
the button looks like this:
<p onClick={Butter + Milk + Bread + Soup + Cheese > 0 ? props.next_ClickHandler : alert('Please Input some food!')}>Buy Now!</p>
I would like it so that if the values add to greater than 0 the props are passed but if not an alert is played why it this not working as intended?
Edit full code:
import React, { useState, useEffect, useContext } from "react";
import Data from '../shoppingData/Ingredients';
import { quantitiesContext } from '../shoppingData/Quantities';
const ShoppingPageOne = (props) => {
//element displays
const [pageone_show, setPageone_show] = useState("pageOne");
//stores quantities of items as JSON objects
const [Quantities, setQuantities] = useContext(quantitiesContext);
const quantities = useContext(quantitiesContext);
const Bread = quantities[0].Bread.quantities;
const Milk = quantities[0].Milk.quantities;
const Cheese = quantities[0].Cheese.quantities;
const Soup = quantities[0].Soup.quantities;
const Butter = quantities[0].Butter.quantities;
useEffect(() => {
//sets info text using Json
if (props.showOne) {
setPageone_show("pageOne");
} else {
setPageone_show("pageOne hide");
}
}, [props.showOne]);
return (
<div className={"Shopping_Content " + pageone_show}>
<div className="Shopping_input_aligner">
<div className='Shopping_input_container'>
{Data.map((Ingredients) => {
//updates Quanties Hook
const handleChange = (event) => {
setQuantities({
...Quantities,
[Ingredients.Name]: {
...(Quantities[Ingredients.Name] ?? {}),
quantities: event.target.value
}
});
};
return (<div className={"Shopping_input " + Ingredients.Name} key={Ingredients.Name}>
<p>{Ingredients.Name} £{Ingredients.Price}</p>
<input onChange={handleChange.bind(this)} min="0" placeholder="Input food quantity" type="number"></input>
</div>)
})}
</div>
<div className='Discount_list'>
<h3>Discounts:</h3>
<li>Buy one cheese get one free!</li>
<li>Buy a Soup get a half price bread!</li>
<li>A third off butter!</li>
</div>
</div>
<div className="Shopping_Buttons">
<p onClick={() => {Butter + Milk + Bread + Soup + Cheese > 0 ? props.next_ClickHandler : alert('Please Input some food!')}} >Buy Now!</p>
</div>
</div>
);
};
export default ShoppingPageOne;
You can have a cleaner code with something like this if you're using React Hooks
const [ingredientsGreaterThanZero, setIngredientsGreaterThanZero] = useState(false);
useEffect(() => {
if (butter + milk + bread + soup + cheese > 0) {
setIngredientsGreaterThanZero(true)
} else {
setIngredientsGreaterThanZero(false)
}
}, [butter, milk, bread, soup, cheese]);
...
{ingredientsGreaterThanZero ?
<p onClick={props.next_ClickHandler}>Buy Now!</p> :
<p onClick={() => alert('Please Input some food!')}>Buy Now!</p>
}
<p onClick={() => { Butter + Milk + Bread + Soup + Cheese > 0 ? props.next_ClickHandler : alert('Please Input some food!')}}>Buy Now!</p>
Can you try using this?
Reason
If you attach any event in element with onClick() or any other event handler,
You shouldn't add any function invocation like in your example alert().
Because, of parentheses () the function runs when component mounted in dom.
Example:
<p onClick={alert('runs on load')}>wrong way</p>
Solution
You have to add an empty function and and writes your codes in it. If your codes contain function invocation ( with parentheses () ).
Example:
<p onClick={() => alert ('only runs on click')}>right way</p>
Happy Coding :)

Categories