Making an array render wait for an axios call - arrays

My intention is to get the weather data for the selected country, passing selectedCountry.capital to the query, so it is displayed the weather from current country capital when the data of a country is displayed.
The problem is my code tries to render the weather data before the weather array is fetched, resulting in an error.
TypeError: Cannot read property 'temperature' of undefined
I get the array data
useEffect(() => {
if( selectedCountry.capital !== '' )
{
axios
.get(
`http://api.weatherstack.com/current?access_key=b51dfd70b0b2ccf136a0d7352876661c&query=${selectedCountry.capital}`
)
.then(res =>{
console.log(res.data)
console.log("capital" +selectedCountry.capital)
setWeather(res.data.current)
} )
}
}, [selectedCountry.capital])
render it
<div>
<h4>Weather</h4>
<h5>temperature: {weather.temperature} Celisues</h5>
<img src={weather.weather_icons[0]} alt='' />
<h5>
wind: {weather.wind_degree} mph direction {weather.wind_dir}
</h5>
</div>
i
If I don't render the array, I get the weather data just fine to the console. Also, If I add the array render code when the array is already there, the weather data gets displayed propperly.
What is the best way to make the array render wait for the array to be fetched from the axios call?
Full code
import React, { useState, useEffect } from 'react'
import axios from 'axios'
//setCountries is a function for setting the country's state
const App = () => {
const [countries, setCountries] = useState([])
//Filter
const [searchFilter, setSearchFilter] = useState('')
//Update state with button
const [selectedCountry, setSelectedCountry] = useState('')
const [weather, setWeather] = useState('')
const hook = () => {
console.log('effect')
axios
.get('https://restcountries.eu/rest/v2/all')
.then(response => {
console.log('promise fulfilled')
setCountries(response.data)
})
}
useEffect(hook,[])
/* by default the effect is always run after the component has been rendered. In our case, however, we only want to execute the effect along with the first render.
The second parameter of useEffect is used to specify how often the effect is run. If the second parameter is an empty array [], then the effect is only run along with the first render of the component. */
console.log('render', countries.length, 'countries')
console.log(countries)
/* weather */
useEffect(() => {
if( selectedCountry.capital !== '' )
{
axios
.get(
`http://api.weatherstack.com/current?access_key=b51dfd70b0b2ccf136a0d7352876661c&query=${selectedCountry.capital}`
)
.then(res =>{
console.log(res.data)
console.log("capital" +selectedCountry.capital)
setWeather(res.data.current)
} )
}
}, [selectedCountry.capital])
//When button es clicked the state is set, and the state variable is used
const renderCountryDetails = () => {
return (
selectedCountry && (
<p key={selectedCountry.alpha2Code}>
<p> Capital: {selectedCountry.capital}.</p>
<p> Population:{" "}
{selectedCountry.population}</p>
<p>
<img src={selectedCountry.flag} style={{ width: '200px'}}/>
</p>
<h3>Languages</h3>
<p> {selectedCountry.languages.map(language => <li key={language.name}>{language.name}</li>)}
<div>
<h4>Weather</h4>
<h5>temperature: {weather.temperature} Celisues</h5>
<img src={weather.weather_icons[0]} alt='' />
<h5>
wind: {weather.wind_degree} mph direction {weather.wind_dir}
</h5>
</div>
</p>
</p>
)
);
};
const filteredCountries =
searchFilter.length === 1
? countries
: countries.filter(
(country) => country.name.toLowerCase().indexOf(searchFilter.toLowerCase()) > -1
)
//showCountries returns either a message or else the contents of filteredcountries array
const showCountries = () => {
if (filteredCountries.length > 10) {
return 'Too many matches, keep on typing'
}
if (filteredCountries.length > 0
&& filteredCountries.length<10
&& filteredCountries.length>1 )
{
return (
<div>
{filteredCountries.map((country) => (
<p key={country.alpha2Code}>
{country.name}
{
//Update stste when button is clicked, passing country as a prop to the state
//onClick state is updated, causing the page to refresh and executing renderCountryDetails
//that uses the set state (the country) to render the info.
<button onClick={
() => setSelectedCountry(country)}>
show
</button>
}
</p>
))}
<div>{renderCountryDetails()}</div>
<div>
<p></p>
</div>
</div>
);
}
if (filteredCountries.length === 1) {
return filteredCountries.map((country) =>
<p key={country.alpha2Code}>
<p>Capital: {country.capital}.
<p> Population: {country.population} </p>
<h3>languages</h3>
{country.languages.map(language => <li key={language.name}>{language.name}</li>)}
<p><img src={country.flag} style={{ width: '200px'}}/>
</p>
</p>
</p>
)
}
}
const searchHandler = (e) => {
//setSelectedCountry state is set to empty
setSelectedCountry("");
setSearchFilter(e.target.value)
}
return (
<div>
<div>
<h1>Countries</h1>
</div>
<div>
Type to find countries:
<input onChange={searchHandler} />
<div>
{showCountries()}
</div>
</div>
</div>
);
}
export default App;

