How to nest Reselect selectors? - javascript

I have a selector that returns an array. The elements in the array themselves have derived data. I essentially need a recursive memoization selector that returns a derived array composed of derived elements.
my current attempt is:
export const selectEntitesWithAssetBuffers = createSelector(
[selectSceneEntities, getAssets],
(entities, loadedAssets) => {
return entities.map((entity) => {
entity.buffers = entity.assets.map((assetName) => {
return loadedAssets[assetName].arrayBuffer;
})
return entity;
})
}
)
My concerns here are anytime entities or loadedAssets change this will recompute the entire list. What I'm expecting to setup is something like a selectEntityWithBuffer that would get passed to the entities.map. Ideally, I want this to only recompute when an entity.assets array changes.

Reselect allows you to provide custom equality definitions to your selectors.
import { defaultMemoize, createSelectorCreator } from 'reselect'
const compareByAssets = (a, b) => {
return a.every((element, index) => {
return element.assets === b[index].assets
});
};
const createAssetsComparatorSelector = createSelectorCreator(
defaultMemoize,
compareByAssets
);
const selectSceneEntitiesByAssetsComparator = createAssetsComparatorSelector((state) => {
//however you normally get entities for the pre-existing selectors
});
Now you can use this new selectSceneEntitiesByAssetsComparator in place of the previous selectSceneEntities in the above code you provided and it will only re-run when the equality check in compareByAssets fails.
Feel free to further update that comparator function if a strict comparison of assets === assets doesn't suite your needs.

As a proof of concept, I'd try to provide loadedAssets object to the result function by bypassing reselect identity checks.
// Keep a private selector instance
let cachedSelector;
export const selectEntitesWithAssetBuffers = function(){
// loadedAssets should be recalculated on each call?
const loadedAssets = getAssets(arguments);
// create selector on first call
if(cachedSelector === undefined) {
cachedSelector = createSelector(
selectSceneEntities,
entities => {
return entities.map(entity => {
entity.buffers = entity.assets.map((assetName) => {
return loadedAssets[assetName].arrayBuffer;
})
return entity;
})
}
)
}
// Return selector result
return cachedSelector(arguments);
}

Getting deeper memoization than what you've got is kind of a tricky problem because Reselect doesn't really support passing arguments to selectors. If you're returning an array from your selector, and the input used to build that array has changed, it's sort of the intended behavior from Reselect that you will need to recompute. See the advice in the readme for dynamic arguments.

Related

Redux how to update nested state object immutably without useless shallow copies?

