Why forEach if returns []? - reactjs

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

Related

Updating list from child component react hooks

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

React button to show an element

I'm pulling countries from the Restcountries API and if the current state of the array has more than one or less than or equal to ten countries, I want to list the country names along with a 'show' button next to each one. The show button should display what's in the return (render) of my Country function. In the App function, I wrote a handler for the button named handleViewButton. I'm confused on how to filter the element in the Countries function in the else conditional statement in order to display the Country. I tried passing handleViewButton to the Button function, but I get an error 'Uncaught TypeError: newSearch.toLowerCase is not a function'. I really just want to fire the Country function to display the country button that was pressed.
App.js
import React, { useState, useEffect } from 'react';
import './App.css';
import axios from 'axios';
const Country = ({country}) => {
return (
<>
<h2>{country.name}</h2>
<p>capital {country.capital}</p>
<p>population {country.population}</p>
<br/>
<h3>languages</h3>
{country.languages.map(language => <li key={language.name}>{language.name}</li>)}
<br/>
<img src={country.flag} alt="country flag" style={{ width: '250px'}}/>
</>
);
}
const Countries = ({countries, handleViewButton}) => {
const countriesLen = countries.length;
console.log(countriesLen)
if (countriesLen === 0) {
return (
<p>Please try another search...</p>
);
} else if (countriesLen === 1) {
return (
<ul>
{countries.map((country, i) => <Country key={i} countriesLen={countriesLen} country={country}/>)}
</ul>
);
} else if (countriesLen > 10) {
return (
<div>
<p>Too many matches, specify another filter</p>
</div>
);
} else {
return (
<ul>
{countries.map((country, i) => <li key={i}>{country.name}<Button handleViewButton={handleViewButton}/></li>)}
</ul>
)
};
};
const Button = ({handleViewButton}) => {
return (
<button onClick={handleViewButton}>Show</button>
);
};
const Input = ({newSearch, handleSearch}) => {
return (
<div>
find countries <input value={newSearch} onChange={handleSearch}/>
</div>
);
};
function App() {
const [countries, setCountries] = useState([]);
const [newSearch, setNewSearch] = useState('');
const handleSearch = (event) => {
const search = event.target.value;
setNewSearch(search);
};
const handleViewButton = (event) => {
const search = event.target.value;
setNewSearch(countries.filter(country => country === search));
};
const showCountrySearch = newSearch
? countries.filter(country => country.name.toLowerCase().includes(newSearch.toLowerCase()))
: countries;
useEffect(() => {
axios
.get('https://restcountries.eu/rest/v2/all')
.then(res => {
setCountries(res.data);
console.log('Countries array loaded');
})
.catch(error => {
console.log('Error: ', error);
})
}, []);
return (
<div>
<Input newSearch={newSearch} handleSearch={handleSearch}/>
<Countries countries={showCountrySearch} handleViewButton={handleViewButton}/>
</div>
);
};
export default App;
you can use a displayCountry to handle the country that should be displayed. Most often you would use an id, but I'm using here country.name since it should be unique.
Then you would use matchedCountry to find against your list of countries.
After that, a onHandleSelectCountry to select a given country. if it's already selected then you could set to null to unselect.
Finally, you would render conditionally your matchedCountry:
const Countries = ({countries}) => {
const [displayCountry, setDisplayCountry] = useState(null);
const countriesLen = countries.length;
const matchedCountry = countries.find(({ name }) => name === displayCountry);
const onHandleSelectCountry = (country) => {
setDisplayCountry(selected => {
return selected !== country.name ? country.name : null
})
}
if (countriesLen === 0) {
return (
<p>Please try another search...</p>
);
} else if (countriesLen === 1) {
return (
<ul>
{countries.map((country, i) => <Country key={i} countriesLen={countriesLen} country={country}/>)}
</ul>
);
} else if (countriesLen > 10) {
return (
<div>
<p>Too many matches, specify another filter</p>
</div>
);
} else {
return (
<>
<ul>
{countries.map((country, i) => <li key={i}>{country.name}<Button handleViewButton={() => onHandleSelectCountry(country)}/></li>)}
</ul>
{ matchedCountry && <Country countriesLen={countriesLen} country={matchedCountry}/> }
</>
)
};
};
I can only help to point out some guidelines.
First: The button does not have value attribute. Hence what you will get from event.target.value is always blank.
const Button = ({handleViewButton}) => {
return (
<button onClick={handleViewButton}>Show</button>
);};
First->Suggestion: Add value to the button, of course you need to pass the value in.
const Button = ({handleViewButton, value}) => {
return (
<button onClick={handleViewButton} value={value}>Show</button>
);};
Second: To your problem 'Uncaught TypeError: newSearch.toLowerCase is not a function'. Filter always returns an array, not a single value. if you do with console or some sandbox [1,2,3].filter(x=>x===2) you will get [2] not 2.
const handleViewButton = (event) => {
const search = event.target.value;
setNewSearch(countries.filter(country => country === search));
};
Second->Suggestion: To change it to get the first element in array, since country(logically) is unique.
const result = countries.filter(country => country === search)
setNewSearch(result.length>0?result[0]:"");
A better approach for array is find, which always return first result and as a value. E.g. [1,2,2,3].find(x=>x===2) you will get 2 not [2,2] or [2].
countries.find(country => country === search)