Simply use Optional chaining here:
<h5>temperature: {weather?.temperature||""} Celisues</h5>
In this case if the temperature is undefined it wont complain and would render an empty string instead.
"" can be replaced with any default value u need to show like 0 or something else in your case while your data is being fetched from API.
More on Optional chaining here:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining

Related

How can I default category through api

I have written a project which receives data through an api. Clicking on each button displays corresponding news. For example, when you press the sports button, sports news comes. However, I want the All category to be active when the page is first opened. In other words, those news should have arrived without pressing the all button. How can I do this?
Not - The function inside useffect returns every time it renders and it doesn't work for me. For example, when you refresh the page while reading sports news, all news comes
import React, { useEffect, useState } from "react";
import SpinnerLoad from './components/SpinnerLoad'
import NewsItem from "./components/NewsItem";
import Category from "./components/data/Category"
const App = () => {
const [state, setState] = useState([]);
const [loading, setLoading] = useState(false)
const fetchValue = (category) => {
fetch(`https://inshorts-api.herokuapp.com/news?category=${category}`)
.then(res => res.json())
.then(res => {
setState(res.data)
setLoading(true)
})
.catch((error) => console.log(error))
setLoading(false);
};
const CategoryButton = ({ category }) => (
<button onClick={() => fetchValue(category)} style={{ textTransform: 'capitalize' }}>{category}</button>
);
useEffect(() => {
fetchValue('all')
}, [])
return (
<>
<div className="header-bg">
<h1 className="mb-3">News</h1>
<div className="btns ">
{Category.map((value, index) => {
return <CategoryButton category={value} key={index} />;
})}
</div>
</div>
<div className="news">
<div className="container">
<div className="row">
{
!loading
? <SpinnerLoad/>
:
state.map((data,index) => {
return (
<NewsItem
imageUrl={data.imageUrl}
author={data.author}
title={data.title}
content={data.content}
date={data.date}
key={data.id}
/>
);
})
}
</div>
</div>
</div>
</>
);
};
export default App;
import React from 'react'
import clock from "../components/assets/img/Clock.svg"
import user from "../components/assets/img/User.svg"
const NewsItem = (props) => {
const {imageUrl, title, content, date, author} = props
return (
<div class="col-lg-4 col-md-6 col-12 p-2">
<div className="newsItem">
<img src={imageUrl} alt=''/>
<div className="itemBody">
<p className='title'>{title}</p>
<div className="line"></div>
<p className='content'>{content}</p>
<div className="itemfooter">
<h6><img src={clock} alt='clock'/>{date}</h6>
<h6><img src={user} alt='user'/>{author}</h6>
</div>
</div>
</div>
</div>
)
}
export default NewsItem
In react, if you refresh the app , the state values will reinitialise.
From your question , it seems like you want to store the category value and even after refresh , you want to persist the category value..
For that you can store category value in local or sessionStorage..
const fetchValue = (category) => {
localStorage.setItem("category", category);
// your code
}
// in useEffect , you can check for the category value in the local Storage
useEffect(() => {
// check value in localStorage, if does not exist use "all" as default value
let categoryValue = localStorage.getItem("category") || "all" ;
fetchValue(categoryValue)
},[]);

