How to update state of elements from array.map() separately? - reactjs

In my code, I am trying to retrieve background images depending on the background data on the object, however those backgrounds will also change depending on the state of the element.
First of all, my data array looks like this:
const Cube = [
{
name: "foo"
faces: [
{
data: [
[{bonus: "bar", bg: "bar2"}],
...
],
...
},
...
],
...
},
...
];
And this is how my App.js looks:
function App() {
const [cellState, setCellState] = useState("inactive");
return (
<div className="App">
<div id="cube-container">
{Cube[0].faces[0].data.map((row) => {
return row.map((cell, index) => {
return (
<img
src={require(`./assets/cube/Square_Cube_Icon/${cell.bg}${
cellState === "inactive" ? "_Unlit" : "_Partial"
}.png`)}
alt={`${cell.bg} background`}
className="cellItem"
onClick={() => setCellState("active")}
state={cellState}
key={index}
/>
);
});
})}
</div>
</div>
);
}
Question is, in the 4x4 grid output, if I click on any item, instead of changing the clicked elements background because of state change, it changes background of every cell, which should happen since the useState is shared between all of them.
How can I make it so every element of the map function has their own state that I can update separately?
Found the solution:
Separating it as a component and passing key and cell values as props, then creating and manipulating the state inside fixed the issue.Selected answer also solves the issue.
Here's the solution for future readers.
App.js
function App() {
return (
<div className="App">
<div id="cube-container">
{CubeOfTruth[0].faces[0].data.map((row) => {
return row.map((cell, index) => {
return <CubeItem key={index} cell={cell} />;
});
})}
</div>
</div>
);
}
CubeItem.js
function CubeItem({ cell, index }) {
const [cellState, setCellState] = useState("inactive");
return (
<img
src={require(`../assets/cube/Square_Cube_Icon/${cell.bg}${
cellState === "inactive" || cellState === "partial" ? "_Unlit" : ""
}.png`)}
alt={`${cell.bg} background`}
className="cellItem"
onClick={() => setCellState("active")}
state={cellState}
key={index}
/>
);
}

Here's an easy way to manage multiple states using an object with its keys representing your cells and the values representing whether it is active or inactive. This is also easy to generalize so if you add more cells then all you have to do is just add it to the initialState & rest of your code should work as is.
function App() {
// const [cellState, setCellState] = useState("inactive");
const initialState = {
bar1: "inactive",
bar2: "inactive",
bar3: "inactive",
bar4: "inactive",
}
const [cellState, SetCellState] = React.useState(initialState);
const handleSetCellState = (key) => {
setCellState({
...cellState,
[key]: "active",
});
};
return (
<div className="App">
<div id="cube-container">
{Cube[0].faces[0].data.map((row) => {
return row.map((cell, index) => {
return (
<img
src={require(`./assets/cube/Square_Cube_Icon/${cell.bg}${
cellState[cell.bg] === "inactive" ? "_Unlit" : "_Partial"
}.png`)}
alt={`${cell.bg} background`}
className="cellItem"
onClick={() => handleSetCellState(cell.bg)}
state={cellState[cell.bg]}
key={index}
/>
);
});
})}
</div>
</div>
);
}

Related

Nested state (react) with onclick events