Im using NGRX store on angular project.
This is the state type:
export interface TagsMap {
[key:number]: { // lets call this key - user id.
[key: number]: number // and this key - tag id.
}
}
So for example:
{5: {1: 1, 2: 2}, 6: {3: 3}}
User 5 has tags: 1,2, and user 6 has tag 3.
I have above 200k keys in this state, and would like to make the updates as efficient as possible.
The operation is to add a tag to all the users in the state.
I tried the best practice approach like so :
const addTagToAllUsers = (state, tagIdToAdd) => {
const userIds = Object.keys(state.userTags);
return userIds.reduce((acc, contactId, index) => {
return {
...acc,
[contactId]: {
...acc[contactId],
[tagIdToAdd]: tagIdToAdd
}
};
}, state.userTags);
};
But unfortunately this makes the browser crush when there are over 200k users and around 5 tags each.
I managed to make it work with this:
const addTagToAllUsers = (state, tagIdToAdd) => {
const stateUserTagsShallowCopy = {...state.userTags};
const userIds = Object.keys(stateUserTags);
for (let i = 0; i <= userIds.length - 1; i++) {
const currUserId = userIds[i];
stateUserTagsShallowCopy[currUserId] = {
...stateUserTagsShallowCopy[currUserId],
[tagIdToAdd]: tagIdToAdd
};
}
return stateUserTagsShallowCopy;
};
And the components are updated from the store nicely without any bugs as far as I checked.
But as written here: Redux website mentions :
The key to updating nested data is that every level of nesting must be copied and updated appropriately
Therefore I wonder if my solution is bad.
Questions:
I Believe I'm still shallow coping all levels in state, am i wrong ?
Is it a bad solution? if so what bugs may it produce that I might be missing ?
Why is it required to update sub nested level state in an immutable manner, if the store selector will still fire because the parent reference indeed changed. (Since it works with shallow checks on the top level.)
What is the best efficient solution ?
Regarding question 3, here is an example of the selector code :
import {createFeatureSelector, createSelector, select} from '#ngrx/store';//ngrx version 10.0.0
//The reducer
const reducers = {
userTags: (state, action) => {
//the reducer function..
}
}
//then on app module:
StoreModule.forRoot(reducers)
//The selector :
const stateToUserTags = createSelector(
createFeatureSelector('userTags'),
(userTags) => {
//this will execute whenever userTags state is updated, as long as it passes the shallow check comparison.
//hence the question why is it required to return a new reference to every nested level object of the state...
return userTags;
}
)
//this.store is NGRX: Store<State>
const tags$: Observable<any> = this.store.pipe(select(stateToUser))
//then in component I use it something like this:
<tagsList tags=[tags$ | async]>
</tagsList>
Your solution is perfectly fine.
The rule of thumb is that you cannot mutate an object/array stored in state.
In Your example, the only thing that You are mutating is the stateUserTagsShallowCopy object (it is not stored inside state since it is a shallow copy of state.userTags).
Sidenote: It is better to use for of here since you don't need to access the index
const addTagToAllUsers = (state, tagIdToAdd) => {
const stateUserTagsShallowCopy = {...state.userTags};
const userIds = Object.keys(stateUserTags);
for (let currUserId of userIds) {
stateUserTagsShallowCopy[currUserId] = {
...stateUserTagsShallowCopy[currUserId],
[tagIdToAdd]: tagIdToAdd
};
}
return stateUserTagsShallowCopy;
};
If you decide to use immer this will look like this
import produce from "immer";
const addTagToAllUsers = (state, tagIdToAdd) => {
const updatedStateUserTags = produce(state.userTags, draft => {
for (let draftTags of Object.values(draft)) {
tags[tagIdToAdd] = tagIdToAdd
}
})
return updatedStateUserTags
});
(this comes usually with performance cost). With immer you can sacrifice performance to gain readability
ad 3.
Why is it required to update sub nested level state in an immutable manner, if the store selector will still fire because the parent reference indeed changed. (Since it works with shallow checks on the top level.)
Every store change selectors recompute to see if the dependent component should re-render.
imagine that instead of immutable update of the user tags we decided to mutate tags inside user (state.userTags is a new object reference but we mutate (reuse) old entries objects state.userTags[userId])
const addTagToAllUsers = (state, tagIdToAdd) => {
const stateUserTagsShallowCopy = {...state.userTags};
const userIds = Object.keys(stateUserTags);
for (let currUserId of userIds) {
stateUserTagsShallowCopy[currUserId][tagIdToAdd] = tagIdToAdd;
}
return stateUserTagsShallowCopy;
};
In your case, you have a selector that takes out state.userTags.
It means that every time a state update happens nrgx will compare the previous result of the selector and the current one (prevUserTags === currUserTags by reference). In our case, we change state.userTags so the component that uses this selector will be refreshed with new userTags.
But imagine other selectors that instead of all userTags will select only one user tags. In our imaginary situation, we mutate directly userTags[someUserId] so the reference remains the same each time. The negative effect here is that subscribing component will be not refreshed (will not see an update after a tag is added).

ReactJs a state is triggeing in a loop when I use a selector normal function