react all classNames are affected in map()

import React from 'react'
import { useState, useEffect } from 'react'
import axios from 'axios'
const Home = () => {
const getSongs = () => {
axios.get('http://localhost:8000/api/songs/')
.then(res => setSongs(res.data))
}
let [songs, setSongs] = useState([])
let [paused, setPause] = useState(true)
useEffect(() => {
getSongs()
}, [])
const toggleSong = (id) => {
const x = document.getElementById(id)
if (x.paused){
x.play()
setPause(false)
} else {
x.pause()
setPause(true)
}
}
// Got rid of the functions that are not needed
return (
<>
{
songs.map(song =>
(
<div className='music-controller' key={song.id}>
<div id={'songDiv'} style={{cursor: 'pointer'}} onClick={(e) => changeSongTime(e, song.id)}>
<div id={`songTime-${song.id}`}></div>
</div>
<div className="music-controller-body">
<div className="music-controller-header">
<h2>{song.title}</h2>
<p><small>{song.genre}</small></p>
</div>
<div className="controls">
// here <----------------------
<i unique={song.id} className={`fas fa-${paused ? 'play' : 'pause'}`} onClick={() => toggleSong(song.id)}></i>
<audio id={song.id} onTimeUpdate={() => songTime(song.id)}>
<source src={`http://localhost:8000/api/songs/audio/${song.id}`} />
</audio>
</div>
</div>
</div>
))}
</>
)
}
export default Home
Whenever I click on a specific i element all of the i elements that were not clicked on get changed too.. to put it simply when I click on the 1st i element only its className should change, but all of the i elements classNames are affected, what is causing this?
I think you should use event.target
const handlePlay = (song) => {
song.play();
};
const handlePause = (song) => {
song.pause();
};
...
<div className="controls">
<i
onMouseOver={(e) => handlePlay(e.target)}
onMouseLeave={(e) => handlePause(e.target)}
className={`fas fa-${paused ? 'play' : 'pause'}`}
onClick={() => toggleSong(song.id)}>
</i>
<audio id={song.id} onTimeUpdate={() => songTime(song.id)}>
<source src={`http://localhost:8000/api/songs/audio/${song.id}`} />
</audio>
</div>
I don't think Toggle would work in this case, an action should happen so it knows when it should stop.
Can you put console in toggleSong function at top and check if you are getting correct id. If you are not getting single Id then work is needed with onClick. So, after that also try passing id like this
onClick={(song?.id) => toggleSong(song?.id)}
then see console again and look for correct id if it is displayed or not. I think your className is not updating due to this issue.
One thing more you can try at end is replacing with this
const x = id; //without document.getElementById
const toggleSong = (e, id) => {
const x = document.getElementById(id)
const button = e.currentTarget
if (x.paused){
x.play()
button.className = 'fas fa-pause'
} else {
x.pause()
button.className = 'fas fa-play'
}
}
<i unique={song.id} className='fas fa-play' onClick={(e) => toggleSong(e, song.id)}></i>
I fixed this by just getting the current target with event.currentTarget and change its className accordingly!

Pass props to an axios get request