I was given the task to build a recursive tree with some functionality. I managed to build a tree using a recursive component, I attach the code below.
function App() {
const [data, setData] = useState([{
id: 1,
name: "node 1",
children: [{
id: 2,
name: "node 1.1",
children: [{
id: 8,
name: "node 1.1.1",
children: []
}]
},{
id: 3,
name: "node 1.2",
children: [{
id: 4,
name: "node 1.2.1",
children: [{
id: 5,
name: "node 1.2.1.1",
children: []
}]
}]
}]
}])
return (
<div className="App">
<Tree data = {data} setData = {setData} margin={15}/>
<button>Add child</button>
</div>
);
}
and
const Tree = ({data, margin, setData}) => {
return (
<Fragment>
{data.map((element, index) => (
<div>
<div className="tier">
<div
style={{marginLeft: `${margin}px`}}
>
{element.name}
</div>
</div>
{element.children && element.children.length ? <Tree
data={element.children}
setData={setData}
margin={margin + 15}
/> : false}
</div>))}
</Fragment>
);
};
I need to add a node selection when clicking on it. I have an idea about the implementation: create a state in which the ID of the desired node will be stored, and design it using CSS.
But I have absolutely no idea how to add a child node to the selected node when the button is clicked.
You did half of the job and your idea about storing id is the best overall, you could actually store the selected item itself but it will be not as great as it sounds due to mutation of this item, you will not be able to trigger dependent hooks update for it.
So you only need to store the id of selected item and a few utility things to simplify the work with the tree. On the code below you can see the flatTree that was calculated with useMemo from the original data. It just flattens your tree to an array, so you will be able to easilly find real selected item and to calculate the next id that will be inserted on button click. And to add a few more props to the Tree component itself. onClick to handle actual click and selectedTreeItem just to add some classes for selected item (optional).
import "./styles.css";
import { Fragment, useEffect, useMemo, useState } from "react";
export default function App() {
const [data, setData] = useState([{id:1,name:"node 1",children:[{id:2,name:"node 1.1",children:[{id:8,name:"node 1.1.1",children:[]}]},{id:3,name:"node 1.2",children:[{id:4,name:"node 1.2.1",children:[{id:5,name:"node 1.2.1.1",children:[]}]}]}]}]);
const [selectedTreeItemId, setSelectedTreeItemId] = useState();
const flatTree = useMemo(() => {
const flat = (item) => [item, ...(item.children || []).flatMap(flat)];
return data.flatMap(flat);
}, [data]);
const selectedTreeItem = useMemo(() => {
if (!selectedTreeItemId || !flatTree) return undefined;
return flatTree.find((x) => x.id === selectedTreeItemId);
}, [flatTree, selectedTreeItemId]);
const getNextListItemId = () => {
const allIds = flatTree.map((x) => x.id);
return Math.max(...allIds) + 1;
};
const onTreeItemClick = ({ id }) => {
setSelectedTreeItemId((curr) => (curr === id ? undefined : id));
};
const onAddChildClick = () => {
if (!selectedTreeItem) return;
if (!selectedTreeItem.children) selectedTreeItem.children = [];
const newObj = {
id: getNextListItemId(),
name: `${selectedTreeItem.name}.${selectedTreeItem.children.length + 1}`,
children: []
};
selectedTreeItem.children.push(newObj);
// dirty deep-clone of the tree to force "selectedTreeItem"
// to be recalculated and its !reference! to be updated.
// so any hook that has a "selectedTreeItem" in depsArray
// will be executed now (as expected)
setData((curr) => JSON.parse(JSON.stringify(curr)));
};
useEffect(() => {
console.log("selectedTreeItem: ", selectedTreeItem);
}, [selectedTreeItem]);
return (
<div className="App">
<Tree
data={data}
margin={15}
onTreeItemClick={onTreeItemClick}
selectedTreeItem={selectedTreeItem}
/>
<button
type="button"
disabled={!selectedTreeItem}
onClick={onAddChildClick}
>
Add child
</button>
</div>
);
}
const Tree = ({ data, margin, onTreeItemClick, selectedTreeItem }) => {
return (
<Fragment>
{data.map((element) => (
<div key={element.id}>
<div
className={`tier ${
selectedTreeItem === element ? "selected-list-item" : ""
}`}
onClick={() => onTreeItemClick(element)}
>
<div style={{ marginLeft: `${margin}px` }}>
{element.id}: {element.name}
</div>
</div>
{element.children?.length > 0 && (
<Tree
data={element.children}
margin={margin + 15}
onTreeItemClick={onTreeItemClick}
selectedTreeItem={selectedTreeItem}
/>
)}
</div>
))}
</Fragment>
);
};
Nested array in your date tree you should map as well. It should look like that
{element.children?.map(el, I => (
<p key={I}> {el. name}</p>
{el.children?.map(child, ind => (
<p key={ind}>{child.name}</p>
)}
)}
the ? is very important, because it would work only if there was a date like that.

React: how to use spread operator in a function that toggles states?

I made an example of my question here:
EXAMPLE
I'm mapping an array of objects that have a button that toggles on click, but when clicking on the button every object is changed.
This is the code
export default function App() {
const [toggleButton, setToggleButton] = useState(true);
// SHOW AND HIDE FUNCTION
const handleClick = () => {
setToggleButton(!toggleButton);
};
return (
<div className="App">
<h1>SONGS</h1>
<div className="container">
{/* MAPPING THE ARRAY */}
{songs.map((song) => {
return (
<div className="song-container" key={song.id}>
<h4>{song.name}</h4>
{/* ON CLICK EVENT: SHOW AND HIDE BUTTONS */}
{toggleButton ? (
<button onClick={handleClick}>PLAY</button>
) : (
<button onClick={handleClick}>STOP</button>
)}
</div>
);
})}
</div>
</div>
);
}
I know I should be using spread operator, but I couldn't get it work as I spected.
Help please!
Of course every object will change because you need to keep track of toggled state for each button. Here is one way to do it:
import { useState } from "react";
import "./styles.css";
const songs = [
{
name: "Song A",
id: "s1"
},
{
name: "Song B",
id: "s2"
},
{
name: "Song C",
id: "s3"
}
];
export default function App() {
const [toggled, setToggled] = useState([]);
const handleClick = (id) => {
setToggled(
toggled.indexOf(id) === -1
? [...toggled, id]
: toggled.filter((x) => x !== id)
);
};
return (
<div className="App">
<h1>SONGS</h1>
<div className="container">
{songs.map((song) => {
return (
<div className="song-container" key={song.id}>
<h4>{song.name}</h4>
{toggled.indexOf(song.id) === -1 ? (
<button onClick={() => handleClick(song.id)}>PLAY</button>
) : (
<button onClick={() => handleClick(song.id)}>STOP</button>
)}
</div>
);
})}
</div>
</div>
);
}
There are many ways to do it. Here, if an id is in the array it means that button was toggled.
You can also keep ids of toggled buttons in object for faster lookup.
One way of handling this requirement is to hold local data into states within the Component itself.
I have created a new Button Component and manages the toggling effect there only. I have lifted the state and handleClick method to Button component where it makes more sense.
const Button = () => {
const [toggleButton, setToggleButton] = useState(true);
const click = () => {
setToggleButton((prevValue) => !prevValue);
};
return <button onClick={click}>{toggleButton ? "Play" : "Stop"}</button>;
};
Working Example - Codesandbox Link

How to pass an object as an argument using a React onClick event?

I have an array of objects, and I rendered the names from it. What I would like to do is when I click on them, I get the exact object that contains the name I clicked on, and then render those other data out.
export default function App() {
const array = [
{
id: 1,
name: 'John',
num: '0123'
},
{
id: 2,
name: 'Dave',
num: '456'
},
{
id: 3,
name: 'Bruce',
num: '789'
},
]
const handleClick = (e) => {
console.log(e.target)
}
return (
<div className="App">
{array.map((item, index) => {
return (
<div key={index}>
<p onClick={handleClick}>{item.name}</p>
</div>
)
})}
</div>
);
}
You need:
add to function handleSelect(userID) with argument userID:
const handleSelect = (userId) => {
const currentUser = array.filter(item => item.id === userId)[0]
console.log(currentUser) // or return
}
Add onClick event to div or p
<div className="App">
{array.map((item, index) => {
return (
<div key={index}>
<p onClick={() => handleSelect(item.id)}>{item.name}</p>
</div>
)
})}
</div>
Give some data-* attribute to the clicked element.
Find the item by id.
Do not use the index as a key in React. Use unique id.
Wrap functions inside React.useCallback.
export default function App() {
const array = [
{
id: 1,
name: 'John',
num: '0123'
},
{
id: 2,
name: 'Dave',
num: '456'
},
{
id: 3,
name: 'Bruce',
num: '789'
},
]
const handleClick = React.useCallback((e) => {
const clickedId = e.target.getAttribute('data-id');
const item = array.find(({ id }) => id === clickedId);
console.log(item);
}, []);
return (
<div className="App">
{array.map((item) => {
return (
<div key={item.id}>
<p data-id={item.id} onClick={handleClick}>{item.name}</p>
</div>
)
})}
</div>
);
}
This would do:
const handleClick = (id, name, num) => {
console.log(`id=${id}, name=${name}, num=${num} `);
};
return (
<div className="App">
{array.map((item, index) => {
return (
<div key={index}>
<p onClick={()=>handleClick(item.id, item.name, item.num)}>{item.name}</p>
</div>
);
})}
</div>
);
Here is a codesandbox link: for the above
Ofcourse, you can try to re-access the object. Check this out:
const handleClick = (id, name, num) => {
console.log(`Easy way : id=${id}, name=${name}, num=${num} `);
//Re-access the object. But why?
const desiredObjArr = array.filter(obj=> obj.id=id);
console.log(`I get this in a tough way: ${desiredObjArr[0].name}, ${desiredObjArr[0].id}, ${desiredObjArr[0].num}`);
};
The temptation is to treat the React onClick as if it was the same as an onclick attribute in an html tag. But while in the latter you would then call the function you want to execute along with it's argument (say, like so: <p onclick="handleClick(item)", in the former you wrap the function you are calling in another anonymous function. Following those strictures, to make your own code work as you want it to, you only have to revise it slightly like so:
const handleClick = (item) => {
console.log(item.id, item.name, item.num)
}
return (
<div className="App">
{array.map((item, index) => {
return (
<div key={index}>
<p onClick={()=>{handleClick(item)}}>{item.name}</p>
</div>
)
})}
</div>
);
Here's a working Stackblitz: https://stackblitz.com/edit/react-6ywm5v

React onClick to affect only this iteration

I have a page that uses an object that contains lists within lists. I have all the components showing the data correctly, but I'm trying to add a toggle button for each primary list item so you can show/hide their child lists. I had previously made something that would affect EVERY instance of the component when clicked, so when you click the expand button it would toggle the child lists of EVERY primary item.
React is new to me and I'm using this project partially as a learning tool. I believe this has to do with binding state to the specific instance of the component, but I'm not sure how or where to do this.
Here is the component:
const SummaryItem = props => {
const summary = props.object;
return(
<div className="summary_item">
{Object.entries(summary).map( item =>
<div>
Source: {item[0]} <br />
Count: {item[1].count} <br />
<button onClick={/*expand only this SummaryItemList component*/}>expand</button>
<SummaryItemList list={item[1].items} />
</div>)
}
</div>
);
}
I previously had a state hook that looked like:
const [isExpanded, setIsExpanded] = useState(false);
const toggle = () => setIsExpanded(!isExpanded);
And in my render function the button had the toggle function in the onClick:
<button onClick={toggle}>expand</button> and I had a conditional if(isExpanded) with two renders, one with the SummaryItemList component and one without.
Is there a better way to do this besides mapping the object, and how do I bind the state of the toggle to affect only the instance it's supposed to affect?
I think you maybe forgot to give each item an isExpanded, the best way to do this is to split up your items and item in different components (in the example below it List for items and Item for item).
const { useState } = React;
const Item = ({ name, items }) => {
const [isExpanded, setIsExpanded] = useState(false);
const toggle = () => {
setIsExpanded((s) => !s);
};
return (
<li>
{name}
{items && (
<React.Fragment>
<button onClick={toggle}>
{isExpanded ? '-' : '+'}
</button>
{isExpanded && <List data={items} />}
</React.Fragment>
)}
</li>
);
};
const List = ({ data }) => {
return !data ? null : (
<ul>
{Object.entries(data).map(([key, { items }]) => (
<Item key={key} items={items} name={key} />
))}
</ul>
);
};
const App = () => {
const data = {
A: {
items: {
AA1: { items: { AAA1: {}, AAA2: {} } },
AA2: { items: { AAA: {} } },
},
},
};
return <List data={data} />;
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

How to show/hide an item of array.map()

I want to show/hide a part of JSX depending on isCommentShown state property. But as this part is inside a map loop isCommentShown acts for all mapped items not only the current one. So when I toggleComment every comment inside a loop is shown/hidden. I imagine this can be solved by moving everything into a separate component because every component has its own state. But I wonder if I can can solve this without that.
const SearchResults = () => {
const [isCommentShown, setIsCommentShown] = useState(false);
const toggleComment = () => {
setIsCommentShown(!isCommentShown);
};
return (
<>
{props.search_results.map(obj =>
<div key={obj.id}>
{ obj.comment ? <img onClick={toggleComment}/> : null }
<div>{obj.text}</div>
{ isCommentShown ? <p>{obj.comment}</p> : null }
</div>
)}
</>
);
};
You could use the useState hook to create an object that will keep all the search result ids as keys and a boolean value indicating if the comment should be shown or not.
Example
const { useState, Fragment } = React;
const SearchResults = props => {
const [shownComments, setShownComments] = useState({});
const toggleComment = id => {
setShownComments(prevShownComments => ({
...prevShownComments,
[id]: !prevShownComments[id]
}));
};
return (
<Fragment>
{props.search_results.map(obj => (
<div key={obj.id}>
{obj.comment ? (
<button onClick={() => toggleComment(obj.id)}>Toggle</button>
) : null}
<div>{obj.text}</div>
{shownComments[obj.id] ? <p>{obj.comment}</p> : null}
</div>
))}
</Fragment>
);
};
ReactDOM.render(
<SearchResults
search_results={[
{ id: 0, text: "Foo bar", comment: "This is rad" },
{ id: 1, text: "Baz qux", comment: "This is nice" }
]}
/>,
document.getElementById("root")
);
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
Instead of storing true or false, you must store the comment id to show provided you only want to show one comment at a time. Its important to uniquely identify the item to be expanded
const SearchResults = () => {
const [commentShown, setCommentShown] = useState({});
const toggleComment = (id) => {
setCommentShown(prev => Boolean(!prev[id]) ? {...prev, [id]: true} : {...prev, [id]: false});
};
return (
<>
{props.search_results.map(obj =>
<div key={obj.id}>
{ obj.comment ? <img onClick={() => toggleComment(obj.id)}/> : null }
<div>{obj.text}</div>
{ commentShown[id] ? <p>{obj.comment}</p> : null }
</div>
)}
</>
);
};
If at all you need to open multiple comments simultaneously you can maintain a map of open ids
const SearchResults = () => {
const [commentShown, setCommentShown] = useState('');
const toggleComment = (id) => {
setCommentShown(prev => prev.commentShown !== id? id: '');
};
return (
<>
{props.search_results.map(obj =>
<div key={obj.id}>
{ obj.comment ? <img onClick={() => toggleComment(obj.id)}/> : null }
<div>{obj.text}</div>
{ commentShown === obj.id ? <p>{obj.comment}</p> : null }
</div>
)}
</>
);
};
Use the id to target the toggle on the comment you want.
More precisely, use the state to store the show/hide values, and pass the id to the onclick event to precise which comment to toggle. This should do the job:
class SearchResults extends React.Component {
constructor(props) {
super(props);
this.state = {};
for (let result of props.search_results) {
this.state[`${result.id}IsShown`] = true;
}
}
toggleComment(id) {
let key = `${result.id}IsShown`;
this.setState({[key]: !this.state[key]});
}
render() {
return (
<>
{this.props.search_results.map(result =>
<div key={result.id}>
{
result.comment
? <img onClick={() => toggleComment(result.id)}/>
: null
}
<div>{result.text}</div>
{ isCommentShown ? <p>{obj.comment}</p> : null }
</div>
)}
</>
);
}
}

Resources