Button Click In React - javascript

I have a problem with React.js. This is the line of code I have:
import React, { useState } from "react";
import { map } from "lodash";
function Steps({ procedure, commandSender, index }) {
const [selected, setSelected] = useState([]);
function clickHandler(command, key, index) {
commandSender(`${command}`)
if (isSelected((index-key))) setSelected(selected.filter(s => s !== (index-key)))
else ([...selected, (index-key)])
}
function isSelected(key) {
return selected.includes(key);
}
return (
<>
{procedure.guide &&
map(procedure.guide, (key) => (
<a
key={`${index}-${key}`}
className={`bt-blue ${isSelected(index-key) ? "active" : ""}`}
onClick={() => clickHandler('GUIDE', key, index)}
>
{procedure.title}
</a>
))
}
{procedure.success &&
map(procedure.success, () => (
<a
key={`${index}-${key}`}
className={`bt-green ${isSelected(index-key) ? "active" : ""}`}
onClick={() => clickHandler('SUCCESS', key, index)}
>
{procedure.title}
</a>
))
}
</>
);
}
export default Steps;
As you can see, I map a procedure, and for each item, I create an A tag, that calls a function clickHandler. This function calls another function and a setSelected. The setSelected function says which A tag is clicked or not. The only problem is that when I click in an A tag, it doesn't get selected.
But I need just the tag I clicked to have a SELECTED effect. I think for you guys it's a very easy error to correct, but I'm really a newbie with React. Please help.

I believe the problem is the data structure you are using for storing selected values. Right now, it's a plain boolean, and you are dealing with an array.
You could do the following:
First, we change the selected to an array.
const [selected, setSelected] = useState([]);
Then, how can we identify each procedure in a unique way? Do they have an ID? By Title? Command? Let's suppose it's by title.
function clickHandler(title, command) {
commandSender(`${command}`)
if(selected.includes(title)) {
setSelected(selected.filter(s => s !== title)) // we unselected our procedure
} else {
setSelected([...selected, title]) // we add our procedure to the selected
}
}
Finally, you should change the rendering of your procedures, and remove the useEffect, as it's unnecessary.
<a
className={`bt-green ${selected.includes(procedure.title) ? "active" : ""}`}
onClick={() => clickHandler(procedure.title, 'SUCCESS')}
>
{procedure.title}
</a>
Furthermore, you could create a function to determine if your procedure is selected, so you don't have to write every time selected.includes... :
function isSelected(procedure) {
return selected.includes(procedure);
}

Related

Capture this.name of a button in React onClick [duplicate]

This question already has an answer here:
ReactJS, event.currentTarget doesn't have the same behavior as Vanilla Javascript
(1 answer)
Closed last month.
I want to capture the name attribute of a button on click in React.
I tried the following code block:
export function TestButton(props){
function logName() {
console.log(this.name)
}
return(
<button name={props.name} onClick={event => logName(event.currentTarget.getAttribute("name"))} type='button'>{props.text}</button>
)
}
My expectation was that this code would allow me to create a button that displays the name in the console log:
<TestButton name='helloWorld' text='Click Me'/>
Instead I get an alert that this is undefined. This is in spite of my ability to see the name when I inspect the element.
I have also tried target instead of currentTarget with no luck. I also tried event.currentTarget.name without the results I desire.
What did i miss?
In react, I believe this is reserved for classes, whereas you are defining a functional component. In a functional component, the comparable state value would be stored with useState(). That being said, I'm not sure I see the need for that here, since this button is getting its props from somewhere and the value of name and text are not changing in this component. I would code it this way:
export const TestButton = ({props}) => {
return(
<button name={props.name} onClick={() => console.log(props.name)}>
{props.text}
</button>
)
}
Now to go a bit further, maybe you want to use state wherever this button is being rendered. That could look like this:
import {TestButton} from "./someFile";
const [name, setName] = useState("some-button");
const [text, setText] = useState("click me!");
// now there could be some code here that decides what the name or text would be
// and updates the values of each with setName("name") and setText("text")
const Page = () => (
<>
<TestButton props={{name: name, text: text}} />
</>
)
This is all building off your current code, but now I will combine everything in a way that makes sense to me:
import {useState} from "react";
const [name, setName] = useState("some-button");
const [text, setText] = useState("click me!");
// some code to determine/change the value of the state vars if necessary
const TestButton = ({name, text}) => {
return(
<button name={name} onClick={() => console.log(name)}>
{text}
</button>
)
}
export const Page = () => (
<>
<TestButton name={name} text={text} />
</>
)
Pleas try as follows:
export function TestButton(props){
function logName() {
console.log(props.name)
}
return(
<button name={props.name} onClick={() => logName()} type='button'>{props.text}</button>
)
}
Try this
export function TestButton(props){
const logName = (e, name) => {
console.log("name attribute ->", name)
}
return(
<button name={props.name} onClick={ (e) => logName(e, props.name)} type='button'>{props.text}</button>
)
}