Building chainable filter components in React?

What's the most "React"y way to build reusable, chainable filter components?
Let's say I have an input array:
[
{name: 'Generic t-shirt', size: 'xxl', available: 35},
{name: 'Generic t-shirt', size: 'md', available: 2},
{name: 'Generic t-shirt', size: 'sm', available: 5},
{name: 'Really awesome shirt', size: 'md', available: 0}
]
And a keyword search for the name, a dropdown for size, and a "sold out" boolean checkbox for availability.
Right now, I have the filtering code inside the rendering loop:
const [productsFilteredByFullText, setProductsFilteredByFullText] = useState;
const [productsFilteredBySize, setProductsFilteredBySize] = useState;
const [productsFilteredByAvailability, setProductsFilteredByAvailability] = useState;
const searchResults = useMemo(() => {
let filteredProducts = eventsFromAPI; // array of products as input
filteredProducts = filteredEvents.filter(product => fullTextFilter(product));
filteredProducts = filteredEvents.filter(product => sizeFilter(product));
filteredProducts = filteredEvents.filter(product => availabilityFilter(product));
return filteredProducts;
}, [productsFilteredByFullText, productsFilteredBySize, productsFilteredByAvailability]);
And the UI inside JSX:
<div>
// Fulltext search
<input
type="text"
placeholder="Keyword search"
value={searchTerm}
onChange={fullTextHandler}
/>
// Size dropdown
<Dropdown
options={allAvailableSizes}
onChange={sizeHandler}
/>
// Sold out checkbox
<input
name="soldout"
type="checkbox"
checked={productsFilteredByAvailability}
onChange={availabilityHandler}
/>
<h1>Results</h1>
{filteredProducts.map(item => (
<Product item={item} />
))}
</div>
This works, but is not very reusable at all. Let's say I have another product category, scarves, that are all one size, but I still want to be able to re-use the filter component and logic for name and availability.
Is there a way to modularize/componentize BOTH the filter logic AND the presentation JSX into separate filter components, and be able to chain them together arbitrarily? Something like this pseuocode:
<TShirtList>
<NameFilter/>
<SizeFilter/>
<AvailabilityFilter/>
</TShirtList>
<ScarfList>
<NameFilter/>
<AvailabilityFilter/>
</ScarfList>
<ServicesList>
<NameFilter/>
</ServicesList>
So that each filter is its own component, able to be inserted into any array of products anywhere? Like how can a React component also provide its own logic/functions that other components can use (product array in, filtered product array + JSX UI out, but chainable).
Is that even the right way to think about this problem...? I'm confused about how to best build this architecturally.
Answer to this question
What's the most "React"y way to build reusable, chainable filter
components?
I suggest separate display & data logic by using hooks. Move all data related logic into a hook, and import that hook to a react component.
// useProducts.js
const useProducts = () => {
const [products, setProducts = useState([]);
const [filters, setFilters] = useState({}); // eg. filters = { size: 'large', availability: 'instock' }
// Get all products on initial load
useEffect(() => {
fetch(....).then(response => setProducts(response.data));
}, []);
// function to set filter.
const setFilter = (name, value) => {
setFilters({ ...filters, [name]: value });
}
// Loop filters and filter product
const filteredProducts = [...products];
for (const [key, value] of Object.entries(filters)) {
// Get list of products from somewhere.
filteredProducts = filterProducts.filter(p => p[key] === value);
}
return [filteredProducts, { setFilter }];
}
// Product List
import React from 'react';
import useProducts from './useProducts';
const ProductList = (props) => {
const [products, { setFilter }] = useProducts();
return (
<div>
<div className="toolbar">
<NameFilter onChange={value => setFilter('name', value)} />
<SizeFilter onChange={value => setFilter('size', value)} />
<AvailabilityFilter onChange={value => setFilter('availability', value)} />
</div>
<div>
{products.map(p => <ProductItem {...p} />}
</div>
</div>
);
}
Additionally, you can even modularize it more by import { setFilter } in the filter component itself. And you can remove the 'onChange' from the ProductList.
const NameFilter = () => {
const [, { setFilter }] = useProducts();
return (
<input
type="text"
onChange={e => setFilter('name', e.target.value)}
/>
);
}
// And now, we can remove onChange from each filter in Product List component
<div>
<div className="toolbar">
<NameFilter />
<SizeFilter />
<AvailabilityFilter />
</div>
<div>
{products.map(p => <ProductItem {...p} />}
</div>
</div>
*Note: Above code is just psuedo, it just shows the idea.
Any issues with conditionally filtering?
const searchResults =(text, size) => {
let filteredProducts = eventsFromAPI; // array of products as input
filteredProducts = text ? filteredEvents.filter(product => fullTextFilter(text)) : filteredProducts;
filteredProducts = size ? filteredEvents.filter(product => sizeFilter(product)) : filteredProducts
filteredProducts = filteredEvents.filter(product => availabilityFilter(product));
return filteredProducts;
};
size can be boolean.
can also be ... (psuedo code)
const Search =({filter = []}) => {
let filteredProducts = eventsFromAPI; // array of products as input
filteredProducts = filter.indexOf('something') > -1 ? filteredEvents.filter(product => fullTextFilter(text)) : filteredProducts;
filteredProducts = filter.indexOf('size') > -1 ? filteredEvents.filter(product => sizeFilter(product)) : filteredProducts
filteredProducts = filteredEvents.filter(product => availabilityFilter(product));
return filteredProducts;
};
then u can call search({ filter: ['size', 'availability']}) or if you made it in to an component then <Search filter={['size', 'availability']} products={products} />
or even
const Search = ({ size = null, text = '', availability = true}) => {
... conditionally filtering
return //whatever product u render
}

How to set the first value in a select dropdown to as a default react

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.

Updating values with React Hooks

Am I updating 'fruits' properly? I would think fruits.push('something') would trigger the useEffect hook? or using setFruits would cause a re-render?
const Fruits = () => {
const [fruitInput, setFruitInput] = useState("")
const [fruits, setFruits] = useState(['Apple', 'Orange'])
const addFruit = fruit => {
console.log('addFruit', fruit)
fruits.push(fruit)
setFruits(fruits)
}
useEffect(() => {
console.log('fruits was updated', fruits)
}, [fruits])
return (
<>
<ul>{fruits.map((fruit, index) => <li key={index}>{fruit}</li>)}</ul>
<input type="text" onChange={ e => setFruitInput(e.target.value) } value={fruitInput} />
<button onClick={() => addFruit(fruitInput)}>Add Fruit</button>
</>
)
}
You're mutating the fruits array in place, so React doesn't see its changes and does not trigger a render. Add the item to newly created array instead:
const addFruit = fruit => {
console.log('addFruit', fruit)
setFruits([...fruits, fruit])
}
You have to use setFruits properly. Refactor
console.log('addFruit', fruit)
fruits.push(fruit)
setFruits(fruits)
}
to
const addFruit = fruit => {
console.log('addFruit', fruit)
setFruits(fruits=>([...fruits, fruit]))
}
Below implementation should resolve your issue:-
const Fruits = () => {
const [fruitInput, setFruitInput] = useState("");
const [fruits, setFruits] = useState(["Apple", "Orange"]);
const addFruit = () => {
const newFruits = [...fruits];
newFruits.push(fruitInput);
setFruits(newFruits);
console.log(newFruits);
};
useEffect(() => {
console.log("fruits was updated", fruits);
}, [fruits]);
return (
<>
<ul>
{fruits.map((fruit, index) => (
<li key={index}>{fruit}</li>
))}
</ul>
<input
type="text"
onChange={e => setFruitInput(e.target.value)}
value={fruitInput}
/>
<button onClick={addFruit}>Add Fruit</button>
</>
);
};
Explanation:
When you use [fruits] in useEffect then it does deep comparison, so you have to ensure that the new value in fruits is different than what was there earlier. Simply pushing value in array won't trigger the update.

Resources