Main problem: I tried to use a function inside the selector to reestructur the data and join another variable, in this case my group and put together with their children as items, the problem is that the function is called every time on an infinite loop despite the state is not being altered.
I have this selector:
const groups = useSelector(state => selectProductGroups(state));
And the function is this one:
const groups = state.PlatformsReducer.groups;
const items = state.PlatformsReducer.items;
return groups.reduce((ac, g) => {
g.items = items.filter(i => i.groupId == g.productNumber);
if (ac[g.platformId]) {
ac[g.platformId].push(g);
} else {
ac[g.platformId] = [g];
}
return ac;
}, {});
};
So when I use a useEffect to detect if the groups variable has changed the useEffect is triggered in a loop despite the variable groups still empty.
Do you know why? or How to prevent this.
I now the problem is the function in the selector, but I don't know how to prevent this case.
This has to do with what the useSelector hook does internally.
useSelector runs your selector and checks if the result is the same as the previously received result (reference comparison). If the results differ then the new result is stored and a rerender is forced. If the results are the same then the old result is not replaced and no rerender is triggered.
What this does mean is that every time the store updates, even if it is an unrelated part of the state, your complex function will be run to determine whether the result has changed. In your case it is always a new reference and therefore always a change.
I think the best way to handle this is to keep your selectors as simple as possible, or use some form of more complex memoization like provided by reselect.
Below is an example of how you might be able to keep your selectors simple but still achieve an easy way to reuse your product group selection using a custom hook.
const useProductGroups = () => {
// Get groups from the store.
// As the selector does not create a new object it should only
// trigger a rerender when groups changes in the store.
const groups = useSelector(state => state.PlatformsReducer.groups);
// Get items from the store,
// As the selector does not create a new object it should only
// trigger a rerender when items changes in the store.
const items = useSelector(state => state.PlatformsReducer.items);
// Reduce the group collection as desired inside of a useMemo
// so that the reduction only occurs when either items or groups
// changes.
const productGroups = useMemo(() => {
return groups.reduce((ac, g) => {
g.items = items.filter(i => i.groupId == g.productNumber);
if (ac[g.platformId]) {
ac[g.platformId].push(g);
} else {
ac[g.platformId] = [g];
}
return ac;
}, {});
}, [groups, items] /* dependency array on items / groups */);
// return the calculated product groups
return productGroups;
}
You can then use the custom hook in your function components:
const groups = useProductGroups();

How to reduce amap Array.prototype.some() calls?

I have a service managing data fetched from a SocketIO API such as
export class DataService {
private data: SomeData[];
// ...
getData(): Observable<SomeData[]> {
// distinctUntilChanged was used to limit Observable to only emits when data changes
// but it does not seem to change much things...
return this.data.pipe(distinctUntilChanged());
}
// ...
}
and a component calling this service to do
this.banana$ = combineLatest([
someFnToRequestANetworkObject(),
DataService.getData()
]).pipe(
map(([network, data]) => network && data.some(_data=> _data.ip === network.ip))
);
The thing is that each time one of the Observable handled within combineLatest gets emitted, I get to call Array.prototype.some() function. Which I don't want to.
How could I optimize this code so that I don't call some too often ?
One thing to note about distinctUntilChanged() operator is it affects only the subscription. So the operators between the source observable and the subscription are still run. As workaround you could manually check if data has changed between emissions. Try the following
let oldData: any;
this.banana$ = combineLatest([
someFnToRequestANetworkObject(),
DataService.getData()
]).pipe(
map(([network, data]) => {
if (!oldData || oldData !== data) {
oldData = data;
return (network && data.some(_data=> _data.ip === network.ip));
}
return false; // <-- return what you wish when `data` hasn't changed
})
);
I think your hunch about distinctUntilChanged not working is correct.
By default, this operator uses an equality check to determine if two values are the same.
However, this will (in most cases) not work properly if the objects being compared aren't simple scalar values like numbers or booleans.
As it turns out, you can provide your own comparator function as the first argument to distinctUntilChanged.
This will be called on "successive" elements to determine if the new element is different from the most recent one.
The exact definition of this comparator function depends on what your SomeData class/interface looks like, along with what it means for an array of SomeData inhabitants to be the same as another one.
But, as an example, let's say that SomeData looks like this:
interface SomeData {
id: string;
name: string;
age: number;
}
and that SomeData inhabitants are the same if they have the same id.
Furthermore, let's suppose that two arrays of SomeDatas are the same if they contain exactly the same SomeData elements.
Then our comparator function might look like:
function eqSomeDataArrays(sds1: SomeData[], sds2: SomeData[]): boolean {
// Two arrays of `SomeData` inhabitants are the same if...
// ...they contain the same number of elements...
return sds1.length === sds2.length
// ...which are "element-wise", the same (i.e. have the same `id`)
&& sds1.every((sd1, i) => sd1.id === sds2[i].id)
}
To round it all out, your getData method would now look like:
getData(): Observable<SomeData[]> {
return this.data.pipe(distinctUntilChanged(eqSomeDataArrays));
}
in the pipe, after map call, you could use shareReplay(1)
this.banana$ = combineLatest([
someFnToRequestANetworkObject(),
DataService.getData()])
.pipe(
map(([network, data]) => network && data.some(_data=> _data.ip === network.ip)),
shareReplay(1)
);