My intention is to get the weather data for the selected country, passing selectedCountry.capital to the query, so it is displayed the weather from current country capital when the data of a country is displayed.
useEffect(() => {
axios
.get(
`http://api.weatherstack.com/current?access_key=b51dfd70b0b2ccf136a0d7352876661c&query=${selectedCountry.capital}`
)
.then(res =>{
console.log(res.data)
console.log("capital"+selectedCountry.capital)
setWeather(res.data.current)
} )
}, [])
First problem: I am not passing selectedCountry.capital to the query, since console.log("capital"+selectedCountry.capital) returns undefined.
If I hardcode the query, I get a weather response.
useEffect(() => {
axios
.get(
`http://api.weatherstack.com/current?access_key=b51dfd70b0b2ccf136a0d7352876661c&query=New York`
)
.then(res =>{
console.log(res.data)
console.log("capital"+selectedCountry.capital)
setWeather(res.data.current)
} )
}, [])
I also tried to pass it like this
useEffect(() => {
axios
.get(
`http://api.weatherstack.com/current?access_key=b51dfd70b0b2ccf136a0d7352876661c&query=New York`
)
.then(res =>{
console.log(res.data)
console.log("capital"+selectedCountry.capital)
setWeather(res.data.current)
} )
}, [selectedCountry.capital])
[selectedCountry.capital]) I can log the capital. But it is some weird way to do it. And I can't display the weather data, since It will pass the selectedCountry.capital only after I select the country. There should be another way.
How do I pass selectedCountry.capital to the weather query?
Full code:
code sandbox
import React, { useState, useEffect } from 'react'
import axios from 'axios'
//setCountries is a function for setting the country's state
const App = () => {
const [countries, setCountries] = useState([])
//Filter
const [searchFilter, setSearchFilter] = useState('')
//Update state with button
const [selectedCountry, setSelectedCountry] = useState('')
const [weather, setWeather] = useState('')
const hook = () => {
console.log('effect')
axios
.get('https://restcountries.eu/rest/v2/all')
.then(response => {
console.log('promise fulfilled')
setCountries(response.data)
})
}
useEffect(hook,[])
/* by default the effect is always run after the component has been rendered. In our case, however, we only want to execute the effect along with the first render.
The second parameter of useEffect is used to specify how often the effect is run. If the second parameter is an empty array [], then the effect is only run along with the first render of the component. */
console.log('render', countries.length, 'countries')
console.log(countries)
/* weather */
useEffect(() => {
if( selectedCountry.capital !== '' )
{
axios
.get(
`http://api.weatherstack.com/current?access_key=b51dfd70b0b2ccf136a0d7352876661c&query=${selectedCountry.capital}`
)
.then(res =>{
console.log(res.data)
console.log("capital" +selectedCountry.capital)
setWeather(res.data.current)
} )
}
}, [selectedCountry.capital])
//When button es clicked the state is set, and the state variable is used
const renderCountryDetails = () => {
return (
selectedCountry && (
<p key={selectedCountry.alpha2Code}>
<p> Capital: {selectedCountry.capital}.</p>
<p> Population:{" "}
{selectedCountry.population}</p>
<p>
<img src={selectedCountry.flag} style={{ width: '200px'}}/>
</p>
<h3>Languages</h3>
<p> {selectedCountry.languages.map(language => <li key={language.name}>{language.name}</li>)}
<div>
<h4>Weather</h4>
<h5>temperature: {weather.temperature} Celisues</h5>
<img src={weather.weather_icons[0]} alt='' />
<h5>
wind: {weather.wind_degree} mph direction {weather.wind_dir}
</h5>
</div>
</p>
</p>
)
);
};
const filteredCountries =
searchFilter.length === 1
? countries
: countries.filter(
(country) => country.name.toLowerCase().indexOf(searchFilter.toLowerCase()) > -1
)
//showCountries returns either a message or else the contents of filteredcountries array
const showCountries = () => {
if (filteredCountries.length > 10) {
return 'Too many matches, keep on typing'
}
if (filteredCountries.length > 0
&& filteredCountries.length<10
&& filteredCountries.length>1 )
{
return (
<div>
{filteredCountries.map((country) => (
<p key={country.alpha2Code}>
{country.name}
{
//Update stste when button is clicked, passing country as a prop to the state
//onClick state is updated, causing the page to refresh and executing renderCountryDetails
//that uses the set state (the country) to render the info.
<button onClick={
() => setSelectedCountry(country)}>
show
</button>
}
</p>
))}
<div>{renderCountryDetails()}</div>
<div>
<p></p>
</div>
</div>
);
}
if (filteredCountries.length === 1) {
return filteredCountries.map((country) =>
<p key={country.alpha2Code}>
<p>Capital: {country.capital}.
<p> Population: {country.population} </p>
<h3>languages</h3>
{country.languages.map(language => <li key={language.name}>{language.name}</li>)}
<p><img src={country.flag} style={{ width: '200px'}}/>
</p>
</p>
</p>
)
}
}
const searchHandler = (e) => {
//setSelectedCountry state is set to empty
setSelectedCountry("");
setSearchFilter(e.target.value)
}
return (
<div>
<div>
<h1>Countries</h1>
</div>
<div>
Type to find countries:
<input onChange={searchHandler} />
<div>
{showCountries()}
</div>
</div>
</div>
);
}
export default App;
Edit:
I get the array data passing selectedCountry.capital, like this
useEffect(() => {
if( selectedCountry.capital !== '' )
{
axios
.get(
`http://api.weatherstack.com/current?access_key=b51dfd70b0b2ccf136a0d7352876661c&query=${selectedCountry.capital}`
)
.then(res =>{
console.log(res.data)
console.log("capital" +selectedCountry.capital)
setWeather(res.data.current)
} )
}
}, [selectedCountry.capital])
However, I can't acess to the array after it has been fetched, resulting on an error.
TypeError: Cannot read property 'temperature' of undefined
Code updated
The issue is here:
useEffect(() => {
}, []);
This useEffect will run on component load and at that time ${selectedCountry.capital} is blank.
To handle this issue try something like:
useEffect(() => {
if( selectedCountry.capital} !== '' )
{
// Make your axios call here
}
}, [selectedCountry.capital]);
selectedCountry.capital is dependency so this effect will run on component load and every time selectedCountry.capital will change and on first time when this effect run we have a conditional check so axios request will not trigger.

