how to toggle a boolean inside an array - reactjs

import React, { useState } from "react";
import { v4 as uuidv4 } from 'uuid';
const ReturantInfo = ()=>{
const data = [
{
name: "Haaland Restaurant's",
stars: 5,
price:"100$",
ranking:"top 1 in england",
ids:[
{id:uuidv4(),key:uuidv4(),visibility:false},
{id:uuidv4(),key:uuidv4(),visibility:false},
{id:uuidv4(),key:uuidv4(),visibility:false},
]
}
]
const [restaurantData,setRestaurantData] = useState(data)
const CardElement = restaurantData.map((data)=>{
return(
<div style={{color:"white"}}>
<h1>{data.name}</h1>
<div>
<div>
<h1>Stars</h1>
<p
id={data.ids[0].id}
onClick={()=>toggleVisibility(data.ids[0].id)}
>show</p>
</div>
{ data.ids[0].visibility ? <p>{data.stars}</p> : ""}
</div>
<div>
<div>
<h1>Price</h1>
<p
id={data.ids[1].id}
onClick={()=>toggleVisibility(data.ids[1].id)}
>show</p>
</div>
{ data.ids[1].visibility ? <p>{data.price}</p> : ""}
</div>
<div>
<div>
<h1>Ranking</h1>
<p
id={data.ids[2].id}
onClick={()=>toggleVisibility(data.ids[2].id)}
>show</p>
</div>
{ data.ids[2].visibility ? <p>{data.ranking}</p> : ""}
</div>
</div>
)
})
function toggleVisibility(id) {
setRestaurantData((prevData) =>
prevData.map((data) => {
data.ids.map(h=>{
return h.id === id ? {...data,ids:[{...h,visibility:!h.visibility}]} : data
})
})
);
}
return(
<div>
{CardElement}
</div>
)
}
export default ReturantInfo
that's a small example from my project I want to toggle visibility property by depending on the id of the clicked element and then if it equals to the id in the array then change the visibility to the opposite.

It looks like your handler isn't mapping the data to the same shape as what was there previously. Your data structure is quite complex which doesn't make the job easy, but you could use an approach more like this, where you pass in the element of data that needs to be modified, as well as the index of hte ids array that you need to modify, then return a callback that you can use as the onClick handler:
function toggleVisibility(item, index) {
return () => {
setRestaurantData((prevData) => prevData.map((prevItem) => {
if (prevItem !== item) return prevItem;
return {
...prevItem,
ids: prevItem.ids.map((id, i) =>
i !== index ? id : { ...id, visibility: !id.visibility }
),
};
}));
};
}
And you'd use it like this:
<p onClick={toggleVisibility(data, 0)}>show</p>
While you're there, you could refactor out each restaurant "property" into a reusable component, since they're all effectively doing the same thing. Here's a StackBlitz showing it all working.

function toggleVisibility(id) {
setRestaurantData(
restaurantData.map((d) => {
const ids = d.ids.map((h) => {
if (h.id === id) {
h.visibility = !h.visibility;
}
return h;
});
return { ...d, ids };
})
);
}

Related

Component lose its state when re-renders

