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

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>
)}
</>
);
}
}

Related

React - How to prevent parent re-render on prop change

I am making a calculator using react.
Every time I press a number button, the whole application re-renders, instead of the <Display />.
To prevent it, I tried 2 different approaches for App, But neither of them worked.
Here is the sandbox link.
Any help would be appreciated.
Put clickHandler inside of useCallback()
const App = () => {
const [screen, setScreen] = useState("0");
console.log("render");
const clickHandler = useCallback(
(val) => {
if (val === "AC") {
setScreen("");
return;
}
screen === "0" ? setScreen(val) : setScreen(screen + val);
},
[screen]
);
return (
<div className="App">
<div className="display">{screen}</div>
<ButtonList clickHandler={clickHandler} />
</div>
);
};
Put Display component inside of React.memo
const App = () => {
const [screen, setScreen] = useState("0");
console.log("render");
const clickHandler = (val) => {
if (val === "AC") {
setScreen("");
return;
}
screen === "0" ? setScreen(val) : setScreen(screen + val);
};
const displayComponent = () => {
return (
<>
<div className="display">{screen}</div>
<ButtonList clickHandler={clickHandler} />
</>
);
};
const MemoizedComponent = React.memo(displayComponent);
return (
<div className="App">
<MemoizedComponent />
</div>
);
};
And here's the ButtonList & Button component.
export const ButtonList = ({ clickHandler }) => {
const arr = [...Array.from(Array(10).keys()).reverse(), "AC"];
return (
<div className="buttons">
<div className="numbersWrapper">
{arr.map((item) => (
<Button
key={item}
clickHandler={clickHandler}
value={item.toString()}
/>
))}
</div>
</div>
);
};
export const Button = ({ value, clickHandler }) => {
return (
<button
name={value}
onClick={() => {
clickHandler(value); //where the clickEvent happens
}}
>
{value}
</button>
);
};
If you don't want a component re-render,You would have to define the click handler in another component that you would like to re-render.
So do it like this:
const App = () => {
console.log("render");
return (
<div className="App">
<childComponent />
</div>
);
};
export const childComponent = () => {
const [screen, setScreen] = useState("0");
const clickHandler = (val) => {
if (val === "AC") {
setScreen("");
return;
}
screen === "0" ? setScreen(val) : setScreen(screen + val);
};
return (
<>
<div className="display">{screen}</div>
<ButtonList clickHandler={clickHandler} />
</>
);
}
This way you prevent a particular component from re-rendering. But note that if you update a state or do anything from which causes re-renders from the parent component, It would equally re-render the child component.

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

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>
);
}

How to toggle class of a single element in a .map() function?

I am trying to toggle a class for a specific element inside a loop.
const ItemList: React.FC<ListItemUserProps> = (props) => {
const { items } = props;
const [showUserOpt, setShowUserOpt] = useState<boolean>(false);
function toggleUserOpt() {
setShowUserOpt(!showUserOpt);
}
const userOptVisible = showUserOpt ? 'show' : 'hide';
return (
<>
{items.map((t) => (
<React.Fragment key={t.userId}>
<div
className={`item ${userOptVisible}`}
role="button"
tabIndex={0}
onClick={() => toggleUserOpt()}
onKeyDown={() => toggleUserOpt()}
>
{t.userNav.firstName}
</div>
</React.Fragment>
))}
</>
);
};
export default ItemList;
When I click on an element, the class toggles for every single one.
You can create another component that can have it's own state that can be toggled without effecting other sibling components' state:
Child:
const ItemListItem: React.FC<SomeInterface> = ({ item }) => {
const [show, setShow] = useState<boolean>(false);
const userOptVisible = show ? "show" : "hide";
const toggleUserOpt = (e) => {
setShow((prevState) => !prevState);
};
return (
<div
className={`item ${userOptVisible}`}
role="button"
tabIndex={0}
onClick={toggleUserOpt}
onKeyDown={toggleUserOpt}
>
{item.userNav.firstName}
</div>
);
};
Parent:
const ItemList: React.FC<ListItemUserProps> = ({ items }) => {
return (
<>
{items.map((t) => (
<ItemListItem key={t.userId} item={t} />
))}
</>
);
};
If you simply adding classes to the element, I would keep it simple and use a handler to toggle the class using pure JS.
const handleClick = (e) => {
// example of simply toggling a class
e.currentTarget.classList.toggle('selected');
};
Demo:
const {
useState,
} = React;
// dummy data
const data = Array(20).fill(null).map((i, index) => `item ${(index + 1).toString()}`);
function App() {
const [items, setItems] = useState(data);
const handleClick = (e) => {
e.currentTarget.classList.toggle('selected');
};
return (
<div>
{items.map((item) => (
<button key={item} onClick={handleClick}>{item}</button>
))}
</div>
);
}
ReactDOM.render( <
App / > ,
document.getElementById("app")
);
.selected {
background: red;
}
<script crossorigin src="https://unpkg.com/react#17/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.production.min.js"></script>
<div id="app"></div>
I think it'd be best if you kept track of the index so that you could target a single item in your list. As it stands the boolean is going to change the styling for all as you haven't specified which one should get the className.
Add a useState hook to keep track of it like:
const [activeIndex, setActiveIndex] = useState(null);
Then create a new function:
function handleIndexOnClick(index) {
setActive(index);
}
Then in your map() function add index. You'll then need to pass index in to you className attribute and the onClick function. The end result for that bit should look like:
{items.map((t, index) => (
<React.Fragment key={t.userId}>
<div
className={`item ${activeIndex && items[activeIndex] ? 'show' : 'hide }`}
role="button"
tabIndex={0}
onClick={() => handleIndexOnClick(index)}
onKeyDown={() => toggleUserOpt()}
>
{t.userNav.firstName}
</div>
</React.Fragment>
))}

