I rendered a list of buttons using Array.map method in a function component. When I tried to pass state to each mapped array items, the rendered results changed all array items at once, instead of one by one.
Here is my code. Am I doing something wrong? Sorry if the question has been solved in other thread or I used the wrong method. This is my first React project and I am still learning. It would be very appreciated if someone could advise. Thank you!
import React, { useState } from "react"
export default function Comp() {
const [isActive, setActive] = useState(false)
const clickHandler = () => {
setActive(!isActive)
console.log(isActive)
}
const data = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
{ id: 3, name: "Charlie" },
]
const renderList = items => {
return items.map(item => (
<li key={item.id}>
<button onClick={clickHandler}>
{item.name} {isActive ? "active" : "not active"}
</button>
</li>
))
}
return (
<ul>{renderList(data)}</ul>
)
}
Put the individual item into a different component so that each has its own active state:
export default function Comp() {
const data = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
{ id: 3, name: "Charlie" },
]
const renderList = items => (
items.map(item => <Item key={item.id} name={item.name} />)
);
return (
<ul>{renderList(data)}</ul>
)
}
const Item = ({ name }) => {
const [isActive, setActive] = useState(false);
const clickHandler = () => {
setActive(!isActive);
};
return (
<li>
<button onClick={clickHandler}>
{name} {isActive ? "active" : "not active"}
</button>
</li>
);
};
You need to set the active-id in handling the click-event. That will in-turn render active/non-active conditionally:
Notice the flow (1) > (2) > (3)
function Comp() {
const [activeId, setActiveId] = React.useState(null);
const clickHandler = (item) => {
setActiveId(item.id) // (2) click-handler will set the active id
}
const data = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
{ id: 3, name: "Charlie" },
]
const renderList = items => {
return items.map(item => (
<li key={item.id}>
<button onClick={() => clickHandler(item)}> // (1) passing the clicked-item so that we can set the active-id
{item.name} {item.id === activeId ?
"active" : "not active" // (3) conditionally render
}
</button>
</li>
))
}
return (
<ul>{renderList(data)}</ul>
)
}
Good Luck...
Related
I have a simple problem here which I can't figure out. I wanted to hide menus depending on the condition.
For example if status contains at least one "Unlinked". "All unlinked images" menu should appear. I did used .some and I wonder why it doesn't return a boolean.
Codesandbox is here Click here
const showDeleteAllInvalidButton = () => {
const productImages = products?.flatMap((product) =>
product.productImages.filter((image) => image?.status)
);
return productImages?.some((e) => e?.status === "Invalid");
};
const showDeleteAllUnlinkedButton = () => {
const productImages = products?.flatMap((product) =>
product.productImages.filter((image) => image?.status)
);
return productImages?.some((e) => e?.status === "Unlinked");
};
The methods do return a boolean. But in the menus array you are assigning a function reference not the result -
show: showDeleteAllInvalidButton // function reference
show is now assigned a reference to the function showDeleteAllInvalidButton not the result of productImages?.some. You need to invoke the functions when assigning -
show: showDeleteAllInvalidButton() // result of productImages?.some
In your menus object you have a key that contains a function, so if you want this function to filter out your elements you need to execute the show method in side the filter method.
import React, { useState } from "react";
import Button from "#mui/material/Button";
import MenuItem from "#mui/material/MenuItem";
import KeyboardArrowDownIcon from "#mui/icons-material/KeyboardArrowDown";
import CustomMenu from "../../Menu";
const products = [
{
productName: "Apple",
productImages: [
{
status: "Unlinked"
}
]
},
{
productName: "Banana",
productImages: [
{
status: "Unlinked"
}
]
},
{
productName: "Mango",
productImages: [
{
status: "Unlinked"
},
{
status: "Unlinked"
}
]
}
];
const HeaderButtons = () => {
const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl);
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const showDeleteAllInvalidButton = () => {
const productImages = products?.flatMap((product) =>
product.productImages.filter((image) => image?.status)
);
return productImages?.some((e) => e?.status === "Invalid");
};
const showDeleteAllUnlinkedButton = () => {
const productImages = products?.flatMap((product) =>
product.productImages.filter((image) => image?.status)
);
return productImages?.some((e) => e?.status === "Unlinked");
};
const menus = [
{
id: 1,
name: "Invalid images",
action: () => {
handleClose();
},
show: showDeleteAllInvalidButton
},
{
id: 2,
name: "Unlinked images",
action: () => {
handleClose();
},
show: showDeleteAllUnlinkedButton
},
{
id: 3,
name: "All images",
action: () => {
handleClose();
},
show: () => true // not that I changed it to a function for consistency, but you can check for type in the filter method instead of running afunction
}
];
return (
<div>
<Button
color="error"
aria-haspopup="true"
aria-expanded={open ? "true" : undefined}
variant="outlined"
onClick={handleClick}
endIcon={<KeyboardArrowDownIcon />}
>
Options
</Button>
<CustomMenu anchorEl={anchorEl} open={open} onClose={handleClose}>
{menus
.filter((e) => e.show()) // here is your mistake
.map(
({
id = "",
action = () => {},
icon = null,
name = "",
divider = null
}) => (
<>
<MenuItem key={id} onClick={action} disableRipple>
{icon}
{name}
</MenuItem>
{divider}
</>
)
)}
</CustomMenu>
</div>
);
};
export default HeaderButtons;
In your code, it will always render because your filter functions are evaluating as truth.
I need to set the active classname to multiple onclick items inside a .map
I need the list of active items that were clicked
The items that were clicked will be highlighted in yellow, and when i click the same item again it should be removed from active list items.
const [data, setData] = useState([]);
const [activeIndicies, setActiveIndicies] = useState(() =>
data?.map(() => false)
);
useEffect(() => {
// This data is coming from the API response
const data = [
{ id: 1, name: "one" },
{ id: 2, name: "two" },
{ id: 3, name: "three" }
];
setData(data);
}, []);
return statement
onClick={() => {
setActiveIndicies(
activeIndicies.map((bool, j) => (j === index ? true : bool))
);
}}
Code Sandbox
Thank you.
try this one:
import "./styles.css";
import React, { useState, useEffect } from "react";
export default function App() {
const [data, setData] = useState([
{ id: 1, name: "one", active: false },
{ id: 2, name: "two", active: false },
{ id: 3, name: "three", active: false }
]);
return (
<div className="App">
<h2>Set active className to multiple items on .map</h2>
{data?.map((item, index) => {
return (
<p className={data[index].active ? "selected" : "notselected"}
onClick={() => {
setData((prevState) =>
_.orderBy(
[
...prevState.filter((row) => row.id !== item.id),
{ ...item, active: !item.active }
],
["name"],
["asc"]
)
);
}}
>
{item.name}
</p>
);
})}
</div>
);
}
You can acheive this by simply making some minor changes to your code:
// Changing the state value to an object so that it can
// store the active value for exact item ids
const [activeIndicies, setActiveIndicies] = useState({});
Then inside of .map()
....
// Checking if there is any value for the item id which is being mapped right now.
const selected = activeIndicies[item.id];
return (
<p
className={selected ? "selected" : "notselected"}
onClick={() => {
/* Then setting the state like below where it toggles
the value for particular item id. This way if item is
selected it will be deselected and vice-versa.
*/
setActiveIndicies((prevState) => {
const newStateValue = !prevState[item.id];
return { ...prevState, [item.id]: newStateValue };
});
}}
// Key is important :)
key={item.id}
>
{item.name}
</p>
);
Hello, friends!
I solved this problem in a more convenient way for me )
const data = [
{ id: 1, name: "Ann", selected: true },
{ id: 2, name: "Serg", selected: false },
{ id: 3, name: "Boris", selected: true },
];
//you don't even need to have a boolean field in the object -
//it will be added by itself on the first click on the element
// const data = [{ name:"Ann", id:1}, { name:"Serg", id:2 },{ name:"Boris", id:3 },]
const [users, setUsers] = useState(data); // no square brackets-[data]
function handleActive(item) {
setUsers((prev) => {
return prev.map((itemName) => {
if (itemName.name === item.name) {
return { ...itemName, selected: !itemName.selected };
// itemName.selected = !itemName.selected // or so
}
return itemName;
});
});
}
return (
{
users.map((item, i) => {
// let current = item.selected === true
// or so -> className={current === true ? 'users': ''}
return (
<div
onClick={() => handleActive(item)}
className={item.selected === true ? "active" : ""}
key={i}
>
{item.name}
</div>
);
})
}
);
I'm using this react-beautiful-dnd library to be able to reorder lists. However, even though I'm able to drag and drop and re-order, there is a flicker when I try to reorder lists.
You can see in the video:
enter image description here
Here is my code:
I added sorting to the array by order before mapping.
const Board = () => {
const [currentId, setCurrentId] = useState(null);
const lists = useSelector((state) => state.lists);
const dispatch = useDispatch();
const classes = useStyles();
useEffect(() => {
dispatch(getLists());
}, [currentId, dispatch]);
const onDragEnd = (result) => {
const { destination, source, draggableId, type } = result;
if(!destination) return;
const droppableIdStart = source.droppableId;
const droppableIdEnd = destination.droppableId;
const droppableIndexStart = source.index;
const droppableIndexEnd = destination.index;
const newState = [...lists];
// drag lists
if(type === 'list') {
const dragList = newState.splice(droppableIndexStart, 1);
newState.splice(droppableIndexEnd, 0, ...dragList);
// update order list to be index
newState.forEach((list, index) => {
list.order = index;
dispatch(updateList(list._id , { ...list }));
});
}
return newState;
}
// Arranging lists by order
const newArrange = (a,b) => {
return (a.order - b.order);
}
lists.sort(newArrange);
return (
<>
<DragDropContext onDragEnd={onDragEnd} >
<div>
<h1>Board</h1>
<Droppable droppableId="all-lists" direction="horizontal" type="list">
{ provided => (
<div className={classes.listContainer} {...provided.droppableProps} ref={provided.innerRef} >
{ lists.map((list, index) =>
(user?.result?.googleId === list?.creator || user?.result?._id === list?.creator) ?
<List key={list._id} title={list.title} cards={list.cards} currentId={list._id} index={index} /> :
null
)}
{addListFlag && (
<InputItem
value={listData.title}
btnText={"Add list"}
type={"list"}
placeholder={"Enter a list title..."}
changedHandler={handleChange}
closeHandler={closeHandlerBtn}
addItem={submitHandler}
/>
)}
{!addListFlag && (
<AddBtn btnText={"Add another list"} type={"list"} handleClick={handleAddition} />
)}
{provided.placeholder}
</div>
)}
</Droppable>
</div>
</DragDropContext>
</>
)
}
export default Board;
Sample of data:
{
_id: "6163cdd306d27b",
title: "a",
name: "first one",
order: "0",
cards:[
{
id: "0",
text: "a1",
_id: {_id: "61d0ca2c20d27e"}
},
{
id: "1",
text: "a2",
_id: {_id: "616541ec90630"}
},
{
id: "2",
text: "a3",
_id: {_id: "61606e609"}
}
]
}
Thank :)
It has probably todo with your getLists() call in the useEffect hook.
Is there an async function behind it? Do you get your lists from a server? If so, I suppose that useEffect is firing twice (once on drag end and once when it gets back the data from the backend or whatever getLists is doing), which leads to the flickering.
It would probably help to know what exactly getLists is.
Still learning some ropes of React.
I have the following code where I render a list of buttons:
import React, { useState, useEffect } from 'react';
const MyComponent = (props) => {
const [buttonList, setButtonList] = useState([]);
useEffect(() => { getButtonList()}, [])
const getButtonList = () => {
let data = [
{id: 1, name: 'One', selected: false },
{id: 2, name: 'Two', selected: false },
{id: 3, name: 'Three', selected: false }
]
setButtonList(data)
}
const ButtonItem = ({ item }) => {
const btnClick = (event) => {
const id = event.target.value
buttonList.forEach((el) => {
el.isSelected = (el.id == id) ? true : false
})
setButtonList(buttonList)
console.log('buttonList', buttonList)
}
return (
<button type="button"
className={ "btn mx-2 " + (item.isSelected ? 'btn-primary' : 'btn-outline-primary') }
onClick={btnClick} value={item.id}>
{item.name + ' ' + item.isSelected}
</button>
)
}
return (
<div className="container-fluid">
<div className="card mb-3 rounded-lg">
<div className="card-body">
{
buttonList.map(item => (
<ButtonItem key={item.id} item={item} />
))
}
</div>
</div>
</div>
)
}
export default MyComponent;
So the button renders:
[ One false ] [ Two false ] [ Three false ]
And when I click on any Button, I can see on Chrome React Tools that the value for isSelected of that button becomes true. I can also confirm that the specific array item for the button clicked in the dev tools for State (under hooks), the value is true.
The text for the button clicked does not show [ One true ] say if I clicked on button One. What am I missing here?
P.S. Note that I also want to change the class of the button, but I think that part will be resolved if I get the button isSelected value to be known across the component.
Code Demo:
https://codesandbox.io/s/laughing-keller-o5mds?file=/src/App.js:666-735
Issue: You are mutating the state object instead of returning a new state reference. You were also previously using === to compare a string id to a numerical id which was returning false for all comparisons.
Solution: Use a functional update and array.map to update state by returning a new array.
const ButtonItem = ({ item }) => {
const btnClick = event => {
const id = event.target.value;
setButtonList(buttons =>
buttons.map(button => ({
...button,
isSelected: button.id == id
}))
);
};
...
};
Suggestion: Factor out the btnClick handler, it only needs to be defined once. Curry the id property of item so you can use ===.
const btnClick = id => event => {
setButtonList(buttons =>
buttons.map(button => ({
...button,
isSelected: button.id === id
}))
);
};
Update the attaching of click handler to pass the item id
const ButtonItem = ({ item }) => {
return (
<button
type="button"
className={
"btn mx-2 " +
(item.isSelected ? "btn-primary" : "btn-outline-primary")
}
onClick={btnClick(item.id)} // <-- pass item.id to handler
value={item.id}
>
{item.name + " " + item.isSelected}
</button>
);
};
In your btnClick handler you are mutating your state, you should create a new value and assign it instead:
import React from "react";
import "./styles.css";
import { useState, useEffect } from "react";
const ButtonItem = ({ item, onClick }) => {
return (
<button
type="button"
className={
"btn mx-2 " + (item.isSelected ? "btn-primary" : "btn-outline-primary")
}
onClick={() => onClick(item.id)}
value={item.id}
>
{item.name + " " + item.isSelected}
</button>
);
};
const MyComponent = props => {
const [buttonList, setButtonList] = useState([]);
useEffect(() => {
getButtonList();
}, []);
const getButtonList = () => {
let data = [
{ id: 1, name: "One", isSelected: false },
{ id: 2, name: "Two", isSelected: false },
{ id: 3, name: "Three", isSelected: false }
];
setButtonList(data);
};
const btnClick = id => {
const updatedList = buttonList.map(el => ({
...el,
isSelected: el.id === id
}));
setButtonList(updatedList);
console.log("buttonList", updatedList);
};
return (
<div className="container-fluid">
<div className="card mb-3 rounded-lg">
<div className="card-body">
{buttonList.map(item => (
<ButtonItem key={item.id} item={item} onClick={btnClick} />
))}
</div>
</div>
</div>
);
};
export default MyComponent;
Component
Here is an example of code. What I want is to toggle the active state ( true / false) for each individual list item when I clicked it. I don't want to change them all at once.
Any help with this? Thanks in advance.
import React, { useState } from "react";
const App = () => {
const [active, setActive] = useState({});
const items = [
{ name: 'Item 1' },
{ name: 'Item 2' },
{ name: 'Item 3' }
];
handleClick = (index) => {
...
}
const list = items.map( (item, index) => {
return(
<li
key={index}
onClick={() => handleClick(index)}
className={active ? "active" : null}
>
{item.name}
</li>
)
})
return {
<>
<ul>{list}</ul>
</>
}
}
export default App;
First option - Introducing isActive property for each elements
I would introduce in the items an active property for each item and manipulate them based on the clicked item. Which needs to be added to useState where you can update the isActive property with .map().
Similarly like the following:
const [items, setItems] = useState([
{ name: 'Item 1', isActive: true },
{ name: 'Item 2', isActive: false },
{ name: 'Item 3', isActive: false }
]);
handleClick = (index) => {
setItems(prevItems =>
prevItems.map((e, i) => ({...e, isActive: i === index}))
);
}
const list = items.map( (item, index) => {
return(
<li
key={index}
onClick={() => handleClick(index)}
className={item.isActive ? 'active' : null}
>
{item.name}
</li>
)
})
return {
<>
<ul>{list}</ul>
</>
}
Second option - store the index for the clicked active element
Storing the index for the clicked element helps you the identify in .map() which one is the active one. So with a simple check with i === index you can add active class to the <li> element.
You can create a state for index with useState as the following:
const [index, setIndex] = useState(0);
const items = [
{ name: 'Item 1' },
{ name: 'Item 2' },
{ name: 'Item 3' }
];
handleClick = (i) => {
setIndex(i);
}
const list = items.map( (item, i) => {
return(
<li
key={i}
onClick={() => handleClick(i)}
className={i === index ? 'active' : null}
>
{item.name}
</li>
)
})
return {
<>
<ul>{list}</ul>
</>
}
I hope this helps!
Added comments inline.
const App = () => {
// active is active item, initially '' means, nothing selected.
const [active, setActive] = useState("");
const items = [{ name: "Item 1" }, { name: "Item 2" }, { name: "Item 3" }];
const list = items.map(({ name }) => (
<li
key={name /* avoid using index for key */}
onClick={() => setActive(name)}
className={active === name ? "active" : ""}
>
{name}
</li>
));
// return the Element, not {}
return (
<>
<ul>{list}</ul>
</>
);
};
export default App;