I have a sample where I have a list of answers and a paragraph that has gaps.
I can drag the answers from the list to the paragraph gaps. After the answer fills the gap, the answer will be removed from the list. 3 answers and 3 gaps, the answer list should be empty when i drag all of them to the gaps.
But whenever I filter the listData, the component re-renders and the listData gets reset. The list always remained 2 items no matter how hard I tried. What was wrong here?
My code as below, I also attached the code sandbox link, please have a look
App.js
import GapDropper from "./gapDropper";
import "./styles.css";
const config = {
id: "4",
sort: 3,
type: "gapDropper",
options: [
{
id: "from:drop_gap_1",
value: "hello"
},
{
id: "from:drop_gap_2",
value: "person"
},
{
id: "from:drop_gap_3",
value: "universe"
}
],
content: `<p>This is a paragraph. It is editable. Try to change this text. <input id="drop_gap_1" type="text"/> . The girl is beautiful <input id="drop_gap_2" type="text"/> I can resist her charm. Girl, tell me how <input id="drop_gap_3" type="text"/></p>`
};
export default function App() {
return (
<div className="App">
<GapDropper data={config} />
</div>
);
}
gapDropper.js
import { useEffect, useState } from "react";
import * as _ from "lodash";
import styles from "./gapDropper.module.css";
const DATA_KEY = "answerItem";
function HtmlViewer({ rawHtml }) {
return (
<div>
<div dangerouslySetInnerHTML={{ __html: rawHtml }} />
</div>
);
}
function AnwserList({ data }) {
function onDragStart(event, data) {
event.dataTransfer.setData(DATA_KEY, JSON.stringify(data));
}
return (
<div className={styles.dragOptionsWrapper}>
{data.map((item) => {
return (
<div
key={item.id}
className={styles.dragOption}
draggable
onDragStart={(event) => onDragStart(event, item)}
>
{item.value}
</div>
);
})}
</div>
);
}
function Content({ data, onAfterGapFilled }) {
const onDragOver = (event) => {
event.preventDefault();
};
const onDrop = (event) => {
const draggedData = event.dataTransfer.getData(DATA_KEY);
const gapElement = document.getElementById(event.target.id);
const objData = JSON.parse(draggedData);
gapElement.value = objData.value;
onAfterGapFilled(objData.id);
};
function attachOnChangeEventToGapElements() {
document.querySelectorAll("[id*='drop_gap']").forEach((element) => {
element.ondragover = onDragOver;
element.ondrop = onDrop;
});
}
useEffect(() => {
attachOnChangeEventToGapElements();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div>
<HtmlViewer rawHtml={data} />
</div>
);
}
const GapDropper = ({ data }) => {
const [gaps, setGaps] = useState(() => {
return data.options;
});
function onAfterGapFilled(id) {
let clonedGaps = _.cloneDeep(gaps);
clonedGaps = clonedGaps.filter((g) => g.id !== id);
setGaps(clonedGaps);
}
return (
<div>
<div>
<AnwserList data={gaps} />
<Content
data={data.content}
onAfterGapFilled={(e) => onAfterGapFilled(e)}
/>
</div>
</div>
);
};
export default GapDropper;
Code sandbox
the problem is that you are not keeping on track which ids you already selected, so thats why the first time it goes right, and then the second one, the values just replace the last id.
Without changing a lot of your code, we can accomplish by tracking the ids inside a ref.
const GapDropper = ({ data }) => {
const [gaps, setGaps] = useState(() => {
return data.options;
});
const ids = useRef([])
function onAfterGapFilled(id) {
let clonedGaps = _.cloneDeep(gaps);
ids.current = [...ids.current, id]
clonedGaps = clonedGaps.filter((g) => !ids.current.includes(g.id));
setGaps(clonedGaps);
}
return (
<div>
<div>
<AnwserList data={gaps} />
<Content
data={data.content}
onAfterGapFilled={(e) => onAfterGapFilled(e)}
/>
</div>
</div>
);
};
Maybe there is a better solution but this one does the job

Could'nt return maped item inside jsx component

Hey Guys I am trying to display a list of items according to categories,
this is my json structure. I want to display objects according to itemCategory. for e.g if there are two or more pizza they should come under one category heading.
{
"itemid": 3,
"itemName": "Veg OverLoaded",
"itemPrice": 300.0,
"itemDescription": "chessy, tasty, covered with fresh veggies",
"itemCategory": "pizza"
},
for this i created a map of objects and passed the data according to category as key.
import React, { forwardRef,useState } from 'react';
import MenuItem from './MenuItem';
import './styles.css';
import Category from '../../Home/Category'
const NewMap = new Map()
const Menu = forwardRef(({ list }, ref) => (
<>
<main ref={ref} >
{Object.values(list).map((k) => {
if (NewMap.has(k.itemCategory)){
const itemList = NewMap.get(k.itemCategory);
const newItemList = [...itemList, k];
NewMap.set(k.itemCategory, newItemList);
}else{
NewMap.set(k.itemCategory , [k]);
}
})}
<MenuItem itemMap = {NewMap}/>
</main>
</>
));
i am passing the map to MenuItem as props and trying to display objects here
import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import {
cartAddItem,
cartRemoveItem,
} from '../../../../redux/cart/cart.action';
import {
selectCartItems,
selectCartItemsCount,
} from '../../../../redux/cart/cart.selector';
import ButtonAddRemoveItem from '../../ButtonAddRemoveItem';
import './styles.css';
import Accordion from 'react-bootstrap/Accordion'
import useFetchData from './newData'
const MenuItem = ({
itemMap,
cartCount,
cartList,
cartAddItem,
cartRemoveItem,
}) => {
const {
data,
loading,
} = useFetchData();
const handleQuantity = () => {
let quantity = 0;
if (cartCount !== 0) {
const foundItemInCart = cartList.find((item) => item.itemid === 1);
if (foundItemInCart) {
quantity = foundItemInCart.quantity;
}
}
return quantity;
};
return (
<>
{itemMap.forEach((key, value) => {
{console.log(value)}
<div className='container-menu' >
{console.log(value)}
<ul>
{Object.values(key).map((blah) => {
<li>
<h1>{blah.itemName}</h1>
<div className='item-contents'>
{blah.itemName}
<p className='item-contents' style={{ float: "right", fontSize: "12" }}> ₹ {blah.itemPrice}</p>
<div>
<p className='description'>{blah.itemDescription}</p>
<ButtonAddRemoveItem
quantity={handleQuantity()}
handleRemoveItem={() => cartRemoveItem(blah)}
handleAddItem={() => cartAddItem(blah)}
/>
</div>
</div>
</li>
})}
</ul>
</div>
})}
</>
);
};
const mapStateToProps = createStructuredSelector({
cartCount: selectCartItemsCount,
cartList: selectCartItems,
});
const mapDispatchToProps = (dispatch) => ({
cartAddItem: (item) => dispatch(cartAddItem(item)),
cartRemoveItem: (item) => dispatch(cartRemoveItem(item)),
});
export default connect(mapStateToProps, mapDispatchToProps)(MenuItem);
export default Menu;
i am able to console.log itemName but i am unable to display it inside jsx component. Any reason why
? what am i missing here
The foreach loop should return JSX. In your case there is no return. I suggest removing the curly brackets.
WRONG:
itemMap.forEach((key, value) => {
<>
</>
})
CORRECT:
itemMap.forEach((key, value) => (
<>
</>
))
That's valid for all the loops inside your JSX.
To return an array of elements, you should use map instead of forEach like below code, because forEach loop returns undefined, while map always returns an array.
{itemMap.map((key, value) => {
return (<div className='container-menu'>
...
</div>)
}
}
or
{itemMap.map((key, value) => (<div className='container-menu'>
...
</div>)
}

if else statement not working in react component

I am trying to implement a condition in my react component . When the user triggers the onClick the state updates allStakes creating one array of 4 values. The problem is that I do not want the user to input more than 4 values so tried to give the limit by doing an if else statement. I tried to add a console.log in both statements.The weird fact is that setState get updated but the csonole.log is never displayed.The component keeps rendering all the values that I insert even if the array is longer than 4. Thanks in advance
import React, { Component } from 'react';
import Stake from './stake';
class FetchRandomBet extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
bet: null,
value: this.props.value,
allStakes: []
};
}
async componentDidMount() {
const url = "http://localhost:4000/";
const response = await fetch(url);
const data = await response.json();
this.setState({
loading: false,
bet: data.bets,
});
}
render() {
const { valueProp: value } = this.props;
const { bet, loading } = this.state;
const { allStakes } = this.state;
if (loading) {
return <div>loading..</div>;
}
if (!bet) {
return <div>did not get data</div>;
}
return (
< div >
{
loading || !bet ? (
<div>loading..</div>
) : value === 0 ? (
<div className="bet-list">
<ol>
<p>NAME</p>
{
bet.map(post => (
<li key={post.id}>
{post.name}
</li>
))
}
</ol>
<ul>
<p>ODDS</p>
{
bet.map(post => (
<li key={post.id}>
{post.odds[4].oddsDecimal}
<div className="stake-margin">
<Stake
onClick={(newStake) => {
if (allStakes.length <= 3) {
this.setState({ allStakes: [allStakes, ...newStake] })
console.log('stop')
} else if (allStakes.length == 4) {
console.log('more than 3')
}
}}
/>
</div>
</li>
))
}
</ul>
</div>
May be it happens because of incorrect array destructuring. Try to change this code:
this.setState({ allStakes: [allStakes, ...newStake] })
by the next one:
this.setState({ allStakes: [newStake, ...allStakes] })
Your state belongs to your FetchRandomBet component and you are trying to update that from your imported component. There are 2 solutions to that.
1> Wrap your Stake component to a separate component with onClick handler something like this
<div onClick={(newStake) => {
if (allStakes.length <= 3) {
this.setState({
allStakes: [allStakes, ...newStake
]
})
console.log('stop')
} else if (allStakes.length == 4) {
console.log('more than 3')
}
}}><Stake /></div>
Or
2> Pass the state as a prop to the Stake component which will be responsible to update the state for FetchRandomBet. something like this
<Stake parentState={this}/>
And inside the Stake component change the parentState on click of wherever you want.
I solved the problem. I transfered the onClick method in stake component and I handled the upload of the common array with an array useState. I add the value to newStake and when I click ok I retrieve newStake and spread it into a new array and then I check that array. If there is a value should not keep adding otherwise it can add values. It works fine. Thanks anyway
import React, { useState } from 'react';
import CurrencyInput from 'react-currency-input-field';
function Stake(props) {
const [newStake, setStake] = useState(null);
const [allStakes, setStakes] = useState(null);
const changeStake = (e) => {
setStake([e.target.value])
}
const mySubmit = () => {
if (!allStakes) {
setStakes([...newStake, allStakes])
props.onClick(newStake);
} else if (allStakes) {
console.log('stop')
}
}
return (
<>
<CurrencyInput
onChange={changeStake}
style={{
marginLeft: "40px",
width: "50px"
}}
placeholder="Stake"
decimalScale={2}
prefix="£"
/>
<button onClick={mySubmit}>yes</button>
<button>no</button>
{newStake}
</>
);
}
export default Stake;

useCallback in React seems to be not letting me update state in parent

I've created a simple example of how useCallback is not allowing me to preserve state changes. When I remove the useCallback, the counters that I store in state update as expected, but adding useCallback (which I was hoping would keep rerenders of all speaker items to not re-render) keeps resetting my state back to the original (0,0,0).
The problem code is here in codesandbox:
https://codesandbox.io/s/flamboyant-shaw-2wtqj?file=/pages/index.js
and here is the actual simple one file example
import React, { useState, memo, useCallback } from 'react';
const Speaker = memo(({ speaker, speakerClick }) => {
console.log(speaker.id)
return (
<div>
<span
onClick={() => {
speakerClick(speaker.id);
}}
src={`speakerimages/Speaker-${speaker.id}.jpg`}
width={100}
>{speaker.id} {speaker.name}</span>
<span className="fa fa-star "> {speaker.clickCount}</span>
</div>
);
});
function SpeakerList({ speakers, setSpeakers }) {
return (
<div>
{speakers.map((speaker) => {
return (
<Speaker
speaker={speaker}
speakerClick={useCallback((id) => {
const speakersNew = speakers.map((speaker) => {
return speaker.id === id
? { ...speaker, clickCount: speaker.clickCount + 1 }
: speaker;
});
setSpeakers(speakersNew);
},[])}
key={speaker.id}
/>
);
})}
</div>
);
}
//
const App = () => {
const speakersArray = [
{ id: 1124, name: 'aaa', clickCount: 0 },
{ id: 1530, name: 'bbb', clickCount: 0 },
{ id: 10803, name: 'ccc', clickCount: 0 },
];
const [speakers, setSpeakers] = useState(speakersArray);
return (
<div>
<h1>Speaker List</h1>
<SpeakerList speakers={speakers} setSpeakers={setSpeakers}></SpeakerList>
</div>
);
};
export default App;
first, you can only use a hook at component body, you can't wrap it at speakerClick props function declaration. second, useCallback will keep the original speakers object reference, which will be a stale value. To solve this, you can use setSpeakers passing a callback instead, where your function will be called with the current speakers state:
function SpeakerList({ speakers, setSpeakers }) {
const speakerClick = useCallback(
(id) => {
// passing a callback avoid using a stale object reference
setSpeakers((speakers) => {
return speakers.map((speaker) => {
return speaker.id === id
? { ...speaker, clickCount: speaker.clickCount + 1 }
: speaker;
});
});
},
[setSpeakers] // you can add setSpeakers as dependency since it'll remain the same
);
return (
<div>
{speakers.map((speaker) => {
return (
<Speaker
speaker={speaker}
speakerClick={speakerClick}
key={speaker.id}
/>
);
})}
</div>
);
}

set state of specific attribute inside of an object

I'm struggling to figure out the syntax for setting the state of an object inside of an array. I'm trying to access the fruits amount attribute. I'm familiar with concat for adding a new object and such but instead of adding a new updated object, how do I replace the value of an attribute inside of an object keeping everything the same except the attribute that changed and not adding a completely new object.
import React, { Component } from 'react';
import { render } from 'react-dom';
import './style.css';
import Fruits from'./Fruits';
class App extends Component {
constructor(props) {
super(props);
this.state = {
fruits: [
{
type:'apple',
amount:10,
color:'green',
id: 0
},
{
type:'tomato',
amount:'25',
color:'red',
id: 1
}
]
};
}
renderFruits = () => {
const { fruits } = this.state;
return fruits.map((item, index) =>
<Fruits
key={index}
type={item.type}
amount={item.amount}
color={item.color}
id={item.id}
increment={this.increment}
/>
);
}
increment = (fruitId) => {
const { fruits } = this.state;
const incrementedFruit = fruits.filter((item) => {
return item.id == fruitId;
})
//{fruits: {...fruits, [fruits.amount]: [fruits.amount++]}}
}
render() {
return (
<div>
{this.renderFruits()}
</div>
);
}
}
render(<App />, document.getElementById('root'));
Fruits Component
import React, { Component } from 'react';
class Fruits extends Component {
increment = () => {
const { id } = this.props;
this.props.increment(id);
}
decrement = () => {
console.log("decremented");
}
render() {
const { type, id, amount, color} = this.props;
return(
<div>
<span>
{type}
<ul>
<li>amount: {amount} </li>
<li>color: {color} </li>
</ul>
<button onClick={this.increment}> + </button>
<button onClick={this.decrement}> - </button>
</span>
</div>
);
}
}
export default Fruits;
stackblitz project
First pass index from fruit, lets take as index prop :
<Fruits
key={item.id}
index = {index}
type={item.type}
amount={item.amount}
color={item.color}
id={item.id}
increment={this.increment}
/>
Then pass index, instead of id of fruit on increment :
increment = () => {
this.props.increment(this.props.index);
}
You can make your increment function 2 ways :
1 : with state mutation , Please read : State Mutation
increment = (index) => {
++this.state.fruits[index].amount;
this.forceUpdate();
//OR
this.setState({fruits : this.state.fruits});
}
2 : without state mutation
increment = (index) => {
let fruits = this.state.fruits.slice();
++fruits[index].amount;
this.setState({fruits});
}
P.S: Also found that you are using the array index as key. This is
deprecated. See
https://medium.com/#robinpokorny/index-as-a-key-is-an-anti-pattern-e0349aece318
So also change :
from key={index} to key={item.id}
just change your increment function to this:
increment = (fruitId) => {
const { fruits } = this.state;
fruits.forEach((fruit)=>{
if(fruit.id == fruitId){
fruit.amount = parseInt(fruit.amount)+ 1;
}
})
this.setState({fruits: fruits})
}
Edited

Resources