React js add filter

I want to add a filter in arrays of filters, but all filters are added in the same array in filters. There are some blocks for filters and every filter must be added in its array. Now, every filter is added in its array, but all other filters are updated in that array too.
export const DropDownBlock = () => {
const [filters, setFilters] = useState({
type: [],
license: [],
tag: [],
format: [],
});
const filterKey = Object.keys(item.filters);
const [checked, setChecked] = useState([]);
return (
<section className="filterSection">
{filterKey.map((f, index) => {
const filterArray = [];
const photoItems = photos.map((p) => {
return p.filters[filterKey[index]];
});
photoItems.map((p) => {
if (filterArray.indexOf(p) < 0) {
filterArray.push(p);
}
});
const handleFilters = (filters, category) => {
const newFilters = { ...filters };
newFilters[category] = filters;
setFilters(newFilters);
};
return (
<div className="" key={f}>
<div
className="dropDownTitleBlock"
onClick={() => (isOpen ? setIsOpen(false) : setIsOpen(true))}
>
{isOpen ? <MdKeyboardArrowDown /> : <MdKeyboardArrowRight />}
<h5 className="dropDownTitle">{f}</h5>
</div>
{isOpen && (
<div className="dropDownCategoryBlock">
{filterArray.map((filter) => {
switch (f) {
case filterKey[index]:
return (
<Checkbox
filter={filter}
handleFilters={(filters) =>
handleFilters(filters, filterKey[index])
}
checked={checked}
setChecked={setChecked}
/>
);
}
})}
</div>
)}
</div>
);
})}
</section>
);
};

TypeError: Cannot set property 'eName' of undefined App.inputChangeHandler

