I'm quite new with react stuff, what I am trying is to generate some dynamic stuff using .map()
This is my component:
import React, { Component } from "react";
class DynamicStuff extends Component {
state = {
options: [
{ id: 1, optionOne: "OptionOne" },
{ id: 2, optionTwo: "OptionTwo" },
{ id: 3, optionThree: "OptionThree" }
]
};
render() {
const options = [...this.state.options];
return (
<>
{options.map((option) => {
return {option}
})}
<span>{options.optionOne}</span>
<span>{options.optionTwo}</span>
<span>{options.optionThree}</span>
</>
);
}
}
export default DynamicStuff;
What I am doing wrong here and why the map is not generating expected result ?
Is it ok?
import React, { Component } from "react";
class DynamicStuff extends Component {
state = {
options: [
{ id: 1, value: "OptionOne" },
{ id: 2, value: "OptionTwo" },
{ id: 3, value: "OptionThree" }
]
};
render() {
const options = [...this.state.options];
return (
<>
{options.map((option) => {
return <span>{option.value}</span>
})}
</>
);
}
}
export default DynamicStuff;
You have made your options object incorrectly. We need to have a same attribute over all the objects in the array.
class App extends React.Component {
state = {
options: [
{ id: 1, option: "OptionOne" },
{ id: 2, option: "OptionTwo" },
{ id: 3, option: "OptionThree" }
]
};
render() {
const options = [...this.state.options];
return (
<>
{options.map((option, index) => (
<li key={index}>{option.option}</li>
))}
</>
);
}
}
Another thing, If you need to map an array. You don't need to have many spans. Having a one is just enough. The map function will iterate and give you all the things.
The map used here is actually to convert the js object into a react element, but your usage here is still a js object after the map conversion. The react element may be a <p key = {option.id}> {option. optionOne} </p>.
If there is a react element after the return in your map, it is correct.
{options.map((option) => {
return <p key = {option.id}> {option. optionOne} </p>
})}
or
{options.map((option) => <p key = {option.id}> {option. optionOne} </p>)}
YOu need to map and return the HTML element
return ({
options.map((option) => {
return `<span key={option.id}>${option. option}</span>`
})
});
You should do something like
render() {
const { options } = this.state;
return (
<div className="option-wrapper">
{options.length > 0 && options.map(option => {
return <span key={option.id}>{option.option}</span>
})}
</div>
);
}
Related
I am trying to create an ObjectList component, which would contain a list of Children.
const MyList = ({childObjects}) => {
[objects, setObjects] = useState(childObjects)
...
return (
<div>
{childObjects.map((obj, idx) => (
<ListChild
obj={obj}
key={idx}
collapsed={false}
/>
))}
</div>
)
}
export default MyList
Each Child has a collapsed property, which toggles its visibility. I am trying to have a Collapse All button on a parent level which will toggle the collapsed property of all of its children. However, it must only change their prop once, without binding them all to the same state. I was thinking of having a list of refs, one for each child and to enumerate over it, but not sure if it is a sound idea from design perspective.
How can I reference a dynamic list of child components and manage their state?
Alternatively, is there a better approach to my problem?
I am new to react, probably there is a better way, but the code below does what you explained, I used only 1 state to control all the objects and another state to control if all are collapsed.
Index.jsx
import MyList from "./MyList";
function Index() {
const objList = [
{ data: "Obj 1", id: 1, collapsed: false },
{ data: "Obj 2", id: 2, collapsed: false },
{ data: "Obj 3", id: 3, collapsed: false },
{ data: "Obj 4", id: 4, collapsed: false },
{ data: "Obj 5", id: 5, collapsed: false },
{ data: "Obj 6", id: 6, collapsed: false },
];
return <MyList childObjects={objList}></MyList>;
}
export default Index;
MyList.jsx
import { useState } from "react";
import ListChild from "./ListChild";
const MyList = ({ childObjects }) => {
const [objects, setObjects] = useState(childObjects);
const [allCollapsed, setallCollapsed] = useState(false);
const handleCollapseAll = () => {
allCollapsed = !allCollapsed;
for (const obj of objects) {
obj.collapsed = allCollapsed;
}
setallCollapsed(allCollapsed);
setObjects([...objects]);
};
return (
<div>
<button onClick={handleCollapseAll}>Collapse All</button>
<br />
<br />
{objects.map((obj) => {
return (
<ListChild
obj={obj.data}
id={obj.id}
key={obj.id}
collapsed={obj.collapsed}
state={objects}
setState={setObjects}
/>
);
})}
</div>
);
};
export default MyList;
ListChild.jsx
function ListChild(props) {
const { obj, id, collapsed, state, setState } = props;
const handleCollapse = (id) => {
console.log("ID", id);
for (const obj of state) {
if (obj.id == id) {
obj.collapsed = !obj.collapsed;
}
}
setState([...state]);
};
return (
<div>
{obj} {collapsed ? "COLLAPSED!" : ""}
<button
onClick={() => {
handleCollapse(id);
}}
>
Collapse This
</button>
</div>
);
}
export default ListChild;
I have 3 elements and I want to add a new element by clicking on any div, but the problem is after adding new elements to the array they don't get rendered out of the component.
import React, { useState } from "react";
import "./styles.css";
export default function App() {
let elements = [
{ id: 0, text: "first" },
{ id: 1, text: "second" },
{ id: 2, text: "third" }
];
const [state, setstate] = useState(elements);
function handleClick() {
elements.push({ id: 3, text: "xxx", checkBox: null });
setstate(elements);
console.log(state); //state shows 4 elememnt but they don't render in
}
return (
<div className="App">
{state.map((e) => (
// why this don't render the new elements?
<div onClick={handleClick}>{e.text}</div>
))}
</div>
);
}
in codesandbox https://codesandbox.io/s/beautiful-silence-c1t1k?file=/src/App.js:0-641
You should not mutate the state directly, it's not a good practice. Instead try as:
function handleClick() {
setstate(prevState => [
...prevState,
{ id: 3, text: "xxx", checkBox: null }
])
}
By doing this you are cloning the previous state of the array and adding that new element into the copy of the array what you can pass to setState function.
See the working CodeSandbox here.
You should not mutate the state directly
import React, { useState } from "react";
import "./styles.css";
const defaultElements = [
{ id: 0, text: "first" },
{ id: 1, text: "second" },
{ id: 2, text: "third" }
];
const newElement = {
id: 3,
text: "xxx",
checkBox: null
};
export default function App() {
const [state, setState] = useState(defaultElements);
function handleClick() {
setState((item) => [...item, newElement]);
}
return (
<div className="App">
{state.map(({ text }, index) => (
<div key={index} onClick={handleClick}>
{text}
</div>
))}
</div>
);
}
I'm working on an assignment where I need to implement an order management system. The problem is that I need to make a GET call to get all the orders, and for each order I need to make another GET call so I can get the Items of this order.
My question is how can I make those calls and create some data structure of orders and items before rendering everything.
I tried using async/await but I couldn't manage to create this data structure of orders and their related items before everything rendered.
For now I only have the orders GET call handled
async componentDidMount() {
const orders = await api.getOrders()
this.setState({
orders
});
}
I also created a function for the GET calls of the items which returns a Promise<Item[]>
createItemsList = (order: Order) => {
let a = order.items.map((item) => {
return api.getItem(item.id);
});
return Promise.all(a);
};
Any suggestion for a way to combine those two? Thanks in advance!
*** Editing ***
This is the part of the code where I render the orders
{filteredOrders.map((order) => (
<div className={'orderCard'}>
<div className={'generalData'}>
<h6>{order.id}</h6>
<h4>{order.customer.name}</h4>
<h5>Order Placed: {new Date(order.createdDate).toLocaleDateString()},
At: {new Date(order.createdDate).toLocaleTimeString()}</h5>
</div>
<Fulfilment order={order}/>
<div className={'paymentData'}>
<h4>{order.price.formattedTotalPrice}</h4>
<img src={App.getAssetByStatus(order.billingInfo.status)}/>
</div>
<ItemsList subItemsList={order.items} api={api}/>
</div>
))}
The component ItemsList is where I render the Items of a specific order, and order.items is not the items itself but an array of items ID and quantities which I get with each order
I suggest you move the data retrieval into each component.
Check the sandbox here
import React, { PureComponent } from "react";
const fakeOrderItems = {
1: [
{
id: 1,
name: "Ramen",
qty: 1
},
{
id: 1,
name: "Beer",
qty: 1
}
],
2: [
{
id: 1,
name: "Steak",
qty: 1
},
{
id: 2,
name: "Iced Tea",
qty: 1
}
]
};
const fakeOrders = [
{
id: 1,
name: "Table 1",
totalItems: 2
},
{
id: 2,
name: "Table 3",
totalItems: 2
}
];
const fakeApi = {
getOrders() {
return new Promise((resolve) =>
setTimeout(() => resolve(fakeOrders), 3000)
);
},
getOrderItems(id) {
return new Promise((resolve) =>
setTimeout(() => resolve(fakeOrderItems[id]), 3000)
);
}
};
class OrderItem extends PureComponent {
render() {
const { id, name, qty } = this.props;
return (
<div style={{ marginBottom: 10 }}>
<span>
{id}. {name} qty:{qty}
</span>
</div>
);
}
}
class OrderItemList extends PureComponent {
state = {
orderItems: []
};
componentDidMount() {
fakeApi
.getOrderItems(this.props.orderId)
.then((orderItems) => this.setState({ orderItems }));
}
render() {
const { orderItems } = this.state;
if (!orderItems.length) {
return <span>Loading orderItems...</span>;
}
return orderItems.map((item) => (
<OrderItem key={item.id + item.name} {...item} />
));
}
}
class Order extends PureComponent {
render() {
const { id, name } = this.props;
return (
<div style={{ marginBottom: 10 }}>
<div>
<span>Order #{id}</span>
</div>
<div>
<span>For table {name}</span>
</div>
<OrderItemList orderId={id} />
</div>
);
}
}
class OrderList extends PureComponent {
state = {
orders: []
};
componentDidMount() {
fakeApi.getOrders().then((orders) => this.setState({ orders }));
}
render() {
const { orders } = this.state;
if (!orders.length) {
return <div>Loading orders...</div>;
}
return orders.map((order) => <Order key={order.id} {...order} />);
}
}
export default function App() {
return <OrderList />;
}
Create a separate method outside that gets the data you need.
First get the orders from the API. Then loop through each order and call the api again for each item. Await the Promise.all to wait for each item in the order to finish, then concatenate the result to an array where you store all the fetched items. Then after the loop is finished return the results array.
In your componentDidMount call this method and update the state based on the result that the promised returned.
state = {
orders: []
}
async getOrderItems() {
let orderItems = [];
const orders = await api.getOrders()
for (const order of orders) {
const items = await Promise.all(
order.items.map((item) => api.getItem(item.id))
)
orderItems = [...orderItems, ...items]
}
return orderItems
}
componentDidMount() {
this.getOrderItems().then(orders => {
this.setState({
orders
})
})
}
render() {
if (this.state.orders.length === 0) {
return null
}
return (
{this.state.orders.map(order => (
// Render content.
)}
)
}
You can try this solution.I was stuck in the same issue a few days ago.So what it does is that setState renders after createItemsList function is run
async componentDidMount() {
const orders = await api.getOrders()
this.setState({
orders
}),
() => this.createItemsList()
}
I am making API calls and rendering different components within an object. One of those is illustrated below:
class Bases extends Component {
constructor() {
super();
this.state = {
'basesObject': {}
}
}
componentDidMount() {
this.getBases();
}
getBases() {
fetch('http://localhost:4000/cupcakes/bases')
.then(results => results.json())
.then(results => this.setState({'basesObject': results}))
}
render() {
let {basesObject} = this.state;
let {bases} = basesObject;
console.log(bases);
//FALSY values: undefined, null, NaN, 0, false, ""
return (
<div>
{bases && bases.map(item =>
<button key={item.key} className="boxes">
{/* <p>{item.key}</p> */}
<p>{item.name}</p>
<p>${item.price}.00</p>
{/* <p>{item.ingredients}</p> */}
</button>
)}
</div>
)
}
}
The above renders a set of buttons. All my components look basically the same.
I render my components here:
class App extends Component {
state = {
ordersArray: []
}
render() {
return (
<div>
<h1>Bases</h1>
<Bases />
<h1>Frostings</h1>
<Frostings />
<h1>Toppings</h1>
<Toppings />
</div>
);
}
}
I need to figure out the simplest way to, when a button is clicked by the user, add the key of each clicked element to a new array and I am not sure where to start. The user must select one of each, but is allowed to select as many toppings as they want.
Try this
We can use the same component for all categories. All the data is handled by the parent (stateless component).
function Buttons({ list, handleClick }) {
return (
<div>
{list.map(({ key, name, price, isSelected }) => (
<button
className={isSelected ? "active" : ""}
key={key}
onClick={() => handleClick(key)}
>
<span>{name}</span>
<span>${price}</span>
</button>
))}
</div>
);
}
Fetch data in App component, pass the data and handleClick method into Buttons.
class App extends Component {
state = {
basesArray: [],
toppingsArray: []
};
componentDidMount() {
// Get bases and toppings list, and add isSelected attribute with default value false
this.setState({
basesArray: [
{ key: "bases1", name: "bases1", price: 1, isSelected: false },
{ key: "bases2", name: "bases2", price: 2, isSelected: false },
{ key: "bases3", name: "bases3", price: 3, isSelected: false }
],
toppingsArray: [
{ key: "topping1", name: "topping1", price: 1, isSelected: false },
{ key: "topping2", name: "topping2", price: 2, isSelected: false },
{ key: "topping3", name: "topping3", price: 3, isSelected: false }
]
});
}
// for single selected category
handleSingleSelected = type => key => {
this.setState(state => ({
[type]: state[type].map(item => ({
...item,
isSelected: item.key === key
}))
}));
};
// for multiple selected category
handleMultiSelected = type => key => {
this.setState(state => ({
[type]: state[type].map(item => {
if (item.key === key) {
return {
...item,
isSelected: !item.isSelected
};
}
return item;
})
}));
};
// get final selected item
handleSubmit = () => {
const { basesArray, toppingsArray } = this.state;
const selectedBases = basesArray.filter(({ isSelected }) => isSelected);
const selectedToppings = toppingsArray.filter(({ isSelected }) => isSelected);
// submit the result here
}
render() {
const { basesArray, toppingsArray } = this.state;
return (
<div>
<h1>Bases</h1>
<Buttons
list={basesArray}
handleClick={this.handleSingleSelected("basesArray")}
/>
<h1>Toppings</h1>
<Buttons
list={toppingsArray}
handleClick={this.handleMultiSelected("toppingsArray")}
/>
</div>
);
}
}
export default App;
CSS
button {
margin: 5px;
}
button.active {
background: lightblue;
}
I think the following example would be a good start for your case.
Define a handleClick function where you can set state with setState as the following:
handleClick(item) {
this.setState(prevState => {
return {
...prevState,
clickedItems: [...prevState.clickedItems, item.key]
};
});
}
Create an array called clickedItems in constructor for state and bind handleClick:
constructor() {
super();
this.state = {
basesObject: {},
clickedItems: [],
}
this.handleClick = this.handleClick.bind(this);
}
You need to add a onClick={() => handleClick(item)} handler for onClick:
<button key={item.key} className="boxes" onClick={() => handleClick(item)}>
{/* <p>{item.key}</p> */}
<p>{item.name}</p>
<p>${item.price}.00</p>
{/* <p>{item.ingredients}</p> */}
</button>
I hope that helps!
When I'm clicking the button, I am getting that all the values are undefined for the "name" and the "value". I am not sure why, my binding seems correct.
I've tried changing the bindings, I've tried calling an anonymous function for the onClick and passing in the item within my map function. No luck.
import React, { Component } from 'react'
const starterItems = [{ id: 1, name: 'longsword', enhancement: 4 },
{ id: 2, name: 'gauntlet', enhancement: 9 },
{ id: 3, name: 'wizard\'s staff', enhancement: 14 }];
export default class Items extends Component {
constructor(props) {
super(props)
this.handleScoreChange = this.handleScoreChange.bind(this);
this.state = {
items: starterItems
}
}
handleScoreChange(e){
let { name, value } = e.target;
const id = name;
const newScore = value++;
const items = this.state.items.slice();
items.forEach((item) => {
if (item[id] === name){
item.enhancement = newScore
}
});
this.setState(items);
};
render() {
return (
<div>
<h3 data-testid="title">Items</h3>
{this.state.items.map(item => (
<div key={item.id}>
<div name={item.name}data-testid="item">{item.name}</div>
<div name={item.enhancement}data-testid="enhancement" value=
{item.enhancement}>{item.enhancement}
</div>
<button onClick={this.handleScoreChange}>Enhance</button>
</div>
))}
</div>
);
};
}
I am expecting the value of the item passed through to +1
e.target is the DOM reference for the button
including name and value as attributes for the div are not necessary
If you want to get the values for the current name and enhancement when clicking you can add a binding
{
this.state.items.map(item => {
const onClick = this.handleScoreChange.bind(this, item.name, item.enhancement)
return (
<div key={item.id}>
<div name={item.name}data-testid="item">{item.name}</div>
<div name={item.enhancement}data-testid="enhancement" value={item.enhancement}>{item.enhancement}</div>
<button onClick={onClick}>Enhance</button>
</div>
)
)
}
...
handleScoreChange(name, enhancement) {
// your logic here
}
<button onClick={()=>this.handleScoreChange(...item)}>Enhance</button>
please try this i am sure this will work for you
thanks
This Solution Worked. Slight modification from what Omar mentioned. Thank you
import React, { Component } from 'react'
const starterItems = [{ id: 1, name: 'longsword', enhancement: 4 },
{ id: 2, name: 'gauntlet', enhancement: 9 },
{ id: 3, name: 'wizard\'s staff', enhancement: 14 }];
export default class Items extends Component {
constructor(props) {
super(props)
this.state = {
items: starterItems
}
}
enhanceItem(passedItem) {
const newScore = passedItem.enhancement + 1;
const items = this.state.items.slice(); /* creates a copy of state that we can change */
items.forEach((item) => {
if (item.id === passedItem.id) {
return item.enhancement = newScore
}
});
this.setState(items);
};
render() {
return (
<div>
<h3 data-testid="title">Items</h3>
{
this.state.items.map(item => {
const onClick = this.enhanceItem.bind(this, item)
/* this line above binds the item with the onClick Function */
/* notice below, I had to put a return since I added the above code */
return (
<div key={item.id}>
<div data-testid="item">{item.name}</div>
<div data-testid="enhancement">{item.enhancement}</div>
{/* We can use the "data=testid" as a way to grab an item, since it won't impact the html any */}
<button onClick={onClick}>Enhance</button>
</div>
)
})};
</div>
);
};
}