How to find specific items in an array in React/Framer? - javascript

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

Related

I'm trying to add to an array of objects that is broken into two inputs in React

So I have an array of objects where the keys are 'cost' and 'service' called estimate. You can add to the array by clicking 'Add' which adds a new index (i) to the array. The issue is on the first cycle I get a good array of {'cost': 2500, 'service': "commercial cleaning"} (imgSet-1) but when I add another item it completely erases the array and sets only one of the nested objects key and value. (imgSet-2). This is the outcome I'm looking for once the state has been saved (imgSet-3) I have tried going with #RubenSmn approach but then I receive this error. (imgSet-4)
imgSet-1 *********
Adding an initial service
Outcome of the initial service addition
imgSet-2 *********
Adding the second service
Outcome of the second service addition
imgSet-3 *********
imgSet-4 *********
Below is the code for the part of the page where you can add services and the output of the text inputs.
const [estimate, setEstimate] = useState([]);
{[...Array(numServices)].map((e, i) => {
return (
<div key={i} className="flex justify-between">
<div>
<NumericTextBoxComponent
format="c2"
name={`cost-${i}`}
value={estimate?.items?.["cost"]?.[i]}
change={(e) =>
setEstimate({ ...estimate, items: [{...estimate?.items?.[i],cost: e?.value}]})
}
placeholder='Price'
floatLabelType="Auto"
data-msg-containerid="errorForCost"
/>
</div>
<div>
<DropDownListComponent
showClearButton
fields={{ value: "id", text: "service" }}
name={`service-${i}`}
value={estimate?.items?.["service"]?.[i]}
change={(e) =>
setEstimate({ ...estimate, items: [{...estimate?.items?.[i],service: e?.value}]})
}
id={`service-${i}`}
floatLabelType="Auto"
data-name={`service-${i}`}
dataSource={estimateData?.services}
placeholder="Service"
data-msg-containerid="errorForLead"
></DropDownListComponent>
<div id="errorForLead" />
</div>
</div>
);
})}
</form>
<button onClick={() => setNumServices(numServices + 1)}>Add</button>
I have tried multiple variations of spread operators but I can't seem to get it to work. My expected result would be:
estimate:{
items: [
{'cost': 2500, 'service': 'Commercial Clean'},
{'cost': 500, 'service': 'Bathroom Clean'},
{'cost': 180, 'service': 'Apartment Clean'},
{etc.}
]
}
The initial state is an array which is not the object you're setting in the change handlers. You can have an initial state like this.
const [estimate, setEstimate] = useState({ items: [] });
You're not adding back the old items of the state when you're setting the new state.
setEstimate({
...estimate,
items: [{ ...estimate?.items?.[i], cost: e?.value }],
// should be something like
// items: [...estimate.items, { ...estimate.items?.[i], cost: e?.value }],
});
But you can't do that since it will create a new object in your items array every time you change a value.
I made this dynamic handleChange function which you can use for you state changes. The first if statement is to check if the itemIndex is already in the items array. If not, create a new item with the propertyName and the value
const handleChange = (e, itemIndex, propertyName) => {
const newValue = e?.value;
setEstimate((prevEstimate) => {
if (prevEstimate.items.length <= itemIndex) {
const newItem = { [propertyName]: newValue };
return {
...prevEstimate,
items: [...prevEstimate.items, newItem]
};
}
// loop over old items
const newItems = [...prevEstimate.items].map((item, idx) => {
// if index' are not the same just return the old item
if (idx !== itemIndex) return item;
// else return the item with the new service
return { ...item, [propertyName]: newValue };
});
return {
...prevEstimate,
items: newItems,
};
});
};
For the Service dropdown, you can do the same for the Cost just change the property name
<DropDownListComponent
...
value={estimate.items[i]?.service}
change={(e) => handleChange(e, i, "service")}
...
></DropDownListComponent>
See here a simplified live version

How to dynamically change a nested state with a recursive function?

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??

How can I add new elements to a list?

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

Not rendering JSX from function in React

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.

how to make a dynamic text show the correct sentence in react when deleting a component (custom html element)

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.

Categories

Resources