I was trying to add a new element of array to the list with update of one property (id). I want to make it 1 more than length of array.
But I get some weird outputs, with add every new object. All elements are getting array.length +1 value.
I made several variations of this code with let, const or even operating directly on this.state.produktsToBuy, and every time I got the same output
handleAddToShop = (produktToBuy) => {
const id = this.state.produktsToBuy.length+1;
produktToBuy.id = id + 1;
const produktsToBuy = this.state.produktsToBuy;
produktsToBuy.push(produktToBuy);
this.setState({produktsToBuy});
};
I Should get the output as 1,2,3,4,5,6,7
But on the end I get 7,7,7,7,7,7
Make sure you're not mutating the state directly. In JS, objects are a reference type. When you assign this.state.produktsToBuy to const produktsToBuy and push something to produktsToBuy, you're actually pushing to the original this.state.produktsToBuy and you modify the state.
You can use the spread operator (...) to create a shallow copy of the state (produktsToBuy):
class App extends React.Component {
state = {
items: [
{ name: "test item 1", price: 4.99 },
{ name: "test item 2", price: 7.99 },
{ name: "test item 3", price: 19.99 }
],
produktsToBuy: []
};
handleAddToShop = (produktToBuy) => {
this.setState((prev) => ({
produktsToBuy: [
...prev.produktsToBuy,
{
...produktToBuy,
id: prev.produktsToBuy.length + 1
}
]
}));
};
render() {
return (
<div className="App">
<div style={{ display: "flex" }}>
{this.state.items.map((item) => (
<div
key={item.name}
style={{
border: "1px solid #ccc",
margin: "1rem",
padding: "1rem",
textAlign: "center"
}}
>
<h3>{item.name}</h3>
<p>${item.price}</p>
<button onClick={() => this.handleAddToShop(item)}>Add</button>
</div>
))}
</div>
<pre>{JSON.stringify(this.state.produktsToBuy, null, 2)}</pre>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
You should be maintaining all of the previous state if there's anything other than just the produktsToBuy. Also, you always need the functional form of setState if anything you're setting is dependent on the previous state(as is OFTEN the case). And, like Zsolt said, you never mutate the state directly in React. Here's my answer (very similar to #Zsolt Meszaros'). Note: .concat creates a new array, so we don't have to worry about mutating the original.
handleAddToShop = (produktToBuy) => {
this.setState((prevState) => {
const { produktsToBuy } = prevState;
return {
...prevState,
produktsToBuy: produktsToBuy.concat([
{
...produktToBuy,
id: produktsToBuy.length + 1,
},
]),
};
});
};
Related
I have 6 cubes displayed on the page and there is a counter.
When you click on a cube, its background changes to red and the counter increases by one, but the counter does not want to increase. I always have it 1. But if you remove the setcub([...cubikinapole]) line, then the counter starts working as it should, but now the background does not change. What to do?
import "./styles.css";
import {useState} from 'react'
export default function App() {
let [cubikinapole, setcub] = useState([
{id:1,title:1, img:"https://www.zonkpro.ru/zonk/assets/dice/mini/1.png", style: 'none'},
{id:2,title:2, img:"https://www.zonkpro.ru/zonk/assets/dice/mini/2.png" , style: 'none'},
{id:3,title:3, img:"https://www.zonkpro.ru/zonk/assets/dice/mini/3.png" , style: 'none'},
{id:4,title:4, img:"https://www.zonkpro.ru/zonk/assets/dice/mini/4.png" , style: 'none'},
{id:5,title:5, img:"https://www.zonkpro.ru/zonk/assets/dice/mini/5.png" , style: 'none'},
{id:6,title:6, img:"https://www.zonkpro.ru/zonk/assets/dice/mini/6.png" , style: 'none'},
])
let [count,se] = useState(0)
function a(el) {
se(count++)
el.style = 'active'
setcub([...cubikinapole])
console.log(count)
}
return (
<div className="App">
{cubikinapole.length !== 0 ?
cubikinapole.map((el)=>
<div id='cub' key={el.id} className={el.style} onClick={()=>a(el)}>
<img src={el.img} />
</div>
)
: console.log() }
</div>
);
}
CSS
.active{
background-color: red;
}
you're never supposed to change the value of a useState by the value itself, only by the setter - what you're doing where you're writing se(count++) is giving the old value of the count to se and then directly changing count by count++,
so it looks like a race condition, you can fix it by just changing it to se(count+1) and your code will run as it should
There are a few issues. Note: I've changed the names of some variable for clarity.
You can't set your counter like that. Ideally you want to take the previous state, and then update and return that. So setCount(count++) becomes setCount(prev => prev += 1).
ids should be unique. If you want to uniquely identify an element you can use a data attribute, and then pick up that value in the handler.
With the id in hand in your handler you should find the object in state that where the object id matches the id of the element you clicked on, and then update that. To do that make a copy of the state first, update the object, and then set the state using the changed copy.
It's not really clear whether it was the background of the cube you wanted to change, or the image itself so I've just left your code as-is. For the actual image to change would be slightly more difficult.
const { useState } = React;
// For clarity I'm passing in the image
// config to the component, and then setting the
// cube state with it
function Example({ config }) {
// Initialise the states
const [ cubes, setCubes ] = useState(config);
const [ count, setCount ] = useState(0);
// When an element is clicked take the
// id from its data set. Make a copy of the state
// and then find the index of the object that has a
// matching id. Then update that object with the new
// style, and set both the cube and count states
function handleClick(e) {
const { id } = e.currentTarget.dataset;
const copy = [...cubes];
const index = copy.findIndex(cube => cube.id === Number(id));
if (index >= 0) copy[index].style = 'active';
setCubes(copy);
setCount(prev => prev += 1);
}
// I've added a count element here.
// `map` over state adding the data attribute
// to the div element.
return (
<div className="App">
<div className="counter">Count: {count}</div>
{config.map(obj => {
const { id, style, img } = obj;
return (
<div
key={id}
data-id={id}
className={style}
onClick={handleClick}
><img src={img} />
</div>
);
})}
</div>
);
}
const config=[{id:1,title:1,img:"https://www.zonkpro.ru/zonk/assets/dice/mini/1.png",style:"none"},{id:2,title:2,img:"https://www.zonkpro.ru/zonk/assets/dice/mini/2.png",style:"none"},{id:3,title:3,img:"https://www.zonkpro.ru/zonk/assets/dice/mini/3.png",style:"none"},{id:4,title:4,img:"https://www.zonkpro.ru/zonk/assets/dice/mini/4.png",style:"none"},{id:5,title:5,img:"https://www.zonkpro.ru/zonk/assets/dice/mini/5.png",style:"none"},{id:6,title:6,img:"https://www.zonkpro.ru/zonk/assets/dice/mini/6.png",style:"none"}];
ReactDOM.render(
<Example config={config} />,
document.getElementById('react')
);
.active { background-color: red; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>
You can try this code. count++ increases its value after the setCount function is executed, so you should use count+1 to pass the increased value to the function. And you'd better use useEffect to monitor the count value.
import "./styles.css";
import { useState, useEffect } from "react";
export default function App() {
let [cubikinapole, setCub] = useState([
{
id: 1,
title: 1,
img: "https://www.zonkpro.ru/zonk/assets/dice/mini/1.png",
style: "none"
},
{
id: 2,
title: 2,
img: "https://www.zonkpro.ru/zonk/assets/dice/mini/2.png",
style: "none"
},
{
id: 3,
title: 3,
img: "https://www.zonkpro.ru/zonk/assets/dice/mini/3.png",
style: "none"
},
{
id: 4,
title: 4,
img: "https://www.zonkpro.ru/zonk/assets/dice/mini/4.png",
style: "none"
},
{
id: 5,
title: 5,
img: "https://www.zonkpro.ru/zonk/assets/dice/mini/5.png",
style: "none"
},
{
id: 6,
title: 6,
img: "https://www.zonkpro.ru/zonk/assets/dice/mini/6.png",
style: "none"
}
]);
let [count, setCount] = useState(0);
useEffect(() => {
console.log(count);
}, [count]);
function a(el) {
setCount((prev) => prev + 1);
el.style = "active";
setCub([...cubikinapole]);
}
return (
<div className="App">
{cubikinapole.length !== 0
? cubikinapole.map((el) => (
<div key={el.id} className={el.style} onClick={(e) => a(el)}>
<img src={el.img} alt={el.title} />
</div>
))
: console.log()}
</div>
);
}
Live demo: https://codesandbox.io/s/amazing-haze-0zdb26?file=/src/App.js:0-1369
I created a component with several div elements.
By adding a ?goto= parameter to the url I want to scroll the the relevant element. I now solved that with const itemsRef = useRef([]);.
My main concern now is if that's the right and performance efficient approach with itemsRef.current[element.id] = el. element.id will be unique for each element.
I also found packages such as: https://github.com/Macil/react-multi-ref
But I don't see the disadvantage of my approach yet.
Here you can find my current solution in action: https://codesandbox.io/s/scrolltoref-w5i7m?file=/src/Element.js
import React, { useRef, useEffect, useState } from "react";
import clsx from "clsx";
const blueprint = [
{
id: "3mD59WO",
name: "AUDITORIUM",
position: 0,
rooms: [
{
id: "zR8Qgpj",
name: "Audimax",
subtitle: null,
details: null,
position: 0,
elements: [
{
id: "1jLv04W",
position: 0,
type: "daily",
element: "listing_large",
properties: {
meetingId: null,
capacity: 6
}
},
{
id: "1jLv12W",
position: 1,
type: "daily",
element: "listing_large",
properties: {
meetingId: null,
capacity: 6
}
}
]
}
]
},
{
id: "4mDd9WO",
name: "FOYER",
position: 1,
rooms: [
{
id: "4R8Qgpj",
name: "Speakers Table",
subtitle: null,
details: null,
position: 0,
elements: [
{
id: "2jLv04W",
position: 0,
type: "daily",
element: "listing_large",
properties: {
meetingId: null,
capacity: 6
}
},
{
id: "2jLv12W",
position: 1,
type: "daily",
element: "listing_large",
properties: {
meetingId: null,
capacity: 6
}
}
]
}
]
}
];
export default function Query() {
const itemsRef = useRef([]);
const [currentRef, setCurrentRef] = useState();
useEffect(() => {
const scrollToRef = ref => {
window.scrollTo(0, ref.offsetTop);
};
const goto = "1jLv12W"; // This will become an URL parameter ?goto=:someID in the final version
const ref = itemsRef.current[goto];
setCurrentRef(ref); // This is needed to change the className to highlight
scrollToRef(ref); // Here I assign the ref and the component should scroll to that ref
}, []);
return (
<div key="element">
{blueprint.map(floor => (
<div key={floor.id} style={{ marginTop: 50 }}>
Floor: {floor.name} <br />
<br />
{floor.rooms.map(room => (
<div key={room.id}>
Room Name: {room.name}
<br />
{room.elements.map(element => (
<div
ref={el => (itemsRef.current[element.id] = el)}
className={clsx({
highlight:
currentRef && currentRef === itemsRef.current[element.id]
})}
key={element.id}
style={{ backgroundColor: "green", marginTop: 100 }}
>
ElementID: {element.id}
<br />
</div>
))}
</div>
))}
</div>
))}
</div>
);
}
That's the right approach, usually, you will see useRef([]) when handling multiple animations in a page, and that's exactly how it's done itemsRef.current[element.id] = el.
My main concern now is if that's the right and performance efficient approach
That's directly related to "Why Premature Optimization Is the Root of All Evil".
Premature optimization is spending a lot of time on something that you may not actually need.
You trying to optimize before you have any performance issues. Focus on delivering the product and clean code, find a time for optimization when you actually measured it.
We also don’t want to waste an enormous amount of time doing performance optimization on things that don’t matter. Many development teams get caught up in focusing on optimizing for performance and scale before they have validated their new product functionality.
useRef is basically the same as doing useState({ current: <value you pass in> });
Given your use case, what you have done is sufficient however I would change use ref to initialise with an object since that is what you are actually using as oppose to an array:
const itemsRef = useRef({});
Your code still works but may potentially give you some unexpected behaviour since assigning properties to an array can be a bit weird and is definitely not what you intend to do anyway.
For example with an array you are actually doing this:
[]["<some id>"] = el; // very weird!
vs an object:
{}["<some id>"] = el
I am pulling down results from an API, like so:
const [state, setState] = React.useState({
matches: undefined,
chosenBets: [{}]
});
const API = "https://api.myjson.com/bins/i461t"
const fetchData = async (endpoint, callback) => {
const response = await fetch(endpoint);
const json = await response.json();
setState({ matches: json });
};
And rendering JSX based off it using the map() function:
export function MatchCardGroup(props) {
return (
<div>
{props.matches.map((match, i) => {
return (
<MatchCard
key={i}
matchCardIndex={i}
team_home={match.teams[0]}
team_away={match.teams[1]}
league_name={match.sport_nice}
odd_home={match.sites[0].odds.h2h[0]}
odd_draw={match.sites[0].odds.h2h[1]}
odd_away={match.sites[0].odds.h2h[2]}
onClick={props.onClick}
timestamp={match.timestamp}
/>
);
})}
</div>
);
}
I then have a card which has odds on it, each odd with its own click event:
export function MatchCard(props) {
const [state, setState] = React.useState({
selection: {
id: undefined
}
});
const {
timestamp,
team_home,
team_away,
league_name,
odd_away,
odd_draw,
odd_home,
onClick,
matchCardIndex,
selection
} = props;
const odds = [
{
id: 0,
label: 1,
odd: odd_home || 1.6
},
{
id: 1,
label: "X",
odd: odd_draw || 1.9
},
{
id: 2,
label: 2,
odd: odd_away || 2.6
}
];
const handleOnClick = (odd, oddIndex) => {
// need to changhe the selection to prop
if (state.selection.id === oddIndex) {
setState({
selection: {
id: undefined
}
});
onClick({}, matchCardIndex);
} else {
setState({
selection: {
...odd,
team_home,
team_away
}
});
onClick({ ...odd, oddIndex, team_home, team_away, matchCardIndex });
}
};
React.useEffect(() => {}, [state, props]);
return (
<div style={{ width: "100%", height: 140, backgroundColor: colour.white }}>
<div>
<span
style={{
...type.smallBold,
color: colour.betpawaGreen
}}
>
{timestamp}
</span>
<h2 style={{ ...type.medium, ...typography }}>{team_home}</h2>
<h2 style={{ ...type.medium, ...typography }}>{team_away}</h2>
<span
style={{
...type.small,
color: colour.silver,
...typography
}}
>
{league_name}
</span>
</div>
<div style={{ display: "flex" }}>
{odds.map((odd, oddIndex) => {
return (
<OddButton
key={oddIndex}
oddBackgroundColor={getBackgroundColour(
state.selection.id,
oddIndex,
colour.lime,
colour.betpawaGreen
)}
labelBackgroundColor={getBackgroundColour(
state.selection.id,
oddIndex,
colour.lightLime,
colour.darkBetpawaGreen
)}
width={"calc(33.3% - 8px)"}
label={`${odd.label}`}
odd={`${odd.odd}`}
onClick={() => handleOnClick(odd, oddIndex)}
/>
);
})}
</div>
</div>
);
}
In my App Component I am logging the returned object from the click event:
const onClick = obj => {
// check if obj exists in state.chosenBets
// if it exists, remove from array
// if it does not exist, add it to the array
if (state.chosenBets.filter(value => value == obj).length > 0) {
console.log("5 found.");
} else {
console.log(state.chosenBets, "state.chosenBets");
}
};
And what I want to do is this:
When the user clicks an odd of any given match, add that odd to chosenBets
If the user deselects the odd, remove that odd from chosenBets
Only 1 odd from each of the 3 possible odds of any match can be selected at any time
Bonus points: the selected odd is selected based on the global state from App, instead of local state. This is so if I edit the array elsewhere, it should update in the UI.
Any help would be greatly appreciated, I'm lost here!
Link to Codesandbox
I've taken a short look at your project, and here are a few pointers to help you out:
Objects are only equal by reference.
This means that
{ id: 0, matchCardIndex: 8 } === { id: 0, matchCardIndex: 8 }
is false, even if you expect it to be true. To compare them, you need to compare every key in the object:
value.id === obj.id && value.matchCardIndex === obj.matchCardIndex
This also affects the filter call you have in the index.tsx, so you should change the comparison there to something similar to
state.chosenBets.filter(value => value.id === obj.id && value.matchCardIndex === obj.matchCardIndex)
State should only live in one place
As you already mentioned, it would be better to keep the state in your index.tsx if it also you needed there, and don't keep it locally in the components further down the tree. I'd suggest having the components only render the state, and have handlers to change the state.
Example
Here's a fork of your code sandbox I think implements it in a way that you described: https://codesandbox.io/s/gifted-star-wg629-so-pg5gx
I have list which elements must be deletable (for example with delete button). How can I realize that from react?
this is my state:
state = {
infos: [
{
id: 1,
info: 'some info',
deleted: false
},
{
id: 2,
info: 'some info',
deleted: false
},
{
id: 3,
info: 'some info',
deleted: false
}
]
}
this is a function for delete that I tried:
removeInfo() {
this.state.infos.splice(key, 0)
}
this is a jsx code that I get after maping:
{
this.state.infos.map((item, key) => {
return (
<ListItem key={item.key + key}>
<Icon color="gray" f7="home" />
<span className="text-black">{item.info}</span>
<Button><Icon f7="edit" color="#39b549" /></Button>
<Button onClick={this.removeInfo}><Icon color="black" f7="trash" /></Button>
</ListItem>
)
})
}
You need few changes.
First we need to pass the id of item we want to remove to the remove function:
<Button onClick={()=>this.removeInfo(item.id)}><Icon color="black" f7="trash" /></Button>
Then you need to remove the item from array in immutable way using setState.
removeInfo(id) {
this.setState(ps=>({infos:ps.infos.filter(x=>x.id!=id)}))
}
splice mutates the array.
You need to use setState and note that you can´t mutate the state so you need to use the spread operator to create a new array.
function removeInfo(index) {
this.setState((prev) => ({
infos: [...prev.infos.slice(0, index), ...prev.infos.slice(index+1)]
}))
}
I have a complicated and dynamic data structure like so:
const tree = [
{
name: "Root Node",
collapsed: true,
nodes: [
{
name: "Node 1",
collapsed: true,
nodes: [
{
name: "Sub node"
}
]
},
{
name: "Node 2",
collapsed: true,
nodes: [
{
name: "Sub node "
}
]
},
{
name: "Node 3",
collapsed: true,
nodes: [
{
name: "Sub node"
}
]
}
]
}
];`
I am setting this as the initial state of my component.
I am then rendering this state out as a hierarchy tree in the UI.
When I click the top level node I want to update the collapsed property in state to have it open up and show the next set of nodes.
The problem I am having is how do I call setState() and update this complicated data structure without causing mutation and doing some ugly stuff like tree[0].nodes[0].collapsed: false.
So I first tried setting state like this
handleClick(el, event) {
this.setState({
tree: this.findAndUpdateState(event.target.id).bind(this)
});
}
So on the handleClick event of the node I call this which calls findAndUpdateState.
findAndUpdateState(id) {
this.state.tree.map((node) => {
//Map over the nodes somehow and find the node that needs its state updated using the ID?
});
});
}
I want to update the collapsed property of a node that has been attempted to be expanded. But to do so I have to loop through all the state and then create a new copy of the state just to update that one property. There must a nicer and simpler way that I am not thinking of.
React is pretty good at doing these sort of things.
I'm using the the Hooks in React, as I like them.. :)
Here is a working snippet below..
const tree = [{"name":"Root Node","collapsed":true,"nodes":[{"name":"Node 1","collapsed":true,"nodes":[{"name":"Sub node"}]},{"name":"Node 2","collapsed":true,"nodes":[{"name":"Sub node "}]},{"name":"Node 3","collapsed":true,"nodes":[{"name":"Sub node"}]}]}];
const {useState} = React;
function TreeItem(props) {
const {item} = props;
const [collapsed, setCollapsed] = useState(item.collapsed);
return <div className="item">
<span onClick={() => setCollapsed(!collapsed)}>{item.name}</span>
{!collapsed && item.nodes &&
<div style={{paddingLeft: "1rem"}}>
<TreeList list={item.nodes}/>
</div>
}
</div>
}
function TreeList(props) {
const {list} = props;
return <div>{list.map(f => <TreeItem key={f.name} item={f}/>)}</div>;
}
ReactDOM.render(<TreeList list={tree}/>, document.querySelector('#mount'));
.item {
cursor: pointer;
user-select: none;
}
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="mount"></div>
You could create a component Node with its own state collapsed:
class Node extends Component {
state = {
collapsed: true,
}
toggle = () => {
this.setState(prevState => ({ collapsed: !prevState.collapsed }))
}
render() {
return (
<div>
<p onClick={this.toggle}>{this.props.node.name}</p>
{!this.state.collapsed && (
<div>{this.props.children}</div>
)}
</div>
)
}
}
And create a parent component Tree that renders all the nodes recursively:
const TREE = [{ name: "Root Node", nodes: [...] }]
class Tree extends Component {
renderNodesRecursively = parent => {
return (
<Node node={parent} key={parent.name}>
{parent.nodes
? parent.nodes.map(node => this.renderNodesRecursively(node))
: null
}
</Node>
)
}
render() {
return TREE.map(node => this.renderNodesRecursively(node))
}
}
You will notice that the state of the children is lost each time you toggle the parent. It's "normal" since toggling the parent mounts/unmounts the children. If you want to avoid that, you can replace this code
{!this.state.collapsed && (
<div>{this.props.children}</div>
)}
with
<div style={{ display: this.state.collapsed ? 'none' : 'block' }}>
{this.props.children}
</div>