I'm pretty new to react and I'm trying to update a list from a child component based on user input add it will not update correctly. The idea is the user is able to add multiple different bikes in one form, each with a type and an ageRange property. When the user clicks an "Add Bike" button it adds BikeInput component (which works fine) and is supposed to be adding a new empty bike to a list of bikes that will be sent when the form is submitted. I console logged in a useEffect function the list of bikes after adding a new input and that works okay, but when I try to set one of the new bikes it removes all the elements from the list except the first. Again I'm pretty new to react so I'm not exactly sure if I'm using the useEffect function correctly or if there's another way to go about this, but if you could let me know that'd be amazing.
Here's some snippets of the important parts of the code that relate to the type property since the ageRange should work the same way
Parent Component:
import { useState, useEffect } from 'react'
const initialList = {
"id": 0,
"type": "",
"ageRange": ""
};
function Donate() {
const [bikes, setBikes] = useState([initialList]);
useEffect(() => console.log(bikes), [bikes])
const setBikeType = (bikeType, bikeIndex) => {
const updateBikes = bikes.map((bike) => {
if (bike.id == bikeIndex) {
bike.type = bikeType;
}
return bike;
});
setBikes(updateBikes);
}
const [bikeInputs, setBikeInputs] = useState([
<BikeInput
setBikeType={setBikeType}
setBikeAgeRange={setBikeAgeRange}
bikeIndex={0} />]);
const addBikeForm = (e) => {
e.preventDefault();
var newBikeIndex = bikeInputs.length;
setBikeInputs(bikeInputs =>
[...bikeInputs,
<BikeInput
setBikeType={setBikeType}
setBikeAgeRange={setBikeAgeRange}
bikeIndex={newBikeIndex}
/>
]
);
var newBikeId = bikes[bikes.length - 1].id + 1;
setBikes(bikes => [...bikes, { "id": newBikeId, "type": "", "ageRange": "" }]);
};
return (
<div className="bike-form-container donate-form-one">
...
<p className="input-title">Bike Info.</p>
{bikeInputs.map((item, i) => (
<div className="bike-input" key={i}>{item}</div>
))}
<button className="add-bike-btn" onClick={addBikeForm}>
<i class="fa-solid fa-circle-plus"></i> Add A Bike
</button>
...
</div>
)
}
export default Donate
Child Component (BikeInput):
function BikeInput(props) {
return (
<div className="input-container">
<select className="form-dropdown text-input"
defaultValue="Type"
onChange={e => props.setBikeType(e.target.value, props.bikeIndex)} >
<option disabled>Type</option>
<option value="Mountain"> Mountain </option>
<option value="Road"> Road </option>
<option value="Cruiser"> Cruiser </option>
<option value="Hybrid"> Hybrid </option>
<option value="Three Wheel"> Three Wheel (Tricycle) </option>
</select>
...
</div>
)
}
export default BikeInput
Remove your bikeInputs state, since you don't have to keep a collection of BikeInputs components. Just use the BikeInput component inside bikes.map to render each bike select option.
Please simplify and update your Donate component code as follows:
export function Donate() {
const [bikes, setBikes] = useState([initialList]);
useEffect(() => console.log(bikes), [bikes]);
const setBikeType = useCallback(
(bikeType, bikeIndex) => {
const updateBikes = bikes.map((bike) => {
if (bike.id === bikeIndex) {
bike.type = bikeType;
}
return bike;
});
setBikes(updateBikes);
},
[bikes]
);
const addBikeForm = useCallback(
(e) => {
e.preventDefault();
setBikes((bikes) => {
const newBikeId = bikes[bikes.length - 1].id + 1;
return [...bikes, { id: newBikeId, type: "", ageRange: "" }];
});
},
[setBikes]
);
return (
<div className="bike-form-container donate-form-one">
<p className="input-title">Bike Info.</p>
{bikes.map((item, i) => (
<div className="bike-input" key={i}>
<BikeInput bikeIndex={i} setBikeType={setBikeType} />
</div>
))}
<button className="add-bike-btn" onClick={addBikeForm}>
<i className="fa-solid fa-circle-plus"></i> Add A Bike
</button>
</div>
);
}
Related
I am a bit lost on what to do for the next step. I have managed to display the content but I can't seem to get it to filter with a click. It was easy enough to do with a different api , i followed webdevsimplified but this i can't work out and I am at my wits end!
All I want is to filter through the mapped api. for example if I check 3, it should show me only 3 starRating. Can anybody offer me some advice please.
App.js
import { useEffect, useState, useRef } from 'react'
import Header from './components/Header';
import SearchBar from './components/SearchBar';
export default function App() {
const [hotelRooms, setHotelRooms] = useState([]);
const fetchHotels = async () => {
const res = await fetch('https://obmng.dbm.guestline.net/api/hotels?collection-id=OBMNG')
const hotels = await res.json()
const hotelRooms = []
for(const hotel of hotels) {
const res = await fetch(`https://obmng.dbm.guestline.net/api/roomRates/OBMNG/${hotel.id}`)
const info = await res.json()
hotelRooms.push({ hotel, rooms: info.rooms })
}
setHotelRooms(hotelRooms)
}
useEffect(() => {
fetchHotels()
}, [])
return (
<div className="App">
<Header/>
{
hotelRooms.map(h => (
<div>
<input value={"1"} type="checkbox" onChange={}/>
<input value={"Adults"}type="checkbox" onChange={}/>
<h2> Name: {h.hotel.name}</h2>
<p> Description: {h.hotel.description}</p>
<p> Rating: {h.hotel.starRating}</p>
<p> Postcode: {h.hotel.postcode}</p>
<p> City: {h.hotel.town}</p>
<img src={h.hotel.images}/>
<p style={{ fontWeight: 'bold' }}>Rooms:</p>
{
h.rooms.map(room => (
<div>
<h5>Occupancy</h5>
<div> Adults: {room.occupancy.maxAdults}</div>
<div> Children: {room.occupancy.maxChildren}</div>
<div> Maximum guests: {room.occupancy.maxOverall}</div>
<div> Room type: {room.name}</div>
<img src={room.images}/>
</div>
))
}
</div>
))
}
</div>
);
}
You should have a state that saves the filtered properties.
const [filter, setFilter] = useState({ ratings: ["1", "2", "3", "4", "5"] });
When you show the checkboxes add a name to them and the respective values.
Remember when you use .map in render, add an unique key to the out most tag.
<div>
{["1", "2", "3", "4", "5"].map((star) => (
<div key={"input-" + star}>
<input
id={"rated" + star}
value={star}
name="ratings"
type="checkbox"
checked={filter.ratings.includes(star)}
onChange={handleRatingFilter}
/>
<label htmlFor={"rated" + star}>Rated {star} star</label>
</div>
))}
</div>
Now in the onChange handler, update the state according to the checkboxes:
const handleRatingFilter = (e) => {
if (e.target.checked) {
// adding value
const temp = [...filter.ratings];
temp.push(e.target.value);
setFilter({ ...filter, ratings: temp });
} else {
// removing value
setFilter({
...filter,
ratings: [...filter.ratings.filter((v) => v !== e.target.value)]
});
}
};
Finally, when you use .map on hotelRooms you can filter the list before mapping it.
{hotelRooms
.filter((h) => filter.ratings.includes(h.hotel.starRating))
.map((h) => (
<div key={h.hotel.name}>
stuff
</div>
))
}
Working CodeSandbox
If I am understanding your question correctly, you want it to re-render after you update hotelRooms? If this is correct, when you first render it, the value is [], a blank array. And in here :
useEffect(() => {
fetchHotels()
}, [])
That last bit [], runs once after rendering. Therefore in your case (if my assumption is correct), you will want to change it to, as you want it to re-render each time hotelRooms value change
useEffect(() => {
fetchHotels()
}, [hotelRooms])
I have a react program that displays a table based on values of a dropdown. I want the program to display the table by default based on the first value in the dropdown.
The first value in the dropdown is very different from the value made as default, and the dropdown values are always changing. So the data can be misleading when it loads for the first time.
here is a snippet of my code with a little description within. Thanks.
const CompletenessApp = () => {
const [completeness, setcompleteness] = useState([]);
const [loading, setloading] = useState(false);
const [valueSelected, setValueSelected] = useState({value:'all'});
const [periodSelected, setPeriodSelected] = useState({value:'20200702'}); // the default value that is used to filter the data.
const valid = [
{id:"all", variable:"all"},{id:"true", variable:"true"}, {id:"false", variable:"false"}, {id:"null", variable:"null"},
];
useEffect(() => {
const fetchData = async () => {
try{
const res = await axios.get('http://localhost:8000/api/completeness/');
setcompleteness(res.data);
setloading(true);
} catch (e) {
console.log(e)
}
}
fetchData();
}, []);
// when the page loads for the first time it filters the data based on the period value set in the state. I want the data to be filtered based on the first value in the dropdown instead, the first value in the dropdown is different from the default value set.
const handlePeriodChange = e => {
setPeriodSelected({value : e.target.value})
}
const handleValueChange = e => {
let boolvalue = Boolean
e.target.value === 'true'? boolvalue = true:
e.target.value === 'false'? boolvalue = false:
e.target.value === 'all'? boolvalue = 'all':
boolvalue=null
setValueSelected({value : boolvalue})
}
//filtered data to be displayed
const filteredCompleteness = completeness.filter(
completedata=> (completedata.period === periodSelected.value)
&&(valueSelected.value !== 'all'?completedata.complete === valueSelected.value:{}))
return(
<div>
<div className="selection-row">
<div className="stats-columns">
<div className="stats-label">Period</div>
//the relevant dropdown is here
<select onChange={e => handlePeriodChange(e)}>
{Array.from(new Set(completeness.map(obj => obj.period)))
.sort().reverse()
.map(period => {
return <option value={period}>{period}</option>
})}
</select>
</div>
<div className="stats-columns">
<div className="stats-label">Value</div>
<select onChange={e => handleValueChange(e)}>
{valid.map((obj) => {
return <option value={obj.id}>{obj.variable}</option>
})
}
</select>
</div>
</div>
<hr></hr>
<div className="results-table">
//table is displayed here
</div>
</div>
);
}
export default CompletenessApp;
// above return
const options = Array.from(new Set(completeness.map((obj) => obj.period)))
.sort()
.reverse()
.map((period) => {
return {
value: period,
label: period,
};
});
// in jsx
<select defaultValue={options[0].value} onChange={e => handlePeriodChange(e)}>
{
options.map((obj) => {
return <option value={obj.value}>{obj.label}</option>;
})
}
</select>
Try this and let me know.
I am trying to implement a multi-step wizard using a state machine and am unsure how to handle some configurations. To illustrate this I put together an example of a wizard that helps you prepare a dish.
Assuming the following example what would be the appropriate way to model this form/wizard behavior as a state machine?
Step 1 - Dish
pick a dish from ["Salad", "Pasta", "Pizza"]
Step 2 - Preparation Method
pick a preparation method from ["Oven", "Microwave"]
Step 3 - Ingredients
add and select ingredients in a form, depending on the dish and the preparation method the form will look different
// ingredients based on previous selections
("Pizza", "Oven") => ["tomato", "cheese", "pepperoni"]
("Pizza", "Microwave") => ["cheese", "pepperoni", "mushrooms"]
("Pasta", "Oven") => ["parmesan", "butter", "creme fraiche"]
("Pasta", "Microwave") => ["parmesan", "creme fraiche"]
("Salad") => ["cucumber", "feta cheese", "lettuce"]
I tried to simplify the problem as much as possible. Here are my questions:
In step 3 I want to show a form with various fields of different types. The selections in step 1 and 2 define which fields will be shown in the form in step 3. What is the appropriate way to specify this form configuration?
Step 2 should be skipped if the selected dish from step 1 is "Salad". What is the appropriate way to declare this?
I plan to implement this using xstate as the project I'm working on is written in react.
Edit: I updated the example in reaction to Martins answer. (see my comment on his answer)
Edit 2: I updated the example in reaction to Davids answer. (see my comment on his answer)
For the overall flow, you can use guarded transitions to skip the method step if "salad" was selected:
const machine = createMachine({
initial: 'pick a dish',
context: {
dish: null,
method: null
},
states: {
'pick a dish': {
on: {
'dish.select': [
{
target: 'ingredients',
cond: (_, e) => e.value === 'salad'
},
{
target: 'prep method',
actions: assign({ dish: (_, e) => e.value })
}
]
}
},
'prep method': {
on: {
'method.select': {
target: 'ingredients',
actions: assign({ method: (_, e) => e.value })
}
}
},
'ingredients': {
// ...
}
}
});
And you can use the data-driven configuration from Matin's answer to dynamically show ingredients based on the context.dish and context.method.
You need to have a data structure that holds data and the relationship between them then you can use state to store the selected item and have your logic to display/hide specific step.
Below is just a simple example to show how you can do it:
Sandbox example link
const data = [
{
// I recommend to use a unique id for any items that can be selective
dish: "Salad",
ingredients: ["ingredient-A", "ingredient-B", "ingredient-C"],
preparationMethods: []
},
{
dish: "Pasta",
ingredients: ["ingredient-E", "ingredient-F", "ingredient-G"],
preparationMethods: ["Oven", "Microwave"]
},
{
dish: "Pizza",
ingredients: ["ingredient-H", "ingredient-I", "ingredient-G"],
preparationMethods: ["Oven", "Microwave"]
}
];
export default function App() {
const [selectedDish, setSelectedDish] = useState(null);
const [selectedMethod, setSelectedMethod] = useState(null);
const [currentStep, setCurrentStep] = useState(1);
const onDishChange = event => {
const selecetedItem = data.filter(
item => item.dish === event.target.value
)[0];
setSelectedDish(selecetedItem);
setSelectedMethod(null);
setCurrentStep(selecetedItem.preparationMethods.length > 0 ? 2 : 3);
};
const onMethodChange = event => {
setSelectedMethod(event.target.value);
setCurrentStep(3);
};
const onBack = () => {
setCurrentStep(
currentStep === 3 && selectedMethod === null ? 1 : currentStep - 1
);
};
useEffect(() => {
switch (currentStep) {
case 1:
setSelectedDish(null);
setSelectedMethod(null);
break;
case 2:
setSelectedMethod(null);
break;
case 3:
default:
}
}, [currentStep]);
return (
<div className="App">
{currentStep === 1 && <Step1 onDishChange={onDishChange} />}
{currentStep === 2 && (
<Step2
onMethodChange={onMethodChange}
selectedMethod={selectedMethod}
selectedDish={selectedDish}
/>
)}
{currentStep === 3 && <Step3 selectedDish={selectedDish} />}
{selectedDish !== null && (
<>
<hr />
<div>Selected Dish: {selectedDish.dish}</div>
{selectedMethod !== null && (
<div>Selected Method: {selectedMethod}</div>
)}
</>
)}
<br />
{currentStep > 1 && <button onClick={onBack}> Back </button>}
</div>
);
}
const Step1 = ({ onDishChange }) => (
<>
<h5>Step 1:</h5>
<select onChange={onDishChange}>
<option value={null} disabled selected>
Select a dish
</option>
{data.map(item => (
<option key={item.dish} value={item.dish}>
{item.dish}
</option>
))}
</select>
</>
);
const Step2 = ({ onMethodChange, selectedMethod, selectedDish }) => (
<>
<h5>Step 2:</h5>
<div>
<select onChange={onMethodChange} value={selectedMethod}>
<option value={null} disabled selected>
Select a method
</option>
{selectedDish.preparationMethods.map(method => (
<option key={method} value={method}>
{method}
</option>
))}
</select>
</div>
</>
);
const Step3 = ({ selectedDish }) => (
<>
<h5>Step 3:</h5>
<h4>List of ingredient: </h4>
{selectedDish.ingredients.map(ingredient => (
<div key={ingredient}>{ingredient}</div>
))}
</>
);
I have an array with some orders, the name the array is getOrdersBySalesman. The orders have some attributes : id, client, stage. I want to filter the order by stage, but I it doesn't work,
When I select for example "PENDENT" why the console.log is [] I need to send the result of filterOrders to another component and I don't have result. What is wrong????
const Comandes = () => {
const [orderStage, setOrderStage] = useState('')
useEffect(() => {
setOrderStage(orderStage)
}, [orderStage])
let filterOrders = []
const filterStage = newStage => {
getOrdersBySalesman.forEach(order => {
if (order.stage === newStage) {
filterOrders.push(order)
}
});
setOrderStage(newStage)
}
console.log(filterOrders) = > IS []
return (
<div>
<Layout>
<select
value={orderStage}
onChange={e => filterStage(e.target.value)}
>
<option value="TOTS ELS ESTATS">TOTS ELS ESTATS</option>
<option value="ACABADA">ACABADA</option>
<option value="PENDENT">PENDENT</option>
<option value="CANCELADA">CANCELADA</option>
</select>
{filterOrders.length === 0 ? (
<p className="mt-5 text-center text-2xl">Encara no hi ha comandes </p>
) : (filterOrders.map(order => (
<Order key={order.id} order={order} />
))
)}
</Layout>
</div >
)
}
export default Comandes
Actually Array#forEach won't return anything and it's just going to run on select, onChange event. So you should either wait for changing select. Also, for better performance, you should use Array#filter and useState hook instead of declaring a variable and using Array#forEach.
So your final code should be something like this:
const [filterOrders , setFilterOrders ] = useState([])
const filterStage = (newStage) => {
setFilterOrders(getOrdersBySalesman.filter(
(order) => order.stage === newStage
));
setOrderStage(newStage);
};
useEffect(()=> {
console.log(filterOrders);
}, [filterOrders]);
I am working on a grocery list project. With this project, when the user enters an item, I want to give the ability to edit said item. I am storing everything inside an array of objects in my state. The structure of my objects is:
{
product: 'toast',
category: 'bakery',
quantity: 3,
type: 'each
},
{
product: 'apple',
category: 'produce',
quantity: 2,
type: 'each'
},
{
product: 'hamburger',
category: 'meat',
quantity: 1,
type: 'Lb'
}
What I want to be able to do is have the user select one of those objects inside a card type function, then update it. Currently, I can add items to the list, but I can not update them.
I have tried setList(list[i].txt=v) and setList(list=>list[i].product=v) plus other variations trying to target the specific object. Any ideas would be greatly appreciated.
The following is my main app.js code. NOTE: const Change() is where I am trying to update the object. The variables that I am passing in come from my item.js code
import React ,{useState,useEffect} from 'react';
import List from './components/list';
import Header from './components/header';
function App() {
const [list, setList] = useState([]);
const Add = (p, c, q, t, crt) => {
console.log({product: p, category: c, quantity: q, type: t, cart: crt})
setList(list=>[...list,{product:p, category:c, quantity:q, type:t, cart: crt}])
}
const Change = (i, txt, v) => {
//setList(list[i].txt=v)
console.log('id: ' + i + ' topic: ' + txt + ' value: ' +v)
setList(list=>list[i].product=v)
}
const Delete = () => {
}
return (
<div>
{console.log(list)}
<h1>Grocery List App</h1>
<Header add={Add}/>
<List set={setList} lst={list} chg={Change} del={Delete} />
</div>
);
}
export default App;
This next code is my list.js file. I am iterating over my list state and creating the individual 'cards' for each item.
import React from 'react';
import Card from './item';
const List = (props) => {
const productChange = (txt, v) => {
console.log(props.lst[v].product)
}
const quantityChange = () => {
}
const cartChange = () => {
}
return(
<div>
<p>To Find:</p>
<ul>
{ props.lst.map((item, index) =>
item.cart === false ?
<Card
key={item.index}
index={index}
value={index}
cart={item.cart}
item={item.product}
units={item.quantity}
unitType={item.type}
cartChange={cartChange}
itemChange={productChange}
quantityChange={quantityChange}
change={props.chg}
delete={props.del}/>
: null)
}
</ul>
<p>Found</p>
<ul>
{ props.lst.map((item, index) =>
item.cart === true ?
<Card
key={item.index}
index={index}
value={index}
cart={item.cart}
item={item.product}
units={item.quantity}
unitType={item.unit}
cartChange={cartChange}
itemChange={productChange}
quantityChange={quantityChange}
change={props.chg}
delete={props.del}/>
: null)
}
</ul>
</div>
)
}
export default List;
This is the code for item.js. This is the final spot where I display the information from the list. NOTE: change() from the first file is getting called here when I change the text of an input.
import React from 'react';
const Card=(props)=>{
return (
<li key={props.value}>
<div>
<input
type="checkbox"
checked={props.cart}
onChange={(e)=> {props.cartChange(props.value)}} />
</div>
<div>
<input
id={'product '+ props.value}
className='update'
type='text'
value={props.item}
onChange={(e) =>
props.change(props.value,'product', e.target.value)
}
/>
<br/>
<input
id='quantityValue'
className='update'
type='number'
value={props.units}
// onChange={(e)=>
props.quantityChange(e.target.value, props.value)}
/>
<span id='quantityType' className='update'>{props.unitType}
</span>
</div>
<div>
<button
id='save-button'
type='button'
onClick={(e) => { props.change(
props.item,
props.units,
props.unitType,
props.value)
}
}>✓ save</button>
<button
id='delete-button'
type='button'
onClick={(e) => {props.delete(props.value)}}>✗ delete</button>
</div>
</li>
)
}
export default Card;
you want to call setList with a list where you just amend that one object.
You can use list.map() for this. Ideally you want to add ID field to your objects.
But even without ID you can use index:
setList(list.map((product, index)=>{index == i ? v : product}))
or more verbose:
const Change = (i, txt, v) =>{
const newList = list.map((product, index) => {
return index == i ? v : product
});
setList(newList);
}
You want to implement change function. Is this right?
Try this one.
const Change = (i,txt,v) =>{
setList(list.map((e, ei) => {
if (ei === i) {
e.product = v;
return e;
}
return e;
}));
}