Once the user updates the input inbox displayed eName should change. It is not updating the array. What is causing this?
Error message:
TypeError: Cannot set property 'eName' of undefined App.inputChangeHandler
Code:
class App extends Component {
state= {
product:[
{eName:"Anu", eNo:"1200", eSalary:"1000"},
{eName:"Jack", eNo: "1201", eSalary:"1200"},
{eName:"Ben", eNo: "1202", eSalary:"1300"}
],
showFlag:true,
}
inputChangeHandler(event,index){
const mProducts = this.state.product;
mProducts[index].eName = event.target.value;
console.log(event.target.value);
this.setState({
product:mProducts
})
}
deleteHandler(index){
console.log("delete clicked" + index);
const mProducts = this.state.product;
mProducts.splice(index,1);
this.setState({
product:mProducts
})
}
showHideHandler=()=>{
this.setState({
showFlag:!this.state.showFlag
})
console.log(this.state.showFlag);
}
render(){
let dp = null;
if (this.state.showFlag === true){
dp =(
<div className ="App">
{this.state.product.map((product,index) => {
return (
<Product
eName={product.eName}
eNo={product.eNo}
eSalary={product.eSalary}
key ={index}
click ={this.deleteHandler.bind(this.index)}
inputChange={this.inputChangeHandler.bind(this)}
/>)
})}
</div>
)
}
return(
<div className ="App">
{dp}
<hr/>
<button onClick={()=>this.showHideHandler()}>show hide</button>
<h2>{this.state.eName} </h2>
</div>
);
}
}
export default App;
Once the user updates the input inbox displayed eName should change. It is not updating the array.
The issue is because the inputChangeHandler function is not receiving the correct index. Please make the below changes:
define inputChangeHandler function as arrow function:
inputChangeHandler = (event,index) => {
const mProducts = this.state.product;
mProducts[index].eName = event.target.value;
console.log(event.target.value);
this.setState({
product:mProducts
})
}
Inside render function, while calling inputChangeHandler, pass both event and index
render(){
let dp = null;
if (this.state.showFlag === true){
dp =(
<div className ="App">
{this.state.product.map((product,index) => {
return (
<Product
eName={product.eName}
eNo={product.eNo}
eSalary={product.eSalary}
key ={index}
click ={this.deleteHandler.bind(this.index)}
inputChange={(e) => this.inputChangeHandler(e, index)}
/>)
})}
</div>
)
}
return(
<div className ="App">
{dp}
<hr/>
<button onClick={()=>this.showHideHandler()}>show hide</button>
<h2>{this.state.eName} </h2>
</div>
);
}
You are mutating state and do a setState with the mutated state in all your handlers. Because React does not see a change in state it won't re render (try to put a console.log in render and you'll see it won't show in console).
Here is how you set a value in state without mutating it:
inputChangeHandler(event, index) {
this.setState({
product: this.state.product.map((product, i) =>
i === index
? { ...product, eName: event.target.value }
: product
),
});
}
I would advice better naming for your variable, if you have a list of products it would be better to call it products (plural) instead of product (single).
If you have React render a list of elements and you can delete or sort elements then you should not use index as key, doesn't product have a unique id you can use and if not then why not?
Below is an example of how you could send event handlers to Product:
class App extends React.Component {
state = {
products: [
{ id: 1, eName: 'Anu', eNo: '1200', eSalary: '1000' },
{
id: 2,
eName: 'Jack',
eNo: '1201',
eSalary: '1200',
},
{ id: 3, eName: 'Ben', eNo: '1202', eSalary: '1300' },
],
showFlag: true,
};
//arrow function will auto bind
inputChangeHandler = (eName, id) => {
this.setState({
products: this.state.products.map(p =>
p.id === id ? { ...p, eName } : p
),
});
};
//arrow function will auto bind
deleteHandler = id => {
this.setState({
products: this.state.products.filter(
p => p.id !== id
),
});
};
showHideHandler = () => {
this.setState({
showFlag: !this.state.showFlag,
});
};
render() {
let dp = null;
if (this.state.showFlag === true) {
dp = (
<div className="App">
{this.state.products.map((product, index) => {
return (
<Product
key={product.id}
{...product}
remove={this.deleteHandler}
inputChange={this.inputChangeHandler}
/>
);
})}
</div>
);
}
return (
<div className="App">
{dp}
<hr />
<button onClick={() => this.showHideHandler()}>
show hide
</button>
<h2>{this.state.eName} </h2>
</div>
);
}
}
//make this a pure component with React.memo
const Product = React.memo(function Product({
remove,
id,
eName,
inputChange,
}) {
console.log('rendering:', id);
return (
<div>
<button onClick={() => remove(id)}>delete</button>
<input
type="text"
value={eName}
onChange={e => inputChange(e.target.value, id)}
></input>
</div>
);
});
//render app
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>
You should also pass the index to your Product component,
{this.state.product.map((product,index) => {
return (
<Product
eName={product.eName}
eNo={product.eNo}
eSalary={product.eSalary}
key = {index}
ind = {index} //pass index here
click ={this.deleteHandler.bind(this.index)}
inputChange={this.inputChangeHandler.bind(this)}
/>
)
})}
your Product component should be this (as per your comment),
import React from 'react'
import './employee.css'
function Employee(props) {
return (
<div className ="prod">
<h1> Name:{props.eName}</h1>
<h2> Emp-No:{props.eNo}</h2>
<h3> Salary:{props.eSalary}</h3>
<button onClick ={props.click}> delete</button>
<input onChange={(e) => props.inputChange(e,props.ind)} value={props.eName} type="text"/> //pass `event` & `index` to `inputChange` function here
</div>
)
}
export default Employee;

Resources