Converting function with logic and ui into higher order functions

I'm trying to improve my JavaScript skills. I'm learning composability and functional patterns and I'm totally lost.
I have two functions: one mapping an array and the other called from within the previous function to generate the markup.
const names = ['peter', 'paul', 'patrice']
const namesMarkup = name => {
return `<p>${name}</p>`
}
const showNames = listOfNames => {
return listOfNames.map(el => {
return namesMarkup(el)
})
}
showNames(names)
I have been reading about HOF, which technically are functions that take a function as an argument and/or return a function.
How could I compose these functions to have a HOF?
I went through the basic examples like
const square = num => num * num
const plus10 = (num, callback) => {
return callback(num) + 10
}
console.log(addTwo(7, square))
but I cannot make my mind around the previous example and working with lists.
I will appreciate help since the more I research the more confused I get.
Your mistake is to assume an array for showNames. Never do this. Always implement the simplest version of a function. In FP array is a computational effect. Don't implement such an effectful function as default:
const nameMarkup = name => {
return `<p>${name}</p>`;
}
const nameMarkup2 = name => {
return `<p>${name.toUpperCase()}!</p>`;
}
const showName = f => name => {
const r = f(name);
/* do something useful with r */
return r;
}
const names = ['peter', 'paul', 'patrice']
console.log(
showName(nameMarkup) ("peter"));
// lift the HOF if you want to process a non-deterministic number of names:
console.log(
names.map(showName(nameMarkup2)));
Now swapping the markup just means to pass another function argument. Your showName is more general, because a HOF lets you pass part of the functionality.
If we drop the array requirement, your showNames doesn't do anything useful anymore. It still illustrates the underlying idea, though.

Why immer.js doesn't allow setting dynamic properties on draft?

//I want my action to dispatch payload like
// {type:'update',payload:{'current.contact.mobile':'XXXXXXXXX'}}
//In reducer dynamically select the segment of state update needs to be applied to
//Below code doesn't work as expected though, draft always remains at same level
draft = dA.key.split('.').reduce((draft, k) => {
return draft[k]
}, draft);
//Or an ideal syntax may look like below line
draft['current.contact.mobile'] = dA.value;
//Code that works
draft['current']['contact']['mobile'] = dA.value;
I want my action to dispatch payload like
{type:'update',payload:{'current.contact.mobile':'XXXXXXXXX'}}
And in reducer dynamically select the segment of state that needs to be updated.
Is there something fundamentally wrong in doing this, I believe this could make life easier. Is there something that can done to achieve this ?
In your case, this code returns a primitive value like a string or number which is immutable.
draft = dA.key.split('.').reduce((draft, k) => {
return draft[k]
}, draft);
"Immer" is using Proxy to implement all this magic. The proxy could work only on objects for example Object, Array, Function etc.
so to fix your problem you can use code like this
import produce from "immer";
describe("Why immer.js doesn't allow setting dynamic properties on draft?", function() {
it("should allow set dynamic properties", function() {
const path = "foo.bar.zoo";
const state = { foo: { bar: { zoo: 1 } } };
const nextState = produce(state, draft => {
const vector = path.split(".");
const propName = vector.pop();
if (propName) {
draft = vector.reduce((it, prop) => it[prop], draft);
draft[propName] += 1;
}
});
expect(nextState.foo.bar.zoo).toEqual(state.foo.bar.zoo + 1);
});
});
In the code above, we get destination object and update the property of this object.
Some note about string and number.
Javascript has constructors for string and number which return objects not primitive values. But this is a very rare case when someone uses it explicitly.
Usually, we deal with it implicitly when writing something like this dA.key.split('.'). In this case, the interpreter would create a string object and call method "split" on it. Usually, this behavior is referred to as "Boxing"

Categories

Resources