setting states in mapped elements react - reactjs

I'm having a problem setting the states of other mapped elements to false when clicking on one of the individual mapped element. For example,
const [edit, setEdit] = useState(false)
const array = ['witch-king', 'sauron', 'azog']
const arrayrow = array && array.map(el=>{
return <div>
<i
className='fal fa-edit'
onClick={(e)=>{
setEdit(true);
e.stopPropagation()
}
></i>
{edit?<i className='fal fa-times'></i>:''}
<span>{el}</span>
</div>
})
useEffect(()=>{
document.addEventListener('click', ()=>{setEdit(false)})
},[])
The issue is that when you click on one of the icons it will set the state to true, but then if you click on another element's icon, the previously clicked element's state will remain true. I want to be able to set the previously clicked element's state to false when the user clicks on another element's icon.
EDIT: here is a video further explaining what I mean
https://gyazo.com/d8123c9f9a5fcfc48b2149c7faf48bad

Maybe try this, but I am not sure if this wont cause current icon to close down.
const [edit, setEdit] = useState(false)
const array = ['witch-king', 'sauron', 'azog']
const arrayrow = array && array.map(el=>{
return <div>
<i
className='fal fa-edit'
onClick={(e)=>{
setEdit(true);
e.stopPropagation()
}
></i>
<span>{el}</span>
</div>
})
useEffect(()=>{
if(edit){
setEdit(false)
}
},[edit])
Or try something like
const [edit, setEdit] = useState(false)
const array = ['witch-king', 'sauron', 'azog']
const arrayrow = array && array.map(el=>{
const checkEl = (e, currentEl) => {
if(el === currentEl){
e.stopPropagation()
setEdit(true)
}else{
setEdit(false)
}
}
return <div>
<i
className='fal fa-edit'
onClick={(e) => checkEl(e, el)}
></i>
<span>{el}</span>
</div>
})
useEffect(()=>{
document.addEventListener('click', ()=>{setEdit(false)})
},[])
Something like that.

FULL CODE SOLUTION
const MainComponent = () => {
const [currentEdit, setCurrentEdit] = useState('')
const array = ['witch-king', 'sauron', 'azog']
const arrayrow = array && array.map(el=>(
<ChildComponent
key={el}
el={el}
currentEdit={currentEdit}
handleClick={setCurrentEdit}>
)
)
return (<> {arrayrow} Some JSX </>)
}
const ChildComponent = ({ el, handleClick, currentEdit }) => {
return (
<div>
<i
className={`fal ${(currentEdit === el) ? 'fa-times' : 'fa-edit' }`}
onClick={(e) => {
handleClick(el)
}
></i>
<span>{el}</span>
</div>
)
}
EXPLANATION
Instead of using a boolean, I used the value of each item to represent the currently edited item, and initially, it is set to an empty string like this
const [currentEdit, setCurrentEdit] = useState('')
I made a child component passing each the currentEdit and the setCurrentEdit as props.
<ChildComponent
key={el}
el={el}
currentEdit={currentEdit}
handleClick={setCurrentEdit}>
)
Then do a check inside the child component and reset the currentEdit to that element on click
<i
className={`fal ${(currentEdit === el) ? 'fa-times' : 'fa-edit' }`}
onClick={(e) => {
handleClick(el)
}
></i>
PS: Remove the useEffect with the event listener, adding event listeners like that is an anti-pattern in React, and also you don't need it with this solution.

Related

Persisting state change with localStorage for each input

So I have two radio button images, one checked and one not. I am trying to persist the change of state to view the corresponding image on button click for each of the inputs.
Please help.
Here's my code:
import React, { useState, useEffect } from 'react';
const Option = (props) => {
const img1 = <img alt='' src='/radio-active.png' className='radio__img' />;
const img2 = <img alt='' src='/radio-inactive.png' className='radio__img' />;
const [state, setState] = useState(false);
const handleStateChange = () => {
state === true ? setState(false) : setState(true);
};
useEffect(() => {
setState(JSON.parse(window.localStorage.getItem('state')));
}, []);
useEffect(() => {
window.localStorage.setItem('state', state);
}, [state]);
return (
<div className='option'>
<div className='radio'>
<button className='radio__button' onClick={handleStateChange}>
{state ? img1 : img2}
</button>
<p className='option__text radio__text'>{props.optionText}</p>
</div>
<button
className='button button--link'
onClick={(e) => {
props.handleDeleteOption(props.optionText);
}}
>
remove
</button>
</div>
);
};
export default Option;
All of your Option components are saving the state using the same key ("state"). You'll want each Option to have its own saved state. For each Option, add a new "optionName" property that is the key you want to use when saving the option's value to local storage.
// Change these:
window.localStorage.setItem('state', state);
setState(JSON.parse(window.localStorage.getItem('state')));
// To these:
window.localStorage.setItem(props.optionName, state);
setState(JSON.parse(window.localStorage.getItem(props.optionName)));

Show/Hide div onClick

I currently have my toggle action in place but the only help I need is that, I would like to close the div as well, like an toggle action. The one that I've currently done is that once I click on another div element the previous one that has been clicked closes, but I'd rather prefer that I have an toggle action on closing and opening on the div element being clicked, without needing to click on another just to close the previous div, I've only grabbed the parts that are needed in the code, just to prevent on copying and pasting the whole file, just to save time on reading.
Code Snippet
const [posts, setPosts] = useState([]);
const [commentState, commentChange] = useState({
activeObject: null
});
const toggleComment = (index) => {
commentChange({...commentState, activeObject: posts[index]})
}
const toggleActiveStyles = (index) => {
if(posts[index] === commentState.activeObject) {
return "dashboard__commentContent toggle";
} else {
return "dashboard__commentContent";
}
}
return error ? (
<span>{error}</span>
) : (
{posts.map((post, i) => (
<button onClick={() => toggleComment(i)} >toggle</button>
<div className={toggleActiveStyles(i)}>
<h1>{post.title}</h1>
</div>
)}
Here is a working codesandbox that you can manipulate to fit to your needs.
Explanation
You would want to keep track of toggled divs and make sure to adjust your class based on that. You can filter out or add to the toggled divs state variable, and do whatever you want while rendering.
Code
import { useState } from "react";
import "./styles.css";
const DATA = ["1", "2", "3", "4"];
export default function App() {
const [closedDivs, setClosedDivs] = useState([]);
const toggleDiv = (i) => {
setClosedDivs((divs) =>
divs.includes(i) ? divs.filter((d) => d !== i) : [...divs, i]
);
};
return (
<div className="App">
{DATA.map((d, i) => (
<div
className={`${closedDivs.includes(i) ? "close" : ""} box`}
onClick={() => toggleDiv(i)}
>
<p> {d} </p>
</div>
))}
</div>
);
}

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!

Custom Hook and cascading dropdown

I'm new to react and I'm learning by doing. Right now I'm stacked on using a custom Hook to share logic among components.
here is the logic inside the custom Hook
const useDropDownLogic = () => {
const { menu } = useContext(MenuContext)
const brand = [...new Set(menu.map(i => i.brand))]
const [category, setCategory] = useState([])
const brandChange = (e) => {
const { type, textContent } = e.target
setCategory([...new Set(menu.filter(i => i.[type] == textContent).map(i => i.category))])}
return {
brandChange,
brand,
category
}
}
export default useDropDownLogic;
here is the first component consuming the custom Hook
const DropDownBrand = () => {
const {brandChange, brand} = useDropDownLogic();
const dropdownRef = useRef(null);
const [isActive, setIsActive] = useDetectOutsideClick(dropdownRef, false);
const onClick = () => setIsActive(!isActive)
return (
<div id="dropdownbrand">
<div className="menu-container menu-brand">
<label className = 'nav-label'>Select a Brand</label>
<button onClick={onClick} className="menu-trigger brand-btn">
<i className="fas fa-chevron-down fa-2x drop-arrow"></i>
</button>
<nav
ref={dropdownRef}
className={`drop-menu ${isActive ? "active" : "inactive"}`}
>
<ul>
{brand.map((i) => (<li key={nanoid()} type = 'brand' value={i} onClick = {(e) => brandChange(e)}>{i}</li>))}
</ul>
</nav>
</div>
</div>
);
};
export default DropDownBrand;
and here the second
const DropDownCategory = () => {
const { category } = useDropDownLogic()
const dropdownRef = useRef(null);
const [isActive, setIsActive] = useDetectOutsideClick(dropdownRef, false);
const onClick = () => setIsActive(!isActive)
console.log(category);
return (
<div id="dropcategory">
<div className="menu-container menu-brand">
<label className = 'nav-label'>Select a Brand</label>
<button onClick={onClick} className="menu-trigger brand-btn">
<i className="fas fa-chevron-down fa-2x drop-arrow"></i>
</button>
<nav
ref={dropdownRef}
className={`drop-menu ${isActive ? "active" : "inactive"}`}
>
<ul>
{category.map((i) => (<li key={nanoid()} type = 'category'>{i}</li>))}
</ul>
</nav>
</div>
</div>
);
};
export default DropDownCategory;
Basically I'm able to populate the first dropdown but not the second. I don't understand why the category state is not update into the DropDownCategory component
any hint in what I'm doing wrong?
Many thanks in advance
I see,
I'm changing the approach. Moving state to a parent component and passing down function to handle click event. Then on Parent I'll update the dstate and pass it as props down to the child.
Nyhting to share yet but it will come
There is no persisted state in a hook, it's a way to share logic and functions. Because you are defining
const [category, setCategory] = useState([])
inside of the hook, it is defined each time you call the hook. In your case, dropdowncategory and dropdownbrand each have their personal state variable called category.
To solve this, define [category, setCategory] inside of the menu context, (or any other context) and pull in the values into your hook.. That way all instances of useDropDownLogic() are associated to global context value.

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