React state updating without setstate, takes on state of deleted item (SOLVED)

I have a React notes app that has a delete button, and a state for user confirmation of deletion.
Once user confirms, the 'isConfirmed' state is updated to true and deletes the item from MongoAtlas and removes from notes array in App.jsx.
The problem is, the note that takes the index (through notes.map() in app.jsx I'm assuming) of the deleted notes position in the array has the 'isConfirmed' state set to true without calling setState. Thus, bugging out my delete button to not work for that specific note until page refresh.
I've included relevant code from my Delete Component:
function DeletePopup(props) {
const mountedRef = useRef(); //used to stop useEffect call on first render
const [isConfirmed, setIsConfirmed] = useState(false);
const [show, setShow] = useState(false);
function confirmDelete() {
// console.log("user clicked confirm");
setIsConfirmed(true);
// console.log(isConfirmed);
handleClose();
}
useEffect(() => {
// console.log("delete useEffect() run");
if (mountedRef.current) {
props.deleteNote(isConfirmed);
}
mountedRef.current = true;
}, [isConfirmed]);
Note Component:
function Note(props) {
function deleteNote(isConfirmed) {
props.deleteNote(props.id, { title: props.title, content: props.content }, isConfirmed);
console.log("note.deleteNote ran with confirmation boolean: " + isConfirmed);
}
return <Draggable
disabled={dragDisabled}
onStop={finishDrag}
defaultPosition={{ x: props.xPos, y: props.yPos }}
>
<div className='note'>
<h1>{props.title}</h1>
<p>{props.content}</p>
<button onClick={handleClick}>
{dragDisabled ? <LockIcon /> : <LockOpenIcon />}
</button>
<EditPopup title={props.title} content={props.content} editNote={editNote} />
<DeletePopup deleteNote={deleteNote} />
</div>
</Draggable>
}
App Component:
function App() {
const [notes, setNotes] = useState([]);
function deleteNote(id, deleteNote, isConfirmed) {
if (!isConfirmed) return;
axios.post("/api/note/delete", deleteNote)
.then((res) => setNotes(() => {
return notes.filter((note, index) => {
return id !== index;
});
}))
.catch((err) => console.log(err));
}
return (
<div id="bootstrap-override">
<Header />
<CreateArea
AddNote={AddNote}
/>
{notes.map((note, index) => {
return <Note
key={index}
id={index}
title={note.title}
content={note.content}
xPos={note.xPos}
yPos={note.yPos}
deleteNote={deleteNote}
editNote={editNote}
/>
})}
<Footer />
</div>);
}
I've tried inserting log statements everywhere and can't figure out why this is happening.
I appreciate any help, Thanks!
EDIT: I changed my Notes component to use ID based on MongoAtlas Object ID and that fixed the issue. Thanks for the help!
This is because you are using the index as key.
Because of that when you delete an element you call the Array.filter then you the elements can change the index of the array which when React tries to rerender the notes and as the index changes it cannot identify the note you've deleted.
Try using a unique id (e.g. an id from the database or UUID) as a key instead.
I hope it solves your problem!

Creating like button for multiple items

I am new to React and trying to learn more by creating projects. I made an API call to display some images to the page and I would like to create a like button/icon for each image that changes to red when clicked. However, when I click one button all of the icons change to red. I believe this may be related to the way I have set up my state, but can't seem to figure out how to target each item individually. Any insight would be much appreciated.
`
//store api data
const [eventsData, setEventsData] = useState([]);
//state for like button
const [isLiked, setIsLiked] = useState(false);
useEffect(() => {
axios({
url: "https://app.ticketmaster.com/discovery/v2/events",
params: {
city: userInput,
countryCode: "ca",
},
})
.then((response) => {
setEventsData(response.data._embedded.events);
})
.catch((error) => {
console.log(error)
});
});
//here i've tried to filter and target each item and when i
console.log(event) it does render the clicked item, however all the icons
change to red at the same time
const handleLikeEvent = (id) => {
eventsData.filter((event) => {
if (event.id === id) {
setIsLiked(!isLiked);
}
});
};
return (
{eventsData.map((event) => {
return (
<div key={event.id}>
<img src={event.images[0].url} alt={event.name}></img>
<FontAwesomeIcon
icon={faHeart}
className={isLiked ? "redIcon" : "regularIcon"}
onClick={() => handleLikeEvent(event.id)}
/>
</div>
)
`
Store likes as array of ids
const [eventsData, setEventsData] = useState([]);
const [likes, setLikes] = useState([]);
const handleLikeEvent = (id) => {
setLikes(likes.concat(id));
};
return (
<>
{eventsData.map((event) => {
return (
<div key={event.id}>
<img src={event.images[0].url} alt={event.name}></img>
<FontAwesomeIcon
icon={faHeart}
className={likes.includes(event.id) ? "redIcon" : "regularIcon"}
onClick={() => handleLikeEvent(event.id)}
/>
</div>
);
})}
</>
);
Your issue is with your state, isLiked is just a boolean true or false, it has no way to tell the difference between button 1, or button 2 and so on, so you need a way to change the css property for an individual button, you can find one such implementation by looking Siva's answer, where you store their ids in an array

How can I avoid updating all instances of my function component with the useState hook in React.js?

TL;DR I am making a reusable Button function component. My useState() hook for the button label is updating every Button instance. How can I prevent this?
I am very new to React and building a Book Finder app in order to learn. So far my app has a BookList and a ReadingList. Each BookDetail in either list has a Button to add/remove that book from the ReadingList. The add/remove function works (phew), but using useState to update the Button's label updates every instance of the Button component, and not just the one that was clicked.
Buttons on books in the BookList start with label 'Add to Reading List', but if I click any of them, all of them update to 'Remove from Reading List'.
I've tried moving the logic around into the Button component or either List component but I just end up breaking the function.
App.js
function App() {
const books = useState([])
const [booksToRead, setBooksToRead] = useState([])
const [addRemove, setAddRemove] = useState(true)
const [label, setLabel] = useState('Add to Reading List')
function handleAddBook(book) {
const newID = book.title_id
if( (typeof booksToRead.find(x => x.title_id === newID)) == 'undefined' ) {
setBooksToRead([...booksToRead, book])
}
}
function handleRemoveBook(book) {
console.log(book)
const array = booksToRead
const index = array.indexOf(book)
const newArray = [...array.slice(0, index), ...array.slice(index +1)]
setBooksToRead(newArray)
}
function addOrRemove(book) {
if( addRemove ) {
handleAddBook(book)
setLabel('Remove from Reading List')
setAddRemove(false)
} else {
handleRemoveBook(book)
setLabel('Add to Reading List')
setAddRemove(true)
}
}
return (
<main>
<BookList books={books} handleAddBook={handleAddBook} addOrRemove={addOrRemove} label={label} />
<ReadingList booksToRead={booksToRead} handleRemoveBook={handleRemoveBook} />
</main>
);
}
export default App;
BookList.js
function BookList ({ book, label, handleAddBook, addOrRemove }) {
return (
<div className="booklist">
{BookData.map((book, index) => {
const onAddBook = () => addOrRemove(book)
return (
<div key={index} className="card">
<BookDetail key={book.title_id} book={book} />
<Button key={index + 'btn'} label={label} onClick={onAddBook} />
</div>
)
})}
</div>
)
}
export default BookList
And finally, Button.js
export default function Button({ styleClass, label, onClick }) {
return (
<button className='btn' onClick={(event) => onClick(event)}>
{label}
</button>
)
}
Unstyled example in codesandbox: https://codesandbox.io/s/cool-rgb-fksrp
Can you make these changes and let me know if there any progress:
<Button label={label} onClick={() => addOrRemove(book)} />
<button className='btn' onClick={onClick}>
It looks like that in button you are passing event instead of book as function parameter
As it is right now, you are declaring a single label and using that same one on all your book entries. This is why they all display the same label. You would need to keep track of the label of each book, for instance by keeping the label as a field in the book object.
For example:
const books = useState([{ label: 'Add to reading list', addRemove: true }])
And then:
function addOrRemove(book) {
if( book.addRemove ) {
handleAddBook(book)
book.label = 'Remove from Reading List'
book.addOrRemove = false
} else {
handleRemoveBook(book)
book.label = 'Add to Reading List'
book.addOrRemove = true
}
}
This way, each book has it's own label.

how do I output the filtered todo list in React TypeScript

It is console logging the right array out all the time, but the point here is that it should be outputting that in the 'TodoList.tsx'. Not sure how to get that fixed in this case. Anyone who could help me with this. To see the bigger picture, please click on this link:
Link to codesandbox todo
I want the returned value from App.js currentFilter function pass it to TodoListItem.js, so it will update the map function constantly when user clicks on filter buttons.
// TodoFilter
import React from 'react';
interface TodoListFilter {
currentFilter: CurrentFilter;
}
export const TodoFilter: React.FC<TodoListFilter> = ({ currentFilter }) => {
return (
<ul>
Filter
<li onClick={() => currentFilter('All')}>All</li>
<li onClick={() => currentFilter('Complete')}>Completed</li>
<li onClick={() => currentFilter('Incomplete')}>Incompleted</li>
</ul>
)
}
// App.js
const currentFilter: CurrentFilter = filterTodo => {
let activeFilter = filterTodo;
switch (activeFilter) {
case 'All':
return todos;
case 'Complete':
return todos.filter(t => t.complete);
case 'Incomplete':
return todos.filter(t => !t.complete);
default:
console.log('Default');
}
}
return (
<React.Fragment>
<TodoList
todos={todos}
toggleTodo={toggleTodo}
deleteTodo={deleteTodo}
editTodo={editTodo}
saveEditedTodo={saveEditedTodo}
getEditText={getEditText}
/>
<TodoFilter currentFilter={currentFilter}/>
<AddTodoForm addTodo={addTodo}/>
</React.Fragment>
)
// TodoListItem
import React from 'react';
import { TodoListItem } from "./TodoListItems";
interface TodoListProps {
todos: Array<Todo>;
toggleTodo: ToggleTodo;
deleteTodo: DeleteTodo;
editTodo: EditTodo;
getEditText: GetEditText;
saveEditedTodo: SaveEditedTodo;
currentFilter: CurrentFilter;
}
export const TodoList: React.FC<TodoListProps> = ({ todos, toggleTodo, deleteTodo, editTodo, getEditText, saveEditedTodo, currentFilter }) => {
return (
<ul>
{todos.map((todo, i) => {
return <TodoListItem key={i}
todo={todo}
toggleTodo={toggleTodo}
deleteTodo={deleteTodo}
editTodo={editTodo}
saveEditedTodo={saveEditedTodo}
getEditText={getEditText}
/>
})}
</ul>
)
}
//Folder structure
src
-App.tsx
-AddTodoForm.tsx
-TodoFilter.tsx
-TodoList.tsx
The reason why the list not updating is that currentFilter passed as a prop to TodoList component is not used there at all.
Please consider two ways of solving it:
Pass a full list + filter object and apply filter inside TodoList
Apply filter object on the list at App component level and pass filtered list to TodoList component.
Personally I would go with the second approach but it's up to you :)
You need to create two arrays.One is original and second is filtered like this in your example.
const [todos, setTodos] = useState(initialTodos);
const [filtered, setFiltered] = useState(initialTodos);
Now you need to send filtered array in list component.Any updation or deletion you have to make on your todos array.And in currentFilter,you have to filter from original array that is todos and set it to filtered array in like this:
useEffect(() => {
setFiltered(todos);
}, [todos]);
Link of forked sandbox : link
Let me know if this helps you.

Categories