Filter array data and return it

This should be some silly mistake I am doing, but I can't find the problem.
I am making an application that gets an array from the server, then, I display the received data filtered by the user input serch.
If only one country is found in the search, the information for that country is displayed.
if (filteredCountries.length === 1) {
return filteredCountries.map((country) => (
<p key={country.alpha2Code}>
Capital: {country.capital}. Population: {country.population} <img src={country.flag} />
</p>
));
}
If several countries are found (but less than 10), a list of them is displayed, with a button beside each country on the list, that if clicked, shows the information from that specific country, using the method handleClick.
if (filteredCountries.length > 0 && filteredCountries.length < 10 && filteredCountries.length > 1) {
return filteredCountries.map((country) => (
<p key={country.alpha2Code}>
{country.name}
{<button onClick={handleClick}>show</button>}
</p>
));
}
handleClick should take as a prop the specific country, and display the data for that specific country. But it is not working for me.
const handleClick = (country) => {
console.log('click');
console.log(country);
return filteredCountries.map((country) => (
<p key={country.alpha2Code}>
Capital: {country.capital}. Population: {country.population} <img src={country.flag} />
</p>
));
};
What am I missing here?
Full code
Code sandbox
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const App = () => {
const [countries, setCountries] = useState([]);
const [searchFilter, setSearchFilter] = useState('');
const hook = () => {
console.log('effect');
axios.get('https://restcountries.eu/rest/v2/all').then((response) => {
console.log('promise fulfilled');
setCountries(response.data);
});
};
useEffect(hook, []);
console.log('render', countries.length, 'countries');
console.log(countries);
const handleClick = (country) => {
console.log('click');
console.log(country);
return filteredCountries.map((country) => (
<p key={country.alpha2Code}>
Capital: {country.capital}. Population: {country.population} <img src={country.flag} />
</p>
));
};
const filteredCountries =
searchFilter.length === 1
? countries
: countries.filter((country) => country.name.toLowerCase().indexOf(searchFilter.toLowerCase()) > -1);
const showCountries = () => {
if (filteredCountries.length > 10) {
return 'Too many matches, keep on typing';
}
if (filteredCountries.length > 0 && filteredCountries.length < 10 && filteredCountries.length > 1) {
return filteredCountries.map((country) => (
<p key={country.alpha2Code}>
{country.name}
{<button onClick={handleClick}>show</button>}
</p>
));
}
if (filteredCountries.length === 1) {
return filteredCountries.map((country) => (
<p key={country.alpha2Code}>
Capital: {country.capital}. Population: {country.population} <img src={country.flag} />
</p>
));
}
};
const searchHandler = (e) => {
setSearchFilter(e.target.value);
};
return (
<div>
<div>
<h1>Countries</h1>
</div>
<div>
Type to find countries:
<input onChange={searchHandler} />
<div>{showCountries()}</div>
</div>
</div>
);
};
export default App;
You shouldn't return any view in the handle click. Instead, track the selected country on the click of show button and based on that selection render the country view.
Try this approach,
import React, { useState, useEffect } from "react";
import axios from "axios";
//setCountries is a function for setting the country's state
const App = () => {
const [countries, setCountries] = useState([]);
//Filter
const [searchFilter, setSearchFilter] = useState("");
const [selectedCountry, setSelectedCountry] = useState("");
const hook = () => {
console.log("effect");
axios.get("https://restcountries.eu/rest/v2/all").then((response) => {
console.log("promise fulfilled");
setCountries(response.data);
});
};
useEffect(hook, []);
/* by default the effect is always run after the component has been rendered. In our case, however, we only want to execute the effect along with the first render.
The second parameter of useEffect is used to specify how often the effect is run. If the second parameter is an empty array [], then the effect is only run along with the first render of the component. */
console.log("render", countries.length, "countries");
console.log(countries);
const renderCountryDetails = () => {
return (
selectedCountry && (
<p key={selectedCountry.alpha2Code}>
Capital: {selectedCountry.capital}. Population:{" "}
{selectedCountry.population} <img src={selectedCountry.flag} />
</p>
)
);
};
const filteredCountries =
searchFilter.length === 1
? countries
: countries.filter(
(country) =>
country.name.toLowerCase().indexOf(searchFilter.toLowerCase()) > -1
);
//showCountries returns either a message or else the contents of filteredcountries array
const showCountries = () => {
/* if (filteredCountries.length === 0) {
return 'No coincidences found'
} */
if (filteredCountries.length > 10) {
return "Too many matches, keep on typing";
}
if (
filteredCountries.length > 0 &&
filteredCountries.length < 10 &&
filteredCountries.length > 1
) {
return (
<div>
{filteredCountries.map((country) => (
<p key={country.alpha2Code}>
{country.name}
{
<button onClick={() => setSelectedCountry(country)}>
show
</button>
}
</p>
))}
<div>{renderCountryDetails()}</div>
</div>
);
}
if (filteredCountries.length === 1) {
return filteredCountries.map((country) => (
<p key={country.alpha2Code}>
Capital: {country.capital}. Population: {country.population}{" "}
<img src={country.flag} />
</p>
));
}
};
const searchHandler = (e) => {
setSelectedCountry("");
setSearchFilter(e.target.value);
};
return (
<div>
<div>
<h1>Countries</h1>
</div>
<div>
Type to find countries:
<input onChange={searchHandler} />
<div>{showCountries()}</div>
</div>
</div>
);
};
export default App;
Codesandbox - https://codesandbox.io/s/kind-bohr-2crws?file=/src/App.js
You just need pass the country variable as a argument in button click same as below.
{<button onClick={() => handleClick(country)}>show</button>}
you also need to filter the country array in button click and need to set filtered array in state so that it will update and display.
I have updated your code hereUpdated Code.

