I am quite new to react an I want to learn how to dynamically update states in deep nested objects.
Let's say I have a deep nested object as:
const initialState = [
{ a: [{ a: [{ a: [{ a: 5 }, { b: 4 }] }, { b: 3 }] }, { b: 2 }] },
{ b: 1 },
];
I want to render each "b" value in a different button and onClick to increment it's value.
Something like this:
buttons image
To do that I want to use a recursive function.
With this I can display all items but I don't know how to do the state update of each individual nested state in the button.
My code so far is:
function RecursiveComponent({ nestedObject, setNestedObject }) {
function handleIncrement() { //this event handler it's not correct for updating the nested states,
setNestedObject((prevState) => {
prevState[1].b = prevState[1].b + 1;
return { ...prevState };
});
}
return (
<>
{nestedObject && (
<div>
<button onClick={handleIncrement}>{nestedObject[1].b}</button>
{typeof nestedObject[0].a === "object" && (
<RecursiveComponent
nestedObject={nestedObject[0].a}
setNestedObject={setNestedObject}
/>
)}
</div>
)}
</>
);
}
function App() {
const [nestedObject, setNestedObject] = useState(initialState);
function handleIncrement() {
setNestedObject((prevState) => {
prevState[1].b = prevState[1].b + 1;
return { ...prevState };
});
}
return (
<>
{nestedObject && (
<button onClick={handleIncrement}>{nestedObject[1].b}</button>
)}
{nestedObject && typeof nestedObject[0].a === "object" && (
<RecursiveComponent
nestedObject={nestedObject[0].a}
setNestedObject={setNestedObject}
/>
)}
</>
);
}
So I have two questions:
1. How to update each nested state dynamicaly?
2. Why when I click on the first event it increases by 1 but after that when clicking again it increases by 2??
Related
I have a problem with my ReactJS App. I want to get values of checked radio buttons and after selecting I want to display the values of selected radio buttons.
The form is generated from a json file
[
{
variantId: 1,
variantName: 'Size',
variantOptions: [
{
variantOptionId: 1,
variantOptionName: 'S',
variantOptionPriceChange: 4.5
},
{
variantOptionId: 2,
variantOptionName: 'M',
variantOptionPriceChange: 4.5
},
]
},
{
variantId: 2,
variantName: 'Color',
variantOptions: [
{
variantOptionId: 3,
variantOptionName: 'Red',
variantOptionPriceChange: 4.5
},
{
variantOptionId: 4,
variantOptionName: 'Blue',
variantOptionPriceChange: 4.5
},
]
}
]
Demo of the problem is visible here: https://codesandbox.io/s/epic-http-bgmx3?file=/src/App.js
I want to display all selected items, not only the last one.
The problem is in this part of code, but I dont know how to rewrite it to achieve the desired behavior.
const addOption = (o) => {
setOptions({
optionId: o.variantOptionId,
optionName: o.variantOptionName,
optionPriceChange: o.variantOptionPriceChange
});
};
Thank you for your help, hope I described it clearly.
Simplest solution would be creating an object in useState with props keys for each variant and then store the selected option of that variant in related object prop
It should work like this:
export default function App() {
const [options, setOptions] = useState({});
const addOption = (name, o) => {
setOptions({ ...options, [name]: o });
};
return (
<div className="App">
{variants.map((variant, index) => {
return (
<Variant
options={options}
variant={variant}
addOption={addOption}
key={index}
/>
);
})}
<h3>
Selected variants are:
<ul>
{Object.keys(options).map((name, i) => {
return (
<li key={i}>
{name}: {options[name].variantOptionName}
</li>
);
})}
</ul>
</h3>
</div>
);
}
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
The function is getting the value of a button click as props. Data is mapped through to compare that button value to a key in the Data JSON called 'classes'. I am getting all the data correctly. All my console.logs are returning correct values. But for some reason, I cannot render anything.
I've tried to add two return statements. It is not even rendering the p tag with the word 'TEST'. Am I missing something? I have included a Code Sandbox: https://codesandbox.io/s/react-example-8xxih
When I click on the Math button, for example, I want to show the two teachers who teach Math as two bubbles below the buttons.
All the data is loading. Just having an issue with rendering it.
function ShowBubbles(props){
console.log('VALUE', props.target.value)
return (
<div id='bubbles-container'>
<p>TEST</p>
{Data.map((item,index) =>{
if(props.target.value == (Data[index].classes)){
return (
<Bubble key={index} nodeName={Data[index].name}>{Data[index].name}
</Bubble>
)
}
})}
</div>
)
}
Sandbox Link: https://codesandbox.io/embed/react-example-m1880
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
const circleStyle = {
width: 100,
height: 100,
borderRadius: 50,
fontSize: 30,
color: "blue"
};
const Data = [
{
classes: ["Math"],
name: "Mr.Rockow",
id: "135"
},
{
classes: ["English"],
name: "Mrs.Nicastro",
id: "358"
},
{
classes: ["Chemistry"],
name: "Mr.Bloomberg",
id: "405"
},
{
classes: ["Math"],
name: "Mr.Jennings",
id: "293"
}
];
const Bubble = item => {
let {name} = item.children.singleItem;
return (
<div style={circleStyle} onClick={()=>{console.log(name)}}>
<p>{item.children.singleItem.name}</p>
</div>
);
};
function ShowBubbles(props) {
var final = [];
Data.map((item, index) => {
if (props.target.value == Data[index].classes) {
final.push(Data[index])
}
})
return final;
}
function DisplayBubbles(singleItem) {
return <Bubble>{singleItem}</Bubble>
}
class Sidebar extends Component {
constructor(props) {
super(props);
this.state = {
json: [],
classesArray: [],
displayBubble: true
};
this.showNode = this.showNode.bind(this);
}
componentDidMount() {
const newArray = [];
Data.map((item, index) => {
let classPlaceholder = Data[index].classes.toString();
if (newArray.indexOf(classPlaceholder) == -1) {
newArray.push(classPlaceholder);
}
// console.log('newArray', newArray)
});
this.setState({
json: Data,
classesArray: newArray
});
}
showNode(props) {
this.setState({
displayBubble: true
});
if (this.state.displayBubble === true) {
var output = ShowBubbles(props);
this.setState({output})
}
}
render() {
return (
<div>
{/* {this.state.displayBubble ? <ShowBubbles/> : ''} */}
<div id="sidebar-container">
<h1 className="sidebar-title">Classes At School</h1>
<h3>Classes To Search</h3>
{this.state.classesArray.map((item, index) => {
return (
<button
onClick={this.showNode}
className="btn-sidebar"
key={index}
value={this.state.classesArray[index]}
>
{this.state.classesArray[index]}
</button>
);
})}
</div>
{this.state.output && this.state.output.map(item=><DisplayBubbles singleItem={item}/>)}
</div>
);
}
}
ReactDOM.render(<Sidebar />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.0.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.0.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
The issue here is ShowBubbles is not being rendered into the DOM, instead (according the sandbox), ShowBubbles (a React component) is being directly called in onClick button handlers. While you can technically do this, calling a component from a function will result in JSX, essentially, and you would need to manually insert this into the DOM.
Taking this approach is not very React-y, and there is usually a simpler way to approach this. One such approach would be to call the ShowBubbles directly from another React component, e.g. after your buttons using something like:
<ShowBubbles property1={prop1Value} <etc...> />
There are some other issues with the code (at least from the sandbox) that you will need to work out, but this will at least help get you moving in the right direction.
In React, upon deleting a component, I want to make a dynamic sentence shows the correct sentence like this in app.js:
let awesomePhrase = '';
if (!this.state.showPersons) {
awesomePhrase = 'Nobody is here, it seems :/';
}
if (this.state.showPersons && this.state.persons.length === 2) {
awesomePhrase = "All aboard :D";
}
if (!this.state.persons.filter(p => p.id === 1)) {
awesomePhrase = "Where's Matin?!";
}
if (!this.state.persons.filter(p => p.id === 2)) {
awesomePhrase = "Where's Mobin?!";
}
It doesn't show any of the sentence when I delete id 1 or id 2.That is, neither "where's Matin?!" nor "Where's Mobin?!".
But the two first sentences work fine.
(EDIT: every piece of code below is within app.js, the main file)
For deleting:
deleteHandler = index => {
const persons = [...this.state.persons].filter(
person => person.id !== index
);
this.setState({ persons });
};
The State:
state = {
persons: [
{ id: 1, name: 'Matin', age: 27 },
{ id: 2, name: 'Mobin', age: 26 }
],
showPersons: false,
...
};
The component within the render of the class:
{this.state.persons.map(person => {
return (
<Person
key={person.id}
name={person.name}
age={person.age}
click={() => this.deleteHandler(person.id)}
/>
);
})}
the part of render where dynamic text is used:
return (
<div>
...
<h2>{awesomePhrase}</h2>
...
</div>
)
The problem with your code is the filter function. Filter will return an empty array if no elements passed the test, and in Javascript, an empty array is not a falsy value.
The condition !this.state.persons.filter(p => p.id === 2) will always be false.
The proper function to use in this situation is Array.some, which return a boolean value depends on the result of the test function.
Be aware of the return type and the falsiness / truthiness in Javascript.
I think I found a workaround
instead of filter I used find, and tried to check them within deleteHandler method. Also added an independent awesomePhrase to State.
So:
deleteHandler = index => {
const persons = [...this.state.persons].filter(
person => person.id !== index
);
if (persons.length === 0) {
this.setState({ awesomePhrase: 'where did they all gone?' });
}
if (persons.find(p => p.name === 'Matin')) {
this.setState({ awesomePhrase: 'Where is Mobin?' });
}
if (persons.find(p => p.name === 'Mobin')) {
this.setState({ awesomePhrase: 'Where is Matin?' });
}
this.setState({ persons });
};
state = {
persons: [
{ id: 1, name: 'Matin', age: 27 },
{ id: 2, name: 'Mobin', age: 26 }
],
...,
showPersons: false,
mobin: true,
awesomePhrase: ''
};
return (
<div className="App">
...
<h2>{this.state.awesomePhrase || awesomePhrase}</h2>
...
</div>
);
I'd welcome any suggestion to help improve my code further or correct it properly. I just made it work now.
I'm trying to make a clone button in React, I have a state with an array that has 2 items in it. The button will send the index of the element selected, in this case let's say index 0. :) I can't get the following code to work
elements = [
{ item: 'something1', another: 'something2' },
{ item: 'something1', another: 'something2' }
];
setState( {
elements: [
...elements.slice( 0, index ),
{
...elements[ index ],
item: 'something'
},
...elements.slice( index + 1 )
]
} )
I know I'm doing something wrong, but...
Use index + 1 in the 1st slice call because you want to get all items up to and including the item you clone (slice stops before the end index), insert the clone, and add all other items after it:
const elements = [
{ item: 'something1', another: 'something1' },
{ item: 'something2', another: 'something2' },
{ item: 'something3', another: 'something3' }
];
const index = 1;
const newElements = [
...elements.slice(0, index + 1),
{
...elements[index],
item: 'new something !!!'
},
...elements.slice(index + 1)
];
console.log(newElements);
Ori Drori's quite correct about why the cloning isn't working. But there are two other issues with the code you'll want to address:
When setting state based on existing state, you must use the callback version of setState since state updates can be stacked and are asynchronous. So not:
this.setState({
elements: [/*...*/]
});
but instead:
this.setState(prevState => {
return {
elements: [/*...*/]
};
});
Partially because of #1 above, using the index is unreliable; other changes may have altered the array (your element may not even be in it anymore). Use the element itself. For instance, if your click handler is cloneClick, you'd use onClick={this.cloneClick.bind(this, el)} on the button to pass the element to the handler.
Here's a complete example:
class Example extends React.Component {
constructor(...args) {
super(...args);
this.state = {
elements: [
{ item: 'item1', another: 'something1' },
{ item: 'item2', another: 'something2' }
]
};
}
cloneClick(el) {
this.setState(prevState => {
const index = prevState.elements.indexOf(el);
if (index === -1) {
return null;
}
return {
elements: [
...prevState.elements.slice(0, index + 1),
{
...el,
item: el.item + " (copy)"
},
...prevState.elements.slice(index + 1)
]
};
// Alternately, this is more efficient:
/*
let clone = null;
const elements = [];
prevState.elements.forEach(prevEl => {
elements[elements.length] = prevEl;
if (prevEl === el) {
elements[elements.length] = clone = {
...el,
item: el.item + " (copy)"
};
}
});
return clone ? {elements} : null; // No update if the el wasn't found
*/
});
}
render() {
return <div>
{this.state.elements.map((el, i) =>
<div key={i}>
{el.item}
-
{el.another}
<input type="button" value="Clone" onClick={this.cloneClick.bind(this, el)} />
</div>
)}
</div>;
}
}
ReactDOM.render(
<Example />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>