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!
Related
i want to optimize my react App by testing with a large list of li
Its a simple todo List.
By exemple, when click on a li, task will be line-through, and check icon will be green. This simple action is very slow with a large list because, the whole list is re render.
How to do this with React Hooks?
function App() {
const [list, setList] = useState([]);
const [input, setInput] = useState("");
const inputRef = useRef(null);
useEffect(() => inputRef.current.focus(), []);
//Pseudo Big List
useEffect(() => {
const test = [];
let done = false;
for (let i = 0; i < 5000; i++) {
test.push({ task: i, done });
done = !done;
}
setList(test);
}, []);
const handlerSubmit = (e) => {
e.preventDefault();
const newTask = { task: input, done: false };
const copy = [...list, newTask];
setList(copy);
setInput("");
};
const checkHandler = (e, index) => {
e.stopPropagation();
const copy = [...list];
copy[index].done = !copy[index].done;
setList(copy);
};
const suppression = (e, index) => {
e.stopPropagation();
const copy = [...list];
copy.splice(index, 1);
setList(copy);
};
const DisplayList = () => {
return (
<ul>
{list.map((task, index) => (
<Li
key={index}
task={task}
index={index}
suppression={suppression}
checkHandler={checkHandler}
/>
))}
</ul>
);
};
//JSX
return (
<div className='App'>
<h1>TODO JS-REACT</h1>
<form id='form' onSubmit={handlerSubmit}>
<input
type='text'
placeholder='Add task'
required
onChange={(e) => setInput(e.target.value)}
value={input}
ref={inputRef}
/>
<button type='submit'>
<i className='fas fa-plus'></i>
</button>
</form>
{list.length === 0 && <div id='noTask'>No tasks...</div>}
<DisplayList />
</div>
);
}
export default App;
Li component
import React from "react";
export default function Li(props) {
return (
<li
onClick={(e) => props.checkHandler(e, props.index)}
className={props.task.done ? "line-through" : undefined}
>
{props.task.task}
<span className='actions'>
<i className={`fas fa-check-circle ${props.task.done && "green"}`}></i>
<i
className='fas fa-times'
onClick={(e) => props.suppression(e, props.index)}
></i>
</span>
</li>
);
}
CodeSandbox here: https://codesandbox.io/s/sad-babbage-kp3md?file=/src/App.js
I had the same question, as #Dvir Hazout answered, I followed this article and made your code the changes you need:
function App() {
const [list, setList] = useState([]);
const { register, handleSubmit, reset } = useForm();
//Pseudo Big List
useEffect(() => {
const arr = [];
let done = false;
for (let i = 0; i < 20; i++) {
arr.push({ id: uuidv4(), task: randomWords(), done });
done = !done;
}
setList(arr);
}, []);
const submit = ({ inputTask }) => {
const newTask = { task: inputTask, done: false, id: uuidv4() };
setList([newTask, ...list]);
reset(); //clear input
};
const checkHandler = useCallback((id) => {
setList((list) =>
list.map((li) => (li.id !== id ? li : { ...li, done: !li.done }))
);
}, []);
const suppression = useCallback((id) => {
setList((list) => list.filter((li) => li.id !== id));
}, []);
//JSX
return (
<div className="App">
<h1>TODO JS-REACT</h1>
<form onSubmit={handleSubmit(submit)}>
<input type="text" {...register("inputTask", { required: true })} />
<button type="submit">
<i className="fas fa-plus"></i>
</button>
</form>
{list.length === 0 && <div id="noTask">No tasks...</div>}
<ul>
{list.map((task, index) => (
<Li
key={task.id}
task={task}
suppression={suppression}
checkHandler={checkHandler}
/>
))}
</ul>
</div>
);
}
Li component
import React, { memo } from "react";
const Li = memo(({ task, suppression, checkHandler }) => {
// console.log each time a Li component re-rendered
console.log(`li ${task.id} rendered.`);
return (
<li
onClick={(e) => checkHandler(task.id)}
className={task.done ? "line-through" : undefined}
>
{task.task}
<span className="actions">
<i className={`fas fa-check-circle ${task.done && "green"}`}></i>
<i className="fas fa-times" onClick={(e) => suppression(task.id)}></i>
</span>
</li>
);
});
export default Li;
You can check it live here
I know it's probably late for your question, but may help others ;)
You can use React.memo and wrap the Li component. This will cache the instances of the Li component based on shallow comparison. Read more in the docs
Otherwise, if you don't need the state in the container, you can keep it locally in the Li component and then it won't cause a whole list rerender.
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:
Here's what I've tried doing
import React, { Fragment, useState } from "react";
const Accordion = ({ items }) => {
const [activeIndex, setActiveIndex] = useState(null);
const [display, setDisplay] = useState("");
const handleChange = (index) => {
setActiveIndex(index);
setDisplay(display === "active" ? "" : "active");
};
const renderedItems = items.map((item, index) => {
return (
<Fragment key={item.title}>
<div className={`${display} title`} onClick={() => handleChange(index)}>
<i className="dropdown icon"></i>
{item.title}
</div>
<div className={`${display} content`}>
<p>{item.content}</p>
</div>
</Fragment>
);
});
return (
<div className="ui styled accordion">
{renderedItems}
{activeIndex}
</div>
);
};
export default Accordion;
But on clicking a single item, it expands or collapses all others. Also, I want only one expanded item at any given time, so if a user clicks another item the previous one should close automatically.
Here is my App.js
import React from "react";
import Accordion from "./components/Accordion";
const items = [
{
title: "What is React?",
content: "React is a front end javascript library",
},
{
title: "What is Angular?",
content: "Angular is a front end javascript framework",
},
{
title: "What is Vue?",
content: "Vue is a front end javascript library",
},
];
const App = () => {
return (
<div>
<Accordion items={items} />
</div>
);
};
export default App;
For your issue :
// Means if display is active so nothing else it's active, so everything is active
setDisplay(display === "active" ? "" : "active");
The display variable is shared between each accordion bodies... Always the same
Do you really need an display var ?
import React, { Fragment, useState } from "react";
const Accordion = ({ items }) => {
const [activeIndex, setActiveIndex] = useState(null);
const handleChange = index => {
setActiveIndex(activeIndex === index ? null : index);
};
const renderedItems = items.map((item, index) => {
return (
<Fragment key={item.title}>
<div className={`${display} title`} onClick={() => handleChange(index)}>
<i className="dropdown icon"></i>
{item.title}
</div>
<div className={`${activeIndex === index ? 'active' : ''} content`}>
<p>{item.content}</p>
</div>
</Fragment>
);
});
return (
<div className="ui styled accordion">
{renderedItems}
{activeIndex}
</div>
);
};
export default Accordion;
The this keyword inside the vidsAsHtml mapping function keeps returning undefined.
I read this, and a couple other SO questions about this but their solutions did not solve the problem. I'm already using es6 syntax arrow function for the map, but I've also tried putting in this as a second argument, which didn't solve the issue. Curious if anyone knows why 'this' keyword keeps coming up as undefined here.
import React, { useState, useEffect } from 'react'
import axios from 'axios'
const VideoGrid = (props) => {
const [videos, setResource] = useState([])
const fetchVideos = async (amount, category) => {
const response = await axios.get('https://pixabay.com/api/videos/', {
params: {
key: '123456679',
per_page: amount,
category: category
}
})
console.log(response)
const vidsAsHtml = response.data.hits.map(vid => {
return (
<div className={`${props.page}--grid-content-wrapper`} key={vid.picture_id}>
<div className={`${props.page}--grid-video`}>
<video
poster="https://i.imgur.com/Us5ckqm.jpg"
onMouseOver={this.play()}
onMouseOut={this.pause()}
src={`${vid.videos.tiny.url}#t=1`} >
</video>
</div>
<div className={`${props.page}--grid-avatar-placeholder`}></div>
<div className={`${props.page}--grid-title`}>{vid.tags}</div>
<div className={`${props.page}--grid-author`}>{vid.user}</div>
<div className={`${props.page}--grid-views`}>{vid.views}
<span className={`${props.page}--grid-date`}> • 6 days ago</span>
</div>
</div>
)
})
setResource(vidsAsHtml)
}
useEffect(() => {
fetchVideos(50, 'people')
}, [])
return (
<main className={`${props.page}--grid-background`}>
<nav className={`${props.page}--grid-nav`}>
<button
id='followButton'
className={`${props.page}--grid-nav-${props.titleOne}`}
>{props.titleOne}
</button>
<button
id='recommendedButton'
className={`${props.page}--grid-nav-${props.titleTwo}`}
>{props.titleTwo}
</button>
<button
id='subscriptionsButton'
className={`${props.page}--grid-nav-${props.titleThree}`}
>{props.titleThree}
</button>
<button className={`${props.page}--grid-nav-${props.titleFour}`}>{props.titleFour}</button>
<button className={`${props.page}--grid-nav-${props.titleFive}`}>{props.titleFive}</button>
<button className={`${props.page}--grid-nav-follow`}>FOLLOW</button>
</nav>
<hr className={`${props.page}--grid-hr-nav-grey`} />
<hr className={`${props.page}--grid-hr-nav-black`} />
<div className={`${props.page}--grid`} style={{marginTop: 'unset'}}>
{videos}
</div>
</main>
)
}
export default VideoGrid
Event handler props are expected to be passed a function. Currently you are trying to pass the return values of this.play() and this.pause() as event handlers, which wouldn't work anyway.
Also React doesn't make the element available to the event handler via this, but you can access it via event.target:
<video
poster="https://i.imgur.com/Us5ckqm.jpg"
onMouseOver={event => event.target.play()}
onMouseOut={event => event.target.pause()}
src={`${vid.videos.tiny.url}#t=1`} >
</video>
You can use ref for this,
let vidRef = React.createRef();
You should create function separately,
const playVideo = () => {
// You can use the play method as normal on your video ref
vidRef.current.play();
};
const pauseVideo = () => {
// Pause as well
vidRef.current.pause();
};
provide ref to video,
<video
ref = {vidRef} //Provide ref here
poster="https://i.imgur.com/Us5ckqm.jpg"
onMouseOver={() => playVideo()}
onMouseOut={() => pauseVideo()}
src={`${vid.videos.tiny.url}#t=1`} >
</video>
Demo
The following code shows a list of 10 users (list-view) and if you click on Details button of any of those users, it shows only that particular user (user-view).
import React, { useState, useEffect } from 'react'
import axios from 'axios'
const UserList = ({ id, setID }) => {
const [resources, setResources] = useState([])
const fetchResource = async () => {
const response = await axios.get(
'https://api.randomuser.me/?results=10'
)
console.log(response.data.results)
setResources(response.data.results)
}
useEffect(() => {
fetchResource()
}, [])
return (
<ul className='card__wrapper'>
{resources.filter(user => (id) ? user.login.uuid === id : true)
.map(item => (
<li className='card' key={item.name.first}>
<div className='card__item'>
<img className='card__image' src={item.picture.large} alt={item.name.first} />
<h2 className='card__title'>{item.name.first} {item.name.last}</h2>
{
id
?
<button
className='card__cta'
onClick={() => setID(null)}
>
Back to overview
</button>
:
<button
className='card__cta'
onClick={() => setID(item.login.uuid)}
>
Details
</button>
}
</div>
</li>
))}
</ul>
)
}
export default UserList
While this is working fine, the code inside the return which builds both the list-view and also the user-view is a bit difficult to understand (at least for me) and also makes it hard for using different CSS classes for List- and User-view.
I'd like to simplify the code so that's easier to understand by splitting it to two different returns.
Basically, saying that if the condition is true, return the user-view otherwise the list-view
How can I do that?
I would put the rendering stuff into another function, and to make what is going to be clearer I would use two returns:
import React, { useState, useEffect } from "react";
import axios from "axios";
const UserList = ({ id, setID }) => {
const [resources, setResources] = useState([]);
const fetchResource = async () => {
const response = await axios.get("https://api.randomuser.me/?results=10");
console.log(response.data.results);
setResources(response.data.results);
};
useEffect(() => {
fetchResource();
}, []);
const renderItem = (item, isLoggedIn) => {
return (
<li className="card" key={item.name.first}>
<div className="card__item">
<img className="card__image" src={item.picture.large} alt={item.name.first} />
<h2 className="card__title">
{item.name.first} {item.name.last}
</h2>
{isLoggedIn ? (
<button className="card__cta" onClick={() => setID(null)}>
Back to overview
</button>
) : (
<button className="card__cta" onClick={() => setID(item.login.uuid)}>
Details
</button>
)}
</div>
</li>
);
};
const user = resources.find(user => user.login.uuid === id);
if (user) {
return <ul className="card__wrapper">{renderItem(user, true)}</ul>;
} else {
return <ul className="card__wrapper">{resources.map(user => renderItem(user, false))}</ul>;
}
};
export default UserList;
Looks like the question asked pertains to this React hooks - OnClick show only the clicked item
Please find my comment for the above post, as I guess this particular issue can be solved as mentioned in the comment! In case it doesn't fix, let me know.