All buttons are clicked at the same time instead of the specific one clicked

I am much confused as I don't know what I am doing wrong. Each time I clicked on the plus sign, all the other div elements display instead of the specific one I click on. I tried to use id argument in my show and hide functions, it is complaining of too many re-rendering . I have been on this for the past 12 hours. I need your help to solving this mystery. All I want to do is to click on the plus sign to display only the content and minus sign to hide it.
import React, {useState, useEffect} from 'react'
function Home() {
const [userData, setUserData] = useState([]);
const [showing, setShowing] = useState(false)
const [search, setSearch] = useState("");
const [clicked, setClicked] = useState("")
async function getData()
{
let response = await fetch('https://api.hatchways.io/assessment/students');
let data = await response.json();
return data;
}
useEffect(() => {
getData()
.then(
data => {
setUserData(data.students ) }
)
.catch(error => {
console.log(error);
})
}, [])
const handleFilterChange = e => {
setSearch(e.target.value)
}
function DataSearch(rows) {
const columns = rows[0] && Object.keys(rows[0]);
return rows.filter((row) =>
columns.some((column) => row[column].toString().toLowerCase().indexOf(search.toLowerCase()) > -1)
);
}
const searchPosts = DataSearch(userData);
const show = (id, e) => {
setShowing(true);
}
const hide = (id, e) => {
setShowing(false);
}
return (
<>
<div>
<input value={search} onChange={handleFilterChange} placeholder={"Search by name"} />
</div>
{
searchPosts.map((student) => (
<div key={student.id} className="holder">
<div className="images">
<img src={student.pic} alt="avatar" width="130" height="130" />
</div>
<div className="data-container">
<span className="name">{student.firstName.toUpperCase()} {student.lastName.toUpperCase()}</span>
<span>Email: {student.email}</span>
<span></span>
<span>Company: {student.company}</span>
<span>Skill: {student.skill}</span>
<span>City: {student.city}</span>
{ showing ?
<button id={student.id} onClick={hide}>-</button>
: <button id={student.id} onClick={show}>+</button>
}
<div data-id={student.id}>
{ (showing )
? student.grades.map((grade, index) => (
<span id={index} key={index}>Test {index}: {grade}%</span>
)) : <span>
</span>
}
</div>
</div>
</div>
))
}
</>
)
}
export default Home
Change,
const [showing, setShowing] = useState(false)
to:
const [showing, setShowing] = useState({});
Here change the useState from boolean to object.. Reason for this is we will store the ids as keys and a boolean value indicating if the grade should be shown or not.
And remove Show and hide function and have a common toggle function like,
const toggleGrades = (id) => {
setShowing((previousState) => ({
...previousState,
[id]: !previousState[id]
}));
};
You are using setShowing(true) in show function and setShowing(false) in hide function which is the reason for opening all and closing all at any click.. Because you have never mentioned which exact grade should be shown so you need to make use of id here..
And buttons click handler will be like,
{showing[student.id] ? (
<button id={student.id} onClick={() => toggleGrades(student.id)}>
-
</button>
) : (
<button id={student.id} onClick={() => toggleGrades(student.id)}>
+
</button>
)}
So pass student id () => toggleGrades(student.id) in both show and hide button an make the button gets toggled.
Display the grades like,
<div data-id={student.id}>
{showing[student.id] ? (
student.grades.map((grade, index) => (
<span id={index} key={index}>
Test {index}: {grade}%
</span>
))
) : (
<span></span>
)}
</div>
Here if showing[student.id] will display only the grades of clicked item.
And that is why id plays a major role in such case.
Working Example:

Resources