I am trying to extract the length of an array while mapping it.
Here is what happens:
First I have an array of objects. Each object has a key of posts where I store the posts for that object. My code takes all the posts from all the objects and maps them to a new array so that I can show all the posts from all the objects to the user on the front end.
I'd like to show only 10 posts at a time. So I put a .slice(0, page * 10) - the variable page is controlled by a button at the bottom of the page. If the user hits the button, the page then increases the number of posts on the screen.
This all works great. BUT - I'd like to be able to count the total number of posts and only show the button when there are more posts available. Is there a way to extract the number of posts while still allowing it to map the results from this function below?
{
bandTypes === "all"
? allBands
.filter(band => {
if (showType !== 'Show Type') {
return band.showTypes.includes(showType)
} else {
return band
}
})
.reduce(
(allPosts, band) =>
allPosts.concat(
(band.youtube.length > 0 &&
band.bandBio !== "n/a" &&
band.bandGenre !== "n/a")
? band.posts.map((post) => ({ post, band }))
: []
),
[]
)
.sort((a, b) => new Date(b.post.date) - new Date(a.post.date))
.slice(0, page * 10)
.map(({ post, band }) => <div key={uuidv4()}>{convertPost(post, band)}</div>)
: null
}
It would be great if I could just put an anonymous function in there somewhere that sets the state to the length of the array.
I think that trying to accomplish this without any temporary variables is not going to be very efficient and probably would be quite ugly.
I think you should first create the array of all posts and then simply use it's length inside the return of your component.
Here's an example of how I would do it:
const MyComponent = () => {
let allPosts = []
if (bandTypes === "all") {
allPosts = allBands
.filter((band) => {
if (showType !== "Show Type") {
return band.showTypes.includes(showType)
} else {
return band
}
})
.reduce(
(allPosts, band) =>
allPosts.concat(
band.youtube.length > 0 &&
band.bandBio !== "n/a" &&
band.bandGenre !== "n/a"
? band.posts.map((post) => ({
post,
band,
}))
: []
),
[]
)
.sort((a, b) => new Date(b.post.date) - new Date(a.post.date))
}
return (
<div>
{allPosts.slice(0, page * 10).map(({ post, band }) => (
<div key={uuidv4()}>{convertPost(post, band)}</div>
))}
{allPosts.length < page * 10 && <button>Show more posts</button>}
</div>
)
}
(BTW using something like uuidv4 for the key isn't ideal, because React is less efficient with rendering then. It's better base the key on something that is unique to each post and doesn't change for each render, for example an id from the database or something of this sort)
Related
I am trying to prevent resultComment being saved into localStorage, how can I achieve that? Currently when I delete localStorage data from browser and calculate result, result gets pushed to my resultHistory array and only the result and temperatureUnit gets saved to localStorage which I want, but on the second calculation now also the resultComment gets saved. How can I prevent this or what can be done differently? Here is StackBlitz: https://stackblitz.com/edit/angular-ivy-mdhnnt?file=src%2Fapp%2Fapp.component.html,src%2Fapp%2Fapp.module.ts,src%2Fapp%2Fapp.component.ts,src%2Fapp%2Fapp.component.css
Steps to recreate problem:
Clear localStorage data and refresh page,
type number in both inputs and click on button -> result and unit gets saved.
Now type different numbers in inputs and click on button -> and now the resultComment gets saved as well). Ignore weird calculation results. Its missing parts which I didn't add to StackBlitz.
component ts
class HistoryResult {
constructor(
public result: number,
public temperatureUnit: string,
public resultComment?: string
) {}
}
resultHistory: HistoryResult[] = [];
//in ngOnInit iam initializing resultHistory so it stays on page when i refresh and adding comment to each result
ngOnInit() {
...
..code..
...
this.resultHistory = JSON.parse(localStorage.getItem('result')) || [];
this.resultHistory.map((item) => {
item.resultComment = this.heatIndexService.determineComment(
item.result,
item.temperatureUnit
);
});
}
onCalculateButtonClick(): void {
this.result = null;
this.resultTemperatureUnit = this.temperatureUnit.value;
if (this.heatIndexForm.invalid) {
this.heatIndexForm.controls['temperature'].markAsDirty();
this.heatIndexForm.controls['humidity'].markAsDirty();
return;
}
this.result = this.heatIndexService.calculateHeatIndex(
this.temperatureValue,
this.humidityValue,
this.resultTemperatureUnit.code
);
this.heatIndexService.saveResultInLocalStorage(
this.result,
this.temperatureUnit.value.code,
this.resultHistory
);
this.resultHistory.map((item) => {
item.resultComment = this.heatIndexService.determineComment(
item.result,
item.temperatureUnit
);
});
}
service functions
saveResultInLocalStorage(
result: number,
unit: string,
historyResults: HistoryResult[]
): void {
// Check if result is the same as last one
if (
historyResults.length === 0 ||
historyResults.slice(-1)[0].result !== result
) {
historyResults.push(new HistoryResult(result, unit));
// Remove oldest result if more than 3 results
if (historyResults.length > 3) {
historyResults.shift();
}
localStorage.setItem('result', JSON.stringify(historyResults));
}
}
determineComment(temperature: number, units: string): string {
if (units === 'C') {
temperature = (temperature * 9) / 5 + 32;
}
if (temperature >= 75 && temperature <= 90) {
return 'Caution: fatigue is possible with prolonged exposure and activity. Continuing activity could result in heat cramps.';
} else if (temperature > 90 && temperature <= 105) {
return 'Extreme caution: heat cramps and heat exhaustion are possible. Continuing activity could result in heat stroke.';
} else if (temperature > 105 && temperature <= 130) {
return 'Danger: heat cramps and heat exhaustion are likely; heat stroke is probable with continued activity.';
} else {
return 'Extreme danger: heat stroke is imminent.';
}
}
and HTML where the previous calculations are shown
<p-table [value]="resultHistory" [tableStyle]="{'min-width': '50rem'}">
<ng-template pTemplate="header">
<tr>
<th>Heat Index</th>
<th class="heat-index-effect">Effect on the body</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-history>
<tr>
<td>{{history.result | number : '1.2-2'}}°{{history.temperatureUnit}}</td>
<td>{{history.resultComment}}</td>
</tr>
</ng-template>
</p-table>
Opening question response
You are mutating the array when calling this.historyResults.map(...) and then saving that value in saveResultInLocalStorage when calling localStorage.setItem(historyResults).
Change your value to be saved as a new array with only the properties you want then call localStorage.setItem(newValue).
To pick out only the properties you want and save them you could do the following:
const resultsToSave: Pick<HistoryResult, 'result' | 'temperatureUnit'>[] = historyResults.map(h => ({ result: h.result, temperatureUnit: h.temperatureUnit }));
localStorage.setItem('result', JSON.stringify(resultsToSave));
An alternative to using Pick above to get the reduced type could be to use Omit which may be shorter depending on how many fields you specify, e.g.:
const resultsToSave: Omit<HistoryResult, 'resultComment'>[] = historyResults.map(h => ({ result: h.result, temperatureUnit: h.temperatureUnit }));
First comment response
When you .map() over the history results it looks like you aren't reassigning the value correctly. Try returning a new object when iterating over the array and saving it back to itself.
From this:
this.resultHistory.map((item) => {
item.resultComment = this.determineComment(
item.result,
item.temperatureUnit
);
});
To this:
this.resultHistory = this.resultHistory.map((item) => ({
...item,
resultComment: this.determineComment(
item.result,
item.temperatureUnit
),
}));
I have a list of Food Products assigned to multiple categories and a specific vendor. example:
I am pre-filtering the products based on logged in vendor.
after that I want to filter the products based on categories clicked.
I am able to achieve this someway, but it is not a nice way to filter I believe.
for prefiltering I am using this code in constructor:
this.foodItemCollection = this.afs.collection<FoodItem>('foodItems');
this.foodItems = this.foodItemCollection.valueChanges({ id: 'id'}).pipe(
map(foodItems => foodItems.filter(result => result.vendorName == this.currentUserName))
);
after that on clicking the categories like fastfood, seafood, chinese etc I am calling categoryFilter() function.
the code of this function is like this:
categoryFilter(foodCategory:any){
console.log(foodCategory);
this.foodItems = this.foodItemCollection.valueChanges({ id: 'id'}).pipe(
map(foodItems => foodItems.filter(result =>
(((result.categories[0]==foodCategory.categoryName) || (result.categories[1]==foodCategory.categoryName))
&& (result.vendorName==this.currentUserName))
))
)
return this.foodItems;
}
there are multiple problems with this approach.
1.This is making a duplicate request to fetch the products which are already fetched.
2.If product contains more than 2 categories, it will fail.
Create a single function that is called on ngOnInit and during valueChanges like so
justFilter() {
// assuming your using reactive forms.
const { foodCategory, itemName } = this.formGroup.value;
this.foodItems = this.foodItemCollection.valueChanges({ id: 'id' }).pipe(
map((foodItems) =>
foodItems.filter(
(result) =>
result.vendorName == this.currentUserName &&
//filter using multiple values here!
(
(foodCategory.categoryName
? result.categories.includes(foodCategory.categoryName)
: true) &&
(itemName ? itemName === result.itemName : true)
)
// && and so on, you can write as many filters as you want!
)
)
);
}
export default function ShoppingCart() {
const classes = useStyle();
const {
productsList, filteredProductsList, setFilteredProductsList, setProductsList,
} = useContext(productsContext);
const [awaitingPaymentList, setAwaitingPaymentList] = useState([]);
const [addedToCartList, setAddedToCartList] = useState([]);
const addToCartHandler = useCallback((itemId) => {
const awaitingPaymentListIds = awaitingPaymentList.map((item) => item.id);
const isInAwaitingPaymentList = awaitingPaymentListIds.includes(itemId);
isInAwaitingPaymentList ? setAddedToCartList([...addedToCartList, addedToCartList.push(awaitingPaymentList[awaitingPaymentList.findIndex((item) => item.id === itemId)])]) : setAddedToCartList([...addedToCartList]);
isInAwaitingPaymentList
? setAwaitingPaymentList(awaitingPaymentList.splice(awaitingPaymentList.findIndex((item) => item.id === itemId), 1))
: setAwaitingPaymentList([...awaitingPaymentList ])
setProductsList(awaitingPaymentList);
}, [addedToCartList, awaitingPaymentList, setProductsList]);
useEffect(() => {
setFilteredProductsList(
productsList.filter((product) => product.status === 'AWAITING_PAYMENT'),
);
}, [productsList, setFilteredProductsList, setFilteredProductsList.length]);
useEffect(() => {
setAwaitingPaymentList(filteredProductsList);
}, [filteredProductsList]);
I manage to delete the item from awaitingPaymentList and to add it into addedToCartList but looks like I am doing something wrong because it is adding the object, but the previous ones are replaced with numbers :). On the first click, the array is with one object inside with all data, but after each followed click is something like this => [1,2,3, {}].
When I console log addedToCartList outside addToCartHandler function it is showing an array: [1] :)))
Since there is some code I hope I am not going to receive a lot of negative comments like last time. And if it's possible, to give me a clue how to make it for all items to be transferred at once, because there will be a button to add all. Thank you for your time.
I think this line of code is causing issue:
isInAwaitingPaymentList
? setAddedToCartList([
...addedToCartList,
addedToCartList.push(
awaitingPaymentList[
awaitingPaymentList.findIndex((item) => item.id === itemId)
]
)
])
: setAddedToCartList([...addedToCartList]);
array.prototype.push returns the new length of the array that you are pushing into, this is likely where the incrementing element values are coming from. The push is also a state mutation.
It is not really clear what you want this code to do, but I think the push is unnecessary. Perhaps you meant to just append the last element into the new array you are building.
isInAwaitingPaymentList
? setAddedToCartList([
...addedToCartList, // <-- copy state
awaitingPaymentList[ // <-- add new element at end
awaitingPaymentList.findIndex((item) => item.id === itemId)
]
])
: setAddedToCartList([...addedToCartList]);
Suggestion
If you simply want to move an element from one array to another then find it in the first, then filter it from the first, and copy to the second if it was found & filtered.
const itemToMove = awaitingPaymentList.find(item => item.id === itemId);
setAwaitingPaymentList(list => list.filter(item => item.id !== itemId));
itemToMove && setAddedToCartList(list => [...list, { ...itemToMove }])
I am using Dexie.JS to work with IndexedDB.
Currently, have a stupid query written as:
return db.events.each((element) => {
let d = element.cause.data;
if (d.hasOwnProperty('deleted') && (false == d.deleted) &&
d.hasOwnProperty('abbreviation') &&
d.hasOwnProperty('contents') && (d.abbreviation == key)) {
snippet = d.contents;
}
}).then(() => {
return snippet;
});
It is working correctly, but slow as molasses on a large database. Should I run each on a collection made from db.events with applied where? Would that improve performance?
Thank you
Yes if assuming your "key" variable is of an indexable type: string, number, Date, TypedArray or Array, you can optimize the query like this:
First, make sure to add the index "cause.data.abbreviation" on db.events:
db.version(2).stores({
events: 'yourPrimaryKey, cause.data.abbreviation'
});
Then, rewrite the query like this:
return db.events
// Let indexedDB sort out all items matching given key:
.where('cause.data.abbreviation').equals(key)
// Filter the rest manually using Collection.filter():
.filter(element => {
let d = element.cause.data;
return (d.hasOwnProperty('deleted') && (false == d.deleted) &&
d.hasOwnProperty('contents'));
})
// Execute the query and only return the first match:
.first();
Currently I'm getting a
continue must be inside a loop
which I recognize as a syntax error on my part because it should be fixed.
Will fixing this to retain this logic in an if statement work with the mapping?
sales = data.map(function(d) {
if (isNaN(+d.BookingID) == false && isNaN(+d["Total Paid"]) == false) {
return [+d.BookingID, +d["Total Paid"]];
} else {
continue;
}
});
map is meant to be 1:1.
If you also want filtering, you should filter and then map
sales = (
data
.filter(d => (!isNaN(+d.BookingID)&& !isNaN(+d["Total Paid"]))
.map(d => [+d.BookingID, +d["Total Paid"]];
});
As others have mentioned, you cannot "continue" from within a map callback to skip elements. You need to use filter. To avoid referencing the fields twice, once in the filter, and once in the map, I'd filter afterwards:
sales = data
.map(d => [+d["bookingId"], +d["Total Paid"]])
.filter(([id, total]) => !isNaN(id) && !isNaN(total));
or, to make it easier in case you later want to include additional values in the array:
sales = data
.map(d => [+d["bookingId"], +d["Total Paid"]])
.filter(results => results.every(not(isNaN)));
where
function not(fn) { return x => !fn(x); }
or
function allNotNaN(a) { return a.every(not(isNaN)); }
and the, using parameter destructuring:
sales = data
.map(({bookingId, "Total Paid": total)) => [bookingId, total])
.filter(allNotNaN);