As stated in the title I'm trying to change the state of an object nested in an array of objects. I just can not get this to work. I've added a sample of what I'm trying to do in a codesandbox.
I'm using a Material-UI component call ToggleButton (link to demo ToggleButton). I want to change the state to toggle the buttons in the group. I was able to get this working for a create function but can not get it working for my update function.
Trying to change the values of the object in the array is just not working for me. Below are some things I've tried to no success. I want to change the IsTrue: so I can toggle the button to display the users selection.
setAppetizer(true);
setRecipeObjectState({
...recipeObjectState,
Category: [
...recipeObjectState.Category,
{
IsTrue: appetizer,
category: "Appetizer",
},
],
});
This just adds more buttons to my button group.
setAppetizer(true);
setRecipeObjectState((prevState) => ({
...prevState,
Category: [
{
IsTrue: appetizer,
category: "Appetizer",
},
],
}));
I'm just lost now at this point. I just want to also state that this is my first React project that is not just a sandbox for learning the framework and I'm also a jr. developer trying to learn. I have search stackoverflow and nothing has helped me out. I hope I have included enough information for someone to help out.
Your state and app appear to be very convoluted, but the general idea when updating nested array state is to shallowly copy the state at each level where an update is being made. Use array::map to map the Category property to a new array object reference and when the category matches toggle the IsTrue "selected" property.
setRecipeObjectState((prevState) => ({
...prevState,
Category: prevState.Category.map((category) =>
category.category === newCategory // <-- newCategory value from toggled button
? {
...category,
IsTrue: !category.IsTrue
}
: category
)
}));
Since your "selected" calculation is selected={Boolean(item.IsTrue)} you'll want to ensure your IsTrue element values are actually togglable, i.e. just store the boolean value right in the array.
const recipeObject = {
AuthorId: authorId,
BookAuthor: bookAuthor,
BookTitle: bookTitle,
Calories: parseInt(calories),
Category: [
{
IsTrue: false,
category: "Appetizer"
},
{
IsTrue: false,
category: "Breakfast"
},
{
IsTrue: false,
category: "Soup / Salad"
},
{
IsTrue: false,
category: "Vegetarian"
},
{
IsTrue: true,
category: "Meat (Beef, Pork, Chicken)"
},
{
IsTrue: false,
category: "Fish"
},
{
IsTrue: false,
category: "Dessert"
}
],
Description: description,
DurationInMinCook: parseInt(durationInMinCook),
DurationInMinPrep: parseInt(durationInMinPrep),
ImageUrl: imageUrl,
Ingredients: addedIngredients, // array
Instructions: addedInstructions, // array
IsRecipe: true,
Likes: 0,
RecipeId: selectedRecipeId,
ServingSize: parseInt(servingSize),
Title: title,
YouTubeUrl: youTubeUrl
};
You mutating the same reference, you need to render a copy or the component won't render (shallow comparison):
Try updating state like this.
const oldState = recipeObjectState;
oldState.Category = [
{
IsTrue: appetizer,
category: "Appetizer"
}
];
setRecipeObjectState(oldState);
I didn't try your component because it's huge.
Updating a single object property in an array of objects seems to be a very common use-case. Here is generally how you do that. Suppose id is a unique identifier of each object and that we want to toggle selected:
import React, { useState } from "react";
import "./styles.css";
import faker from "faker";
const array = [];
for (let id = 1; id < 10; id++) {
array.push({
id,
name: faker.name.firstName(),
age: faker.random.number(100),
selected: false
});
}
export default function App() {
const [list, setList] = useState(array);
const onClick = (id) => (event) => {
setList((list) =>
list.map((item) =>
item.id === id ? { ...item, selected: !item.selected } : item
)
);
};
return (
<div className="App">
Click on a item:
<ul>
{list.map(({ id, name, age, selected }) => (
<li key={id} onClick={onClick(id)} className="item">
{name} {age}
{selected ? " ✅" : null}
</li>
))}
</ul>
</div>
);
}
https://codesandbox.io/s/xenodochial-river-9sq6g?file=/src/App.js:0-825
Related
I want to take data from js files classified as categories such as 'Delivery' and 'Cafe' and deliver different data to different pages.
I thought about how to import it using map(), but I keep getting errors such as 'products' is not defined.'
It must be done, but it is not implemented well with javascript and react weak. If you know how to do it, I'd appreciate it if you could let me know.
Products.js
export const Product = [
{
Delivery: [
{
id: '101',
productName: '허니랩',
summary: '밀랍으로 만든 친환경 식품포장랩 허니랩.',
description:
'~~',
images: ['3k7sH9F'],
companyName: '허니랩',
contact: '02-6082-2720',
email: 'lesslabs#naver.com',
url: 'https://honeywrap.co.kr/',
},
{
id: '102',
productName: '허니포켓',
summary: '밀랍으로 만든 친환경 식품포장랩 허니랩. 주머니형태.',
description:
"~~",
images: ['4zJEqwN'],
companyName: '허니랩',
contact: "02-6082-2720",
email: "lesslabs#naver.com",
url: "https://honeywrap.co.kr/",
},
],
},
{
HouseholdGoods: [
{
id: '201',
productName: '순둥이',
summary: '아기용 친환경 순한 물티슈',
description:
'~',
images: ['4QXJJaz'],
companyName: '수오미',
contact: '080-000-3706',
email: 'help#sumomi.co.kr',
url: 'https://www.suomi.co.kr/main/index.php',
},
{
id: '202',
category: ['HouseholdGoods'],
productName: '순둥이 데일리',
summary: '친환경 순한 물티슈',
description: '품질은 그대로이나 가격을 낮춘 경제적인 생활 물티슈',
images: ['OMplkd2'],
companyName: '수오미',
contact: '080-000-3706',
email: 'help#sumomi.co.kr',
url: 'https://www.suomi.co.kr/main/index.php',
},
],
},
];
Delivery.js
(The file was named temporarily because I did not know how to classify and deliver data without creating a js file separately.)
import React from "react";
function Delivery(
productName,
companyName,
contact,
email,
url,
summary,
description
) {
return (
<div className="Product">
<div className="Product__data">
<h3 className="Product__name">{productName}</h3>
<h4>{companyName}</h4>
<h5>Contact: {contact}</h5>
<h5>Email: {email}</h5>
<h5>URL: {url}</h5>
<p className="Product__summary">{summary}</p>
<p className="Proudct__descriptions">{description}</p>
</div>
</div>
);
}
export default Delivery;
Category.js
import React from "react";
import Delivery from "./Delivery";
import { Product } from "./Products";
class Category extends React.Component {
render() {
state = {
products: [],
};
this.setState(_renderProduct());
return <div>{products ? this._renderProduct() : "nothing"}</div>;
}
_renderProduct = () => {
const { products } = this.state;
const renderProducts = products.map((product, id) => {
return (
<Delivery
productName={Product.productName}
companyName={Product.companyName}
contact={Product.contact}
email={Product.email}
url={Product.url}
summary={Product.summary}
description={Product.description}
/>
);
});
};
}
export default Category;
Sorry and thank you for the long question.
There are quite a few different problems I've found.
First is that you call setState inside render in the Category component, this causes an infinite loop. Instead call setState inside a lifecycle method like componentDidMount or use the useEffect hook if using functional components.
Another problem is that state in Category is also defined inside render. In class components you would normally put this in a class constructor outside of render.
In your setState call you refer to _renderProduct(), this should be this._renderProduct() instead.
Now the main problem here is the structure of your data / how you render this structure.
Products is an array of objects where each object either has a Delivery or HouseholdGoods property which is an array of products. I would advise you to change this structure to something more like this:
export const Product = {
Delivery: [
{
id: "101",
},
{
id: "102",
},
],
HouseholdGoods: [
{
id: "201",
},
{
id: "202",
},
],
};
or this:
export const Product = [
{ id: "101", productType: "Delivery" },
{ id: "102", productType: "Delivery" },
{ id: "201", productType: "HouseholdGoods" },
{ id: "202", productType: "HouseholdGoods" },
];
I personally prefer the second structure, but I've implemented the first as this seems to be what you were going for:
class Category extends React.Component {
constructor(props) {
super(props);
this.state = {
products: null,
};
}
componentDidMount() {
this.setState({ products: Product });
}
render() {
const { products } = this.state;
return (
<div>
{products
? Object.keys(products).map((productKey) => {
return (
<div key={productKey}>
{products[productKey].map((product) => {
return (
<Delivery
key={product.id}
productName={product.productName}
companyName={product.companyName}
contact={product.contact}
email={product.email}
url={product.url}
summary={product.summary}
description={product.description}
/>
);
})}
</div>
);
})
: "no products"}
</div>
);
}
}
We need a nested loop here, because we need to map over each property key and over the array of objects inside each property. If you use the other structure for Product I've shown, you can simply map over Product without needing two loops.
Now the last important problem was that you weren't destructuring the props inside your Delivery component, instead you should do something like this:
function Delivery({
productName,
companyName,
contact,
email,
url,
summary,
description,
}) {
return (
<div className="Product">
<div className="Product__data">
<h3 className="Product__name">{productName}</h3>
<h4>{companyName}</h4>
<h5>Contact: {contact}</h5>
<h5>Email: {email}</h5>
<h5>URL: {url}</h5>
<p className="Product__summary">{summary}</p>
<p className="Proudct__descriptions">{description}</p>
</div>
</div>
);
}
Example Sandbox
I am creating a sidebar in my React application, before I used an array with objects for routing
SideBarItem.js
const SidebarItems = [
{
names: "General",
},
{
name: "E-commerce",
route: '/Maps',
},
{
name: "Maps",
route: '/Maps',
},
{
names: "About",
},
{
name: "Calendar",
route: '/Calendar'
},
];
export default SidebarItems;
Lesson.js
<ul>
{SidebarItems.map((item, index) => {
return (
<>
{item.names && (
<li className="header-menu">
<span>{item.names}</span>
</li>
)}
{item.name && (
<li>
<i className="fa fa-folder"></i>
<Link to={`${path}` + item.route}
className="link">{item.name}</Link>
</li>
)}
</>
);
})}
</ul>
And I got this side bar with links and paragraphs
The names property from the SidebarItems object is a simple <span> tag.
But now I use a different approach instead of creating an object with properties i use map it makes my work easier
const SidebarItems = ['Calendar', 'Messages', 'Notifications'].map((itemName) => ({
name: itemName,
route: `/${itemName}`
}));
This code performs the same logic as the old code. But I can't add the names property, I don't know how to do it, I tried to create an object inside an array
const SidebarItems = ['Calendar', 'Messages', {names: "General"}, 'Notifications'].map((itemName) => ({
name: itemName,
names: itemName.names,
route: `/${itemName}`
}));
But I get an error - Error: Objects are not valid as a React child (found: object with keys {names}). If you meant to render a collection of children, use an array instead.
How can I solve this problem?
Based on the array you're looping through, Here is what you need to do
const SidebarItems = [
"Calendar",
"Messages",
{ names: "General" },
"Notifications",
].map((itemName) => {
if (typeof itemName === "string") {
return {
name: itemName,
names: itemName.names || "",
route: `/${itemName}`,
};
} else {
return {
name: itemName.names,
names: itemName.names,
route: `/${itemName.names}`,
};
}
});
I've tried even manually setting the default values like in the documentation but no dice. I'm not sure if it's a styling issue or what. So below I posted what I have along with a screenshot.
<Select
components={animatedComponents}
getOptionLabel={convertToLabel}
getOptionValue={option => option.resource_name}
isMulti
onChange={changeEvent}
options={users}
theme={theme => ({
...theme,
borderRadius: 0
})}
defaultValue={(props.value || []).map(convertToValue)}
value={(props.value || []).map(convertToValue)}
/>
convertToValue function
const convertToValue = props => {
return {
label: `${props.name} ${props.family_name}`,
value: props.resource_name
};
};
convertToLabel function
const convertToLabel = props => {
return `${props.name} ${props.family_name}`;
};
changeEvent function
const changeEvent = (selectedOption, i) => {
let option = {
name: "reviewers",
value: selectedOption
};
update({ target: option });
};
users & props objects
users:
[
{
resource_name: "facebook_user1",
name: "Joe",
family_name: "Dirt"
},
{
resource_name: "facebook_user2",
name: "Trident",
family_name: "White"
}
]
props:
{
field: "placeholder",
fieldType: "placeholderType"
value:[
{
resource_name: "facebook_user1",
name: "Joe",
family_name: "Dirt"
},
{
resource_name: "facebook_user2",
name: "Trident",
family_name: "White"
}
]
}
What I see on my screen.
It is extremely difficult to tell exactly what your issue is, without seeing the actual JSX of your select render. Here are a few issues I see, looking at your question, and some hard guesses at what might be happening.
You should show us the full JSX render of your Select implementation
You never show us what your defaultValue prop looks like, but
remember that value is expected to be equal to one of your
options, not just an option 'value'
Your label and option getter
methods signature should be getOptionLabel = (option) => string and
getOptionValue = (option) => string. You've used props, which
might conflict with parent scope, in your instance.
You probably want
your convertToValue method signature to line up with those as well.
Your onChange event method signature doesn't line up with
React-Select, and may be causing you pain. See my answer to this
recent question for help on this.
Hi I am new to react and am trying to use onSelect to return on array of items that are associated with that name. I am using the dot filter method to filter an array so that only items with the same key as the name that is selected appear. However my array returns empty.
class HorizantScroller extends React.Component {
state = {
selected: 'Brands',
statelist: [
{name: "Brands",
items: ["1", "2", "3"]
},
{name: "Films",
items: ["f1", "f2", "f3"]
},
{name: "Holiday Destination",
items: ["f1", "f2", "f3"]
}
]
};
onSelect = key => {
this.setState({ selected: key });
const myList = this.state.statelist;
const myItemDetails = myList.filter(items=>items.key === key);
console.log(myItemDetails)
}
render() {
const { selected } = this.state;
// Create menu from items
const menu = Menu(this.state.statelist, selected);
const {statelist} = this.state;
return (
<div className="HorizantScroller">
<ScrollMenu
data={menu}
arrowLeft={ArrowLeft}
arrowRight={ArrowRight}
selected={selected}
onSelect={this.onSelect}
/>
<Items Items={Items[selected]}/>
</div>
);
}
}
export default HorizantScroller;
According to your data-structure, you need to use use item.name to check for the selectedKey
myList.filter(items=>items.name === key);
Note: you must make sure that you are not updating stateList state after filtering the array, otherwise your your state will loose the data
Instead you must use another state variable to store filtered list or apply the filter while render instead of storing the filtered value in state
I'm having trouble figuring out how to set a dynamic dropdown component with multiple-value selections to each rendered element in a feature I'm working on. I think I'm really close but ultimately need a bit of guidance.
Here's the component:
import React, { Component } from 'react'
import { List, Dropdown, Label } from 'semantic-ui-react'
const directions = [
{key: "0.0", text: "0.0", value: "0.0"},
{key: "22.5", text: "22.5", value: "22.5"},
{key: "45.0", text: "45.0", value: "45.0"},
{key: "67.5", text: "67.5", value: "67.5"},
{key: "90.0", text: "90.0", value: "90.0"}
]
const channels = [
{ch: 65, callsign: "TEST1"},
{ch: 49, callsign: "TEST2"},
{ch: 29, callsign: "TEST3"}
]
export default class DirectionalSelection extends Component {
constructor(props) {
super(props)
this.state = {
channels,
directions,
currentValues: {}
}
}
handleDropdownChange = (e, index, { value }) => {
this.setState(({ currentValues }) => {
currentValues[index] = value
return currentValues
})
}
handleDirAddition = (e, index, { value }) => {
this.setState(({ directions }) => {
directions[index] = [{ text: value, value }, ...this.state.directions]
return directions
})
}
render() {
const { channels, currentValues, directions } = this.state
return (
<div>
<List>
{channels.map((el, index) => (
<List.Item key={index}>
<Label>{el.ch}</Label>
<Dropdown
search
multiple
selection
scrolling
allowAdditions
options={directions}
value={currentValues[index]}
placeholder='Choose directions'
onAddItem={this.handleDirAddition.bind(this, index)}
onChange={this.handleDropdownChange.bind(this, index)}
/>
</List.Item>
))}
</List>
</div>
)
}
}
Right now every time I select dropdown values on any channel, currentValues returns as [object Object]: ["22.5", "45.0"]. I want to set the ch key in channels as the key and the dropdown values array as the value and append them to currentValues.
I hope I've clarified the question enough to understand. Here is a link to Semantic-UI-React docs with the original component I'm using: https://react.semantic-ui.com/modules/dropdown#dropdown-example-multiple-allow-additions. Thanks for the help!
I figured it out! It was so simple, just had to switch the params in handleDropdownChange = (e, index, { value }) to handleDropdownChange = (index, e, { value }). It was setting the event function as the object key.