Given the following script:
type Data = {
id: string
value: number
}
// This is a pure function: given val, res will always be the same
const veryExpensiveCalc = (val: number) => {
const res = // ... 5 seconds of sync computation ...
return res
}
const array$ = new ReplaySubject<Array<Data>>(1)
const allComputedValues$ = array$.pipe(
map(arr =>
arr.map(item =>
// I want this veryExpensiveCalc to be performed only when
// item.value changes for this item.id, or the item was not
// there before
veryExpensiveCalc(item.value)
)
)
)
allComputedValues$
.pipe(
tap(newStruct => {
console.log('newStruct', newStruct)
})
)
.subscribe()
I'd like to optimize how allComputedValues$ is calculated. In particular let's say that an item is added or removed to array$, or the order of the elements change: then veryExpensiveCalc is executed again on every item of the array, even though that's not needed at all. I just need allComputedValues$ from the previous computation with the result of veryExpensiveCalc applied only to the newly added element (in case of addition).
What's the best way to write this in a functional reactive fashion? It should work also if there are multiple elements edited/added/removed at the same time (so that veryExpensiveCalc is executed only on the elements that have a different value given the same id).
I think you can use the scan() operator here:
const allComputedValues$ = array$.pipe(
scan(
(acc, crtArr) => {
// with these we 'automatically' remove the current items(from `acc`) whose ids are not in the current array
const newIds = {};
const newValues = {};
crtArr.forEach(crt => {
const { id: currentId, value: crtValue } = crt;
// if new or edited
if (!acc.ids[currentId] || acc.values[currentId] !== crtValue) {
newIds[currentId] = true;
newValues[currentId] = veryExpensiveCalc(crtValue);
}
});
return { ids: newIds, values: newValues };
},
{ ids: {}, values: {} },
),
)
Related
I want to create a smarter way of coding of the following example. Important is that each loop (for activeFilters) needs to be fully done, before we want to return the filtersTest.
const createFilters = async () => {
const filtersTest = [] as any
// Only create active filters by checking count.
const activeFilters = getComponentFilter.value.filter(function(item) {
if (item.items) {
return item.items.some((obj) => obj.count)
}
});
// Loop through the active filters and push arrays into the object.
for(let i = 0 ; i < activeFilters.length; i++) {
const options = await createFilterOptions(activeFilters[i].id, activeFilters[i].items);
const array = {
defaultValue: null,
id: activeFilters[i].id,
value: 'nee',
label: activeFilters[i].label,
options: options,
}
filtersTest.push(array)
}
return filtersTest;
}
First of all, it should be clear that createFilters is not going to return the array, but a promise that will eventually resolve to that array.
With that in mind, you can reduce your code a bit, using Promise.all, the ?. operator, destructuring parameters, and shorthand property names in object literals:
const createFilters = () => Promise.all(
getComponentFilter.value.filter(({items}) =>
items?.some((obj) => obj.count)
).map(({id, label, items}) =>
createFilterOptions(id, items).then(options => ({
defaultValue: null,
id,
value: 'nee',
label,
options
}))
)
);
I am testing some code to try and understand the race condition regarding the use of setState().
my code can be found here
my code below:
import React from "react";
export default class App extends React.Component {
state = {
id: "",
ids: [{ id: 7 }, { id: 14 }]
};
// here is where I create the id numbers
uniqueIdCreatorHandler = incrementAmount => {
let ids = [...this.state.ids];
let highestId = 0;
if (ids.length > 0) {
highestId = ids
.map(value => {
return value.id;
})
.reduce((a, b) => {
return Math.max(a, b);
});
}
let newId = highestId + incrementAmount;
ids.push({ id: newId });
this.setState({ ids: ids });
};
idDeleterHanlder = currentIndex => {
let ids = this.state.ids;
ids.splice(currentIndex, 1);
this.setState({ ids: ids });
};
//below is when I test performing the function twice, in order to figure if the result would be a race condition
double = (firstIncrementAmount, secondIncrementAmount) => {
this.uniqueIdCreatorHandler(firstIncrementAmount);
this.uniqueIdCreatorHandler(secondIncrementAmount);
};
render() {
let ids = this.state.ids.map((id, index) => {
return (
<p onClick={() => this.idDeleterHanlder(index)} key={id.id}>
id:{id.id}
</p>
);
});
return (
<div className="App">
<button onClick={() => this.uniqueIdCreatorHandler(1)}>
Push new id
</button>
<button onClick={() => this.double(1, 2)}>Add some Ids</button>
<p>all ids below:</p>
{ids}
</div>
);
}
}
when invoking the double function on the second button only the secondIncrementAmount works. You can test it by changing the argument values on the call made on the onClick method.
I think that I should somehow use prevState on this.setState in order to fix this.
How could I avoid this issue here? This matter started at CodeReview but I did not realize how could I fix this.
There is also a recommendation to spread the mapped ids into Math.max and I could not figure out how and Why to do it. Isn't the creation of the new array by mapping the spreaded key values safe enough?
.splice and .push mutate the array. Thus the current state then does not match the currently rendered version anymore. Instead, use .slice (or .filter) and [...old, new] for immutable stateupdates:
deleteId = index => {
this.setState(({ ids }) => ({ ids: ids.filter((id, i) => i !== index) }));
};
uniqueIdCreatorHandler = increment => {
const highest = Math.max(0, ...this.state.ids.map(it => it.id ));
this.setState(({ ids }) => ({ ids: [...ids, { id: highest + increment }] }));
};
setState can be asynchronous, batching up multiple changes and then applying them all at once. So when you spread the state you might be spreading an old version of the state and throwing out a change that should have happened.
The function version of setState avoids this. React guarantees that you will be passed in the most recent state, even if there's some other state update that you didn't know about. And then you can product the new state based on that.
There is also a recommendation to spread the mapped ids into Math.max and I could not figure out how and Why to do it
That's just to simplify the code for finding the max. Math.max can be passed an abitrary number of arguments, rather than just two at a time, so you don't need to use reduce to get the maximum of an array.
uniqueIdCreatorHandler = incrementAmount => {
this.setState(prevState => {
let ids = [...prevState.ids];
let highestId = Math.max(...ids.map(value => value.id));
let newId = highestId + incrementAmount;
ids.push({ id: newId });
this.setState({ ids: ids });
});
};
This isn't the most elegant solution but you can pass a callback to setState(see https://reactjs.org/docs/react-component.html#setstate).
If you modify uniqueIdCreatorHandler like this:
uniqueIdCreatorHandler = (incrementAmount, next) => {
let ids = [...this.state.ids];
let highestId = 0;
if (ids.length > 0) {
highestId = ids
.map(value => {
return value.id;
})
.reduce((a, b) => {
return Math.max(a, b);
});
}
let newId = highestId + incrementAmount;
ids.push({ id: newId });
this.setState({ ids: ids }, next); //next will be called once the setState is finished
};
You can call it inside double like this.
double = (firstIncrementAmount, secondIncrementAmount) => {
this.uniqueIdCreatorHandler(
firstIncrementAmount,
() => this.uniqueIdCreatorHandler(secondIncrementAmount)
);
};
i need help with refactoring below block of code. I was asked to avoid using let and to use const, how can i use constant here as i need to return all the options having possible match id.
const findRecordExists = (options, possibleMatchId) => {
let item;
options.forEach(option => {
option.waivers.forEach(waiver => {
if (waiver.waiverNameId === possibleMatchId) {
item = option;
}
});
});
return item;
};
Example of options would be :
options: [{
name:Abc
waivers: [ {waiverNameId :1}, {waiverNameId:2} ]
}]
Use filter to iterate over the options array, returning whether .some of the waiverNameIds match:
const findRecordExists = (options, possibleMatchId) => {
return options.filter(
({ waivers }) => waivers.some(
({ waiverNameId }) => waiverNameId === possibleMatchId
)
);
};
Or, if you don't like destructuring:
const findRecordExists = (options, possibleMatchId) => {
return options.filter(
option => option.waivers.some(
wavier => wavier.waiverNameId => waiverNameId === possibleMatchId
)
);
};
Since the result is being immediately returned from the findRecordExists function, there isn't even any need for an intermediate item (or items) variable.
That's okay.
Using const to declare an identifier only makes the value of the identifier unchangeable if the value of the identifier is a JavaScript primitive e.g a number or a boolean.
If the value of the identifier is an object or an array (an array is a type of object in JavaScript), using const to declare it doesn't mean that the value of that object identifier cannot be changes. It only means that the identifier cannot be reassigned.
To refactor your code using const, use the code listing below
const findRecordExists = (options, possibleMatchId) => {
const optionsWithPossibleMatches = [];
options.forEach(option => {
option.waivers.forEach(waiver => {
if (waiver.waiverNameId === possibleMatchId) {
optionsWithPossibleMatches.push(option);
}
});
});
return optionsWithPossibleMatches;
};
If you want to skip intermediate steps of creating variables to store each option that matches your condition, you can use the filter method as prescribed by #CertainPerformance
You can re-factor with using find method. This will simplify and avoids the item variable.
const options = [
{
name: "Abc",
waivers: [{ waiverNameId: 1 }, { waiverNameId: 2 }]
}
];
const findRecordExists = (options, possibleMatchId) =>
options.find(option =>
option.waivers.find(waiver => waiver.waiverNameId === possibleMatchId)
);
console.log(findRecordExists(options, 2));
console.log(findRecordExists(options, 3));
I have an array of object called TourStop, which is two level nested. The types are as below.
TourStops = TourStop[]
TourStop = {
suggestions?: StoreSuggestion[]
id: Uuid
}
StoreSuggestion= {
id: Uuid
spaceSuggestions: SpaceSuggestion[]
}
SpaceSuggestion = {
id: Uuid
title: string
}
My goal is to remove a particular StoreSuggestion from a particular TourStop
I have written the following code(uses immutability-helper and hooks)
const [tourStopsArray, setTourStopsArray] = useState(tourStops)
// function to remove the store suggestion
const removeSuggestionFromTourStop = (index: number, tourStopId: Uuid) => {
// find the particular tourStop
const targetTourStop = tourStopsArray.find(arr => arr.id === tourStopId)
// Update the storeSuggestion in that tourstop
const filteredStoreSuggestion = targetTourStop?.suggestions?.filter(sugg => sugg.id !== index)
if (targetTourStop) {
// create a new TourStop with the updated storeSuggestion
const updatedTargetTourStop: TourStopType = {
...targetTourStop,
suggestions: filteredStoreSuggestion,
}
const targetIndex = tourStopsArray.findIndex(
tourStop => tourStop.id == updatedTargetTourStop.id,
)
// Find it by index and remove it
setTourStopsArray(
update(tourStopsArray, {
$splice: [[targetIndex, 1]],
}),
)
// add the new TourStop
setTourStopsArray(
update(tourStopsArray, {
$push: [updatedTargetTourStop],
}),
)
}
}
The push action works correctly. However the splice action doesn't work for some reason. What am I doing wrong?
I use angularFirestore to query on firebase and I want join data from multiple documents using the DocumentReference.
The first operator map in the pipe return an array of IOrderModelTable, the second operator, i.e, the switchMap iterate over array an for each element use the id contained in each element to query data in other table.
The problem is that in the swithMap I obtain an array of observable due to anidated map operators. How I can obtain an array of IOrderModelTable and then return an observable of this array.
The code is:
getDataTableOperatorsFromDB(): Observable<IOrderModelTable[]> {
const observable = this.tableOperatorsCollectionsRef.snapshotChanges().pipe(
map(actions => {
return actions.map(a => {
const data = a.payload.doc.data() as IOrdersModelDatabase;
const id = a.payload.doc.id;
data.ot = id;
return data;
});
}),
switchMap(data => {
const result = data.map(element => {
return this.afs.collection('Orders/').doc(element.orderNumberReference.id).valueChanges().pipe(map(order => {
return {
otNumber: element.ot,
clientName: '',
clientReference: order.clientReference,
id: element.orderNumberReference,
};
}));
});
// Result must be an IOrderModelTable[] but is a Observable<IOrderModelTable>[]
return of(result);
})
);
You can use to Array operator to transform a stream to an array, but make sure your stream will end.
The trick is to choose the right stream.
For you problem, the natural source would be the list received by your first call. In a schematic way I can put it , you get a list of ids, that you transform into a list of augmented information :
first input ...snapshopChanges():
----[A, B, C]------>
each element is transformed through ...valueChanges():
-------Call A -------------DataA-------->
-------Call B ------------------------DataB----->
-------Call C --------------------DataC----->
Then reduced using toArray() to :
----------------------------------------------[DataA, DataC, DataB]-------->
Code:
getDataTableOperatorsFromDB(): Observable<IOrderModelTable[]> { {
return this.tableOperatorsCollectionsRef.snapshotChanges()
.pipe(
map(actions => {
from(data).pipe(
map(action => {
const data = a.payload.doc.data() as IOrdersModelDatabase;
const id = a.payload.doc.id;
data.ot = id;
return data;
}),
mergeMap(element => {
return this.afs.collection('Orders/').doc(element.orderNumberReference.id).valueChanges().pipe(
map(order => {
return {
otNumber: element.ot,
clientName: '',
clientReference: order.clientReference,
id: element.orderNumberReference,
};
})
);
}),
toArray()
);
})
)
}
Important : I replaced switchMap by mergeMap, otherwise some information could be thrown away.
#madjaoue
You're right, mergeMap is the correct operator in this case because with switchMap for each event emitted the inner observable is destroyed so in the subscribe you only get the final event emitted, i.e, the last row. This observable is long lived, never complete, so also use the operator take with the length of the actions which is the array that contains the list of documents.
Thank you very much for the help. :D
getDataTableOperatorsFromDB(): Observable<IOrderModelTable[]> {
const observable = this.tableOperatorsCollectionsRef.snapshotChanges().pipe(
switchMap(actions => {
return from(actions).pipe(
mergeMap(action => {
console.log(action);
const data = action.payload.doc.data() as IOrdersModelDatabase;
const otNumber = action.payload.doc.id;
return this.afs.collection('Orders/').doc(data.orderNumberReference.id).valueChanges().pipe(map(order => {
return {
otNumber: otNumber,
clientName: '',
clientReference: order.clientReference,
id: data.orderNumberReference,
};
}));
}),
mergeMap(order => {
console.log(order);
return this.afs.collection('Clients/').doc(order.clientReference.id).valueChanges().pipe(map(client => {
return {
otNumber: order.otNumber,
clientName: client.name,
clientReference: order.clientReference,
id: order.id,
};
}));
}),
take(actions.length),
toArray(),
tap(console.log),
);
}),