Observable map property based on another observable - javascript

Lets say i have an observable that emits employees.
$employees
I wish to manipulate a sub property of each employee based on an Observable. For the example lets say display name.
Currently im performing the task like this.
const getDisplayName = (emp) => {}; //returns an observable
const mapFn = async (emp) => {
emp.displayName = await getDisplayName(emp).toPromise();
return emp;
}
$employees
.pipe(mergeMap(mapFn));
I think my confusion is that, to my understanding, we have two streams. The root $employees and the getDisplayName. My understanding is with the various merge operators the root value would be replaced by the value of the secondary stream. Not merged with.
Is there a better way to do this where i don't need to convert to a promise but can also just map a property of the employee?
Thanks.

you want to do:
$employees
.pipe(mergeMap(employees => {
return forkJoin(employees.map(emp => getDisplayName(emp).pipe(
map(displayName => ({...emp, ...{displayName}}))
)))
}));
if you really want a broken out mapFn:
const mapFn = employees => {
return forkJoin(employees.map(emp => getDisplayName(emp).pipe(
map(displayName => ({...emp, ...{displayName}}))
)));
}
$employees
.pipe(mergeMap(mapFn))
always advise against mixing rxjs and async / await. They're different methods of handling async operations and don't play nice. mergeMap needs you to return an observable, forkJoin executes observables in parralel, so you join all your employees mapped into their getName functions and then map the name into the original employee and return it.
EDIT: the above is if $eployees is emitting an array of employees. if it's just a single employee, do:
const mapFn = emp => {
return getDisplayName(emp).pipe(
map(displayName => ({...e, ...{displayName}}))
);
}
however, if it's emitting a single employee (or an array of employees) multiple times, it's important to understand the implications of using mergeMap vs switchMap vs concatMap. Let me know if that is the case.

This should work:
const emp$ = from([1, 2, 3]);
function getName(emp: number): Observable<string> {
return of(emp.toString());
}
const empNameTuple$ = emp$.pipe(mergeMap(emp => {
return getName(emp).pipe(map(name => [emp, name]));
}))
Here I'm just returning a tuple of [number, string], but you can map it however you wish.

Related

Refactoring chained RxJs subscriptions

I have a piece of code that I need to refactor because it's a hell of chained subscriptions.
ngOnInit(): void {
this.dossierService.getIdTree()
.subscribe(idTree => {
this.bootstrappingService.refreshObligations(idTree)
.subscribe(() => {
this.dossierPersonsService.retrieveDossierPersons(idTree)
.subscribe(debtors => {
this.retrieveObligations();
this.debtors = debtors;
});
});
});
}
The first call dossierService.getIdTree() retrieves idTree which is used by other services except obligationsService.retrieveObligations().
All service methods should be executed in the order they executed now. But retrieveDossierPersons and retrieveObligations can be executed in parallel.
retrieveObligations() is a method that subscribes to another observable. This method is used in a few other methods.
I've refactored it and it seems to work. But did I refactor it in a proper way or my code can be improved?
this.dossierService.getIdTree()
.pipe(
map(idTree => {
this.idTree = idTree;
}),
switchMap(() => {
return this.bootstrappingService.refreshObligations(this.idTree)
}),
switchMap(
() => {
return this.dossierPersonsService.retrieveDossierPersons(this.idTree)
},
)
)
.subscribe(debtors => {
this.retrieveObligations();
this.debtors = debtors;
});
Something like this (not syntax checked):
ngOnInit(): void {
this.dossierService.getIdTree().pipe(
switchMap(idTree =>
this.bootstrappingService.refreshObligations(idTree)).pipe(
switchMap(() => this.dossierPersonsService.retrieveDossierPersons(idTree).pipe(
tap(debtors => this.debtors = debtors)
)),
switchMap(() => this.retrieveObligations())
)
).subscribe();
}
Using a higher-order mapping operator (switchMap in this case) will ensure that the inner observables are subscribed and unsubscribed.
In this example, you don't need to separately store idTree because you have access to it down the chained pipes.
You could try something like:
ngOnInit(): void {
const getIdTree$ = () => this.dossierService.getIdTree();
const getObligations = idTree => this.bootstrappingService.refreshObligations(idTree);
const getDossierPersons = idTree => this.dossierPersonsService.retrieveDossierPersons(idTree);
getIdTree$().pipe(
switchMap(idTree => forkJoin({
obligations: getObligations(idTree)
debtors: getDossierPersons(idTree),
}))
).subscribe(({obligations, debtors}) => {
// this.retrieveObligations(); // seems like duplicate of refreshObligations?
this.debtors = debtors;
});
}
Depending on the rest of the code and on the template, you might also want to avoid unwrapping debtors by employing the async pipe instead
forkJoin will only complete when all of its streams have completed.
You might want also want to employ some error handling by piping catchError to each inner observable.
Instead of forkJoin you might want to use mergeMap or concatMap (they take an array rather than an object) - this depends a lot on logic and the UI. concatMap will preserve the sequence, mergeMap will not - in both cases, data could be display accumulatively, as it arrives. With forkJoin when one request gets stuck, the whole stream will get stuck, so you won't be able to display anything until all streams have completed.
You can use switchMap or the best choice is concatMap to ensure orders of executions
obs1$.pipe(
switchMap(data1 => obs2$.pipe(
switchMap(data2 => obs3$)
)
)

Need to combine two impure observables

I need to make 2 AJAX requests to the same endpoint that would return filtered and unfiltered data. Then I need to combine results and use them both in processing.
loadUnfilteredData() {
// remember status
const {status} = this.service.filters;
delete this.service.filters.status;
this.service.saleCounts$()
.subscribe((appCounts) =>
this.processUnfilteredData(appCounts)
);
// restore status
if (status) {
this.service.filters.status = status;
}
}
loadFilteredData() {
this.service.saleCounts$()
.subscribe((appCounts) =>
this.processFilteredData(appCounts)
);
}
The problem is that this.service.saleCounts$() is impure and instead of using arguments just uses this.service.filters.
That's why i have to store the status, then delete it from filter, then do the request, and then restore (because same filter is used by other requests).
So I can't just do combineLatest over two observables (because i need to restore).
Is there any workaround?
(p.s. I know the approach is disgusting, i know about state management and about pure functions. Just wanted to know is there any beautiful solution).
I believe your constraints require that the two operations are run sequentially , one after the other, rather than in parallel as is generally the case when we're using combineLatest.
To run two Observables sequentially, we can use switchMap (as an operator inside a pipe call in modern rxjs):
doFirstOperation()
.pipe(
switchMap(result => return doSecondOperation())
);
One potential issue with that is that you lose access to the result of doFirstOperation when you switchMap it to the result of doSecondOperation. To work around that, we can do something like:
doFirstOperation()
.pipe(
switchMap(firstResult => return doSecondOperation())
.pipe(
map(secondResult => [firstResult, secondResult])
)
);
i.e., use map to change the returned value of switchMap to be an array including both values.
Putting this together with your "disgusting" requirements for state management, you could use something like:
loadData() {
const { status } = this.service.filters;
delete this.service.filters.status;
return this.service
.saleCounts$()
.pipe(
finalize(() => {
if (status) {
this.service.filters.status = status;
}
}),
switchMap(filteredData => {
return this.service
.saleCounts$() // unfiltered query
.pipe(map(unfilteredData => [filteredData, unfilteredData]));
})
)
.subscribe(results => {
const [filteredData, unfilteredData] = results;
this.processFilteredData(filteredData);
this.processUnfilteredData(unfilteredData);
});
}
I'm not too many people would categorize that is beautiful, but it does at least allow you to get results in a way that looks like you used combineLatest, yet works around the constraints imposed by your impure method.

Modify Observable with another Observable and return the initial Observable

I want to return a ClientDetails object with a loaded image.
So retrieve an Observable, and modify the value with another Observable and return the whole Observable.
I hope the code below indicates what I am trying to do, but I know it can be done much cleaner using RxJS operators. Anyone know how to?
interface ClientDetails {
team: Member[];
}
interface Member {
id: number;
image: string;
}
this.clientDetails$ = this.clientService.getClientDetails().subscribe((details: ClientDetails) => {
details.team.forEach(member => {
this.imageService.getImage(member.id).subscribe((image: string) => {
member.image = image
}
}
}
You're right in assuming RxJS operators would make it more elegant. At the moment the variable this.clientDetails$ doesn't hold an observable and it wouldn't work as you'd expect it to.
Instead you could use higher order mapping operator switchMap to switch from one observable to another (it's better to avoid nested subscriptions in general) and forkJoin function to trigger multiple observables in parallel. You could also use JS destructing and RxJS map operator to return the object with all it's contents.
Try the following
this.clientDetails$ = this.clientService.getClientDetails().pipe(
switchMap((details: ClientDetails) =>
forkJoin(
details.team.map(member =>
this.imageService.getImage(member.id).pipe(
map(image: string => ({...member, member.image: image}))
)
)
).pipe(
map((team: any) => ({...details, details.team: team}))
)
);
);
Note: I didn't test the code. Please check if the object returned is what you actually require.
Try
this.clientDetails$ = this.clientService.getClientDetails().pipe(
switchMap((details: ClientDetails) => {
const images$: Observable<string[]> = forkJoin(
details.team.map(member => this.imageService.getImage(member.id))
);
return forkJoin([of(details), images$]);
}),
map(([details, images]) => {
return {
team: _.zipWith(details.team, images, (d, m) => d.image = m) // zipWith = lodash function
};
}),
).subscribe((details: ClientDetails) => console.log(details));

Subscribing to Map Data Structure in JavaScript doesn't work

I have been trying to observe changes on a Map Object in JavaScript but for some reason only can observe the creation of the object. Do observables not work when adding/removing data from a Map?
Here is the observable:
test(): Observable<Map<string, Object>> {
return of(this.testModel.test());
}
This is me subscribing to it:
test(): Observable<Map<string, Object>> {
let mapOb = this.testModel.test();
return Observable.create((obsrvr) => {
const originalSet = mapOb.set;
const originalDelete = mapOb.delete;
mapOb.set = (...args) => {
obsrvr.next(originalSet.call(mapOb, ...args));
};
mapOb.delete = (...args) => {
obsrvr.next(originalDelete.call(mapOb, ...args));
}
});
}
I see the log statement during the creation of the Map, but if i add any new entries to the Map nothing is logged. Anyone know why this may be happening?
I get an error at maoOb.set and mapOb.delete:
Type '(key: string, value: Object) => void' is not assignable to type '(key: string, value: Object) => Map<string, Object>'.
Type 'void' is not assignable to type 'Map<string, Object>'
You current approach doesn't seem correct when you want to listen to addition/deletion of data in a Map.
You are simply returning an Observable using of(Object Reference), this will no way know about things that you are doing with the Object you are passing with it.
You need to have an Observable which emits when you perform set() or delete() over the MapInstance.
You may modify your Map instance this way to achieve what you desire.
createObservable(mapOb) {
return Observable.create((obsrvr) => {
const originalSet = mapOb.set;
const originalDelete = mapOb.delete;
mapOb.set = (...args) => {
const setReturn = originalSet.call(mapOb, ...args);
obsrvr.next(setReturn);
return setReturn;
};
mapOb.delete = (...args) => {
const deleteReturn = originalDelete.call(mapOb, ...args);
obsrvr.next(deleteReturn);
return deleteReturn;
}
});
}
Pass the map to createObservable() method and subscribe to it. In this method, I have modified the set and delete methods of your map, so that it emits a value when those methods are called.
I have created a dummy example for the answer: Link.

Reselect - selector that invokes another selector?

I have a selector:
const someSelector = createSelector(
getUserIdsSelector,
(ids) => ids.map((id) => yetAnotherSelector(store, id),
); // ^^^^^ (yetAnotherSelector expects 2 args)
That yetAnotherSelector is another selector, that takes user id - id and returns some data.
However, since it's createSelector, I don't have access to store in it (I don't want it as a function because the memoization wouldn't work then).
Is there a way to access store somehow inside createSelector? Or is there any other way to deal with it?
EDIT:
I have a function:
const someFunc = (store, id) => {
const data = userSelector(store, id);
// ^^^^^^^^^^^^ global selector
return data.map((user) => extendUserDataSelector(store, user));
// ^^^^^^^^^^^^^^^^^^^^ selector
}
Such function is killing my app, causing everything to re-render and driving me nuts. Help appreciated.
!! However:
I have done some basic, custom memoization:
import { isEqual } from 'lodash';
const memoizer = {};
const someFunc = (store, id) => {
const data = userSelector(store, id);
if (id in memoizer && isEqual(data, memoizer(id)) {
return memoizer[id];
}
memoizer[id] = data;
return memoizer[id].map((user) => extendUserDataSelector(store, user));
}
And it does the trick, but isn't it just a workaround?
For Your someFunc Case
For your specific case, I would create a selector that itself returns an extender.
That is, for this:
const someFunc = (store, id) => {
const data = userSelector(store, id);
// ^^^^^^^^^^^^ global selector
return data.map((user) => extendUserDataSelector(store, user));
// ^^^^^^^^^^^^^^^^^^^^ selector
}
I would write:
const extendUserDataSelectorSelector = createSelector(
selectStuffThatExtendUserDataSelectorNeeds,
(state) => state.something.else.it.needs,
(stuff, somethingElse) =>
// This function will be cached as long as
// the results of the above two selectors
// does not change, same as with any other cached value.
(user) => {
// your magic goes here.
return {
// ... user with stuff and somethingElse
};
}
);
Then someFunc would become:
const someFunc = createSelector(
userSelector,
extendUserDataSelectorSelector,
// I prefix injected functions with a $.
// It's not really necessary.
(data, $extendUserDataSelector) =>
data.map($extendUserDataSelector)
);
I call it the reifier pattern because it creates a function that is pre-bound to the current state and which accepts a single input and reifies it. I usually used it with getting things by id, hence the use of "reify". I also like saying "reify", which is honestly the main reason I call it that.
For your However Case
In this case:
import { isEqual } from 'lodash';
const memoizer = {};
const someFunc = (store, id) => {
const data = userSelector(store, id);
if (id in memoizer && isEqual(data, memoizer(id)) {
return memoizer[id];
}
memoizer[id] = data;
return memoizer[id].map((user) => extendUserDataSelector(store, user));
}
That's basically what re-reselect does. You may wish to consider that if you plan on implementing per-id memoization at the global level.
import createCachedSelector from 're-reselect';
const someFunc = createCachedSelector(
userSelector,
extendUserDataSelectorSelector,
(data, $extendUserDataSelector) =>
data.map($extendUserDataSelector)
// NOTE THIS PART DOWN HERE!
// This is how re-reselect gets the cache key.
)((state, id) => id);
Or you can just wrap up your memoized-multi-selector-creator with a bow and call it createCachedSelector, since it's basically the same thing.
Edit: Why Returning Functions
Another way you can do this is to just select all the appropriate data needed to run the extendUserDataSelector calculation, but this means exposing every other function that wants to use that calculation to its interface. By returning a function that accepts just a single user base-datum, you can keep the other selectors' interfaces clean.
Edit: Regarding Collections
One thing the above implementation is currently vulnerable to is if extendUserDataSelectorSelector's output changes because its own dependency-selectors change, but the user data gotten by userSelector did not change, and neither did actual computed entities created by extendUserDataSelectorSelector. In those cases, you'll need to do two things:
Multi-memoize the function that extendUserDataSelectorSelector returns. I recommend extracting it to a separate globally-memoized function.
Wrap someFunc so that when it returns an array, it compares that array element-wise to the previous result, and if they have the same elements, returns the previous result.
Edit: Avoiding So Much Caching
Caching at the global level is certainly doable, as shown above, but you can avoid that if you approach the problem with a couple other strategies in mind:
Don't eagerly extend data, defer that to each React (or other view) component that's actually rendering the data itself.
Don't eagerly convert lists of ids/base-objects into extended versions, rather have parents pass those ids/base-objects to children.
I didn't follow those at first in one of my major work projects, and wish I had. As it is, I had to instead go the global-memoization route later since that was easier to fix than refactoring all the views, something which should be done but which we currently lack time/budget for.
Edit 2 (or 4 I guess?): Re-Regarding Collections pt. 1: Multi-Memoizing the Extender
NOTE: Before you go through this part, it presumes that the Base Entity being passed to the Extender will have some sort of id property that can be used to identify it uniquely, or that some sort of similar property can be derived from it cheaply.
For this, you memoize the Extender itself, in a manner similar to any other Selector. However, since you want the Extender to memoize on its arguments, you don't want to pass State directly to it.
Basically, you need a Multi-Memoizer that basically acts in the same manner as re-reselect does for Selectors.
In fact, it's trivial to punch createCachedSelector into doing that for us:
function cachedMultiMemoizeN(n, cacheKeyFn, fn) {
return createCachedSelector(
// NOTE: same as [...new Array(n)].map((e, i) => Lodash.nthArg(i))
[...new Array(n)].map((e, i) => (...args) => args[i]),
fn
)(cacheKeyFn);
}
function cachedMultiMemoize(cacheKeyFn, fn) {
return cachedMultiMemoizeN(fn.length, cacheKeyFn, fn);
}
Then instead of the old extendUserDataSelectorSelector:
const extendUserDataSelectorSelector = createSelector(
selectStuffThatExtendUserDataSelectorNeeds,
(state) => state.something.else.it.needs,
(stuff, somethingElse) =>
// This function will be cached as long as
// the results of the above two selectors
// does not change, same as with any other cached value.
(user) => {
// your magic goes here.
return {
// ... user with stuff and somethingElse
};
}
);
We have these two functions:
// This is the main caching workhorse,
// creating a memoizer per `user.id`
const extendUserData = cachedMultiMemoize(
// Or however else you get globally unique user id.
(user) => user.id,
function $extendUserData(user, stuff, somethingElse) {
// your magic goes here.
return {
// ...user with stuff and somethingElse
};
}
);
// This is still wrapped in createSelector mostly as a convenience.
// It doesn't actually help much with caching.
const extendUserDataSelectorSelector = createSelector(
selectStuffThatExtendUserDataSelectorNeeds,
(state) => state.something.else.it.needs,
(stuff, somethingElse) =>
// This function will be cached as long as
// the results of the above two selectors
// does not change, same as with any other cached value.
(user) => extendUserData(
user,
stuff,
somethingElse
)
);
That extendUserData is where the real caching occurs, though fair warning: if you have a lot of baseUser entities, it could grow pretty large.
Edit 2 (or 4 I guess?): Re-Regarding Collections pt. 2: Arrays
Arrays are the bane of caching existence:
arrayOfSomeIds may itself not change, but the entities that the ids within point to could have.
arrayOfSomeIds might be a new object in memory, but in reality has the same ids.
arrayOfSomeIds did not change, but the collection holding the referred-to entities did change, yet the particular entities referred to by these specific ids did not change.
That all is why I advocate for delegating the extension/expansion/reification/whateverelseification of arrays (and other collections!) to as late in the data-getting-deriving-view-rendering process as possible: It's a pain in the amygdala to have to consider all of this.
That said, it's not impossible, it just incurs some extra checking.
Starting with the above cached version of someFunc:
const someFunc = createCachedSelector(
userSelector,
extendUserDataSelectorSelector,
(data, $extendUserDataSelector) =>
data.map($extendUserDataSelector)
// NOTE THIS PART DOWN HERE!
// This is how re-reselect gets the cache key.
)((state, id) => id);
We can then wrap it in another function that just caches the output:
function keepLastIfEqualBy(isEqual) {
return function $keepLastIfEqualBy(fn) {
let lastValue;
return function $$keepLastIfEqualBy(...args) {
const nextValue = fn(...args);
if (! isEqual(lastValue, nextValue)) {
lastValue = nextValue;
}
return lastValue;
};
};
}
function isShallowArrayEqual(a, b) {
if (a === b) return true;
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false;
// NOTE: calling .every on an empty array always returns true.
return a.every((e, i) => e === b[i]);
}
return false;
}
Now, we can't just apply this to the result of createCachedSelector, that'd only apply to just one set of outputs. Rather, we need to use it for each underlying selector that createCachedSelector creates. Fortunately, re-reselect lets you configure the selector creator it uses:
const someFunc = createCachedSelector(
userSelector,
extendUserDataSelectorSelector,
(data, $extendUserDataSelector) =>
data.map($extendUserDataSelector)
)((state, id) => id,
// NOTE: Second arg to re-reselect: options object.
{
// Wrap each selector that createCachedSelector itself creates.
selectorCreator: (...args) =>
keepLastIfEqualBy(isShallowArrayEqual)(createSelector(...args)),
}
)
Bonus Part: Array Inputs
You may have noticed that we only check array outputs, covering cases 1 and 3, which may be good enough. Sometimes, however, you may need catch case 2, as well, checking the input array.
This is doable by using reselect's createSelectorCreator to make our own createSelector using a custom equality function
import { createSelectorCreator, defaultMemoize } from 'reselect';
const createShallowArrayKeepingSelector = createSelectorCreator(
defaultMemoize,
isShallowArrayEqual
);
// Also wrapping with keepLastIfEqualBy() for good measure.
const createShallowArrayAwareSelector = (...args) =>
keepLastIfEqualBy(
isShallowArrayEqual
)(
createShallowArrayKeepingSelector(...args)
);
// Or, if you have lodash available,
import compose from 'lodash/fp/compose';
const createShallowArrayAwareSelector = compose(
keepLastIfEqualBy(isShallowArrayEqual),
createSelectorCreator(defaultMemoize, isShallowArrayEqual)
);
This further changes the someFunc definition, though just by changing the selectorCreator:
const someFunc = createCachedSelector(
userSelector,
extendUserDataSelectorSelector,
(data, $extendUserDataSelector) =>
data.map($extendUserDataSelector)
)((state, id) => id, {
selectorCreator: createShallowArrayAwareSelector,
});
Other Thoughts
That all said, you should try taking a look at what shows up in npm when you search for reselect and re-reselect. Some new tools there that may or may not be useful to certain cases. You can do a lot with just reselect and re-reselect plus a few extra functions to fit your needs, though.
A problem we faced when using reselect is that there is no support for dynamic dependency tracking. A selector needs to declare upfront which parts of the state will cause a recomputation.
For example, I have a list of online user IDs, and a mapping of users:
{
onlineUserIds: [ 'alice', 'dave' ],
notifications: [ /* unrelated data */ ]
users: {
alice: { name: 'Alice' },
bob: { name: 'Bob' },
charlie: { name: 'Charlie' },
dave: { name: 'Dave' },
eve: { name: 'Eve' }
}
}
I want to select a list of online users, e.g. [ { name: 'Alice' }, { name: 'Dave' } ].
Since I cannot know upfront which users will be online, I need to declare a dependency on the whole state.users branch of the store:
This works, but this means that changes to unrelated users (bob, charlie, eve) will cause the selector to be recomputed.
I believe this is a problem in reselect’s fundamental design choice: dependencies between selectors are static. (In contrast, Knockout, Vue and MobX do support dynamic dependencies.)
We faced the same problem and we came up with #taskworld.com/rereselect. Instead of declaring dependencies upfront and statically, dependencies are collected just-in-time and dynamically during each computation:
This allows our selectors to have a more fine-grained control of which part of state can cause a selector to be recomputed.
Preface
I faced the same case as yours, and unfortunately didn't find an efficient way to call a selector from another selector's body.
I said efficient way, because you can always have an input selector, which passes down the whole state (store), but this will recalculate your selector on each state's changes:
const someSelector = createSelector(
getUserIdsSelector,
state => state,
(ids, state) => ids.map((id) => yetAnotherSelector(state, id)
)
Approaches
However, I found out two possible approaches, for the use-case described below. I guess your case is similar, so you can take some insights.
So the case is as follows: You have a selector, that gets a specific User from the Store by an id, and the selector returns the User in a specific structure. Let's say getUserById selector. For now everything's fine and simple as possible. But the problem occurs when you want to get several Users by their ids and also reuse the previous selector. Let's name it getUsersByIds selector.
1. Using always an Array, for input ids values
The first possible solution is to have a selector that always expects an array of ids (getUsersByIds) and a second one, that reuses the previous, but it will get only 1 User (getUserById). So when you want to get only 1 User from the Store, you have to use getUserById, but you have to pass an array with only one user id.
Here's the implementation:
import { createSelectorCreator, defaultMemoize } from 'reselect'
import { isEqual } from 'lodash'
/**
* Create a "selector creator" that uses `lodash.isEqual` instead of `===`
*
* Example use case: when we pass an array to the selectors,
* they are always recalculated, because the default `reselect` memoize function
* treats the arrays always as new instances.
*
* #credits https://github.com/reactjs/reselect#customize-equalitycheck-for-defaultmemoize
*/
const createDeepEqualSelector = createSelectorCreator(
defaultMemoize,
isEqual
)
export const getUsersIds = createDeepEqualSelector(
(state, { ids }) => ids), ids => ids)
export const getUsersByIds = createSelector(state => state.users, getUsersIds,
(users, userIds) => {
return userIds.map(id => ({ ...users[id] })
}
)
export const getUserById = createSelector(getUsersByIds, users => users[0])
Usage:
// Get 1 User by id
const user = getUserById(state, { ids: [1] })
// Get as many Users as you want by ids
const users = getUsersByIds(state, { ids: [1, 2, 3] })
2. Reuse selector's body, as a stand-alone function
The idea here is to separate the common and reusable part of the selector body in a stand-alone function, so this function to be callable from all other selectors.
Here's the implementation:
export const getUsersByIds = createSelector(state => state.users, getUsersIds,
(users, userIds) => {
return userIds.map(id => _getUserById(users, id))
}
)
export const getUserById = createSelector(state => state.users, (state, props) => props.id, _getUserById)
const _getUserById = (users, id) => ({ ...users[id]})
Usage:
// Get 1 User by id
const user = getUserById(state, { id: 1 })
// Get as many Users as you want by ids
const users = getUsersByIds(state, { ids: [1, 2, 3] })
Conclusion
Approach #1. has less boilerplate (we don't have a stand-alone function) and has clean implementation.
Approach #2. is more reusable. Imagine the case, where we don't have an User's id when we call a selector, but we get it from the selector's body as a relation. In that case, we can easily reuse the stand-alone function. Here's а pseudo example:
export const getBook = createSelector(state => state.books, state => state.users, (state, props) => props.id,
(books, users, id) => {
const book = books[id]
// Here we have the author id (User's id)
// and out goal is to reuse `getUserById()` selector body,
// so our solution is to reuse the stand-alone `_getUserById` function.
const authorId = book.authorId
const author = _getUserById(users, authorId)
return {
...book,
author
}
}
I have made the following workaround:
const getSomeSelector = (state: RootState) => () => state.someSelector;
const getState = (state: RootState) => () => state;
const reportDerivedStepsSelector = createSelector(
[getState, getSomeSelector],
(getState, someSelector
) => {
const state = getState();
const getAnother = anotherSelector(state);
...
}
The function getState will never change and you can get the complete state from your selector without breaking the selector memo.
Recompute is an alternative to reselect that implements dynamic dependency tracking and allows any number of arguments to be passed to the selector, you could check if this would solve your problem
you add as many parameters as you want, and parameters can be other selector functions.
the end callback have the results of these selectors respectively ..
export const anySelector = createSelector(firstSelector, second, ..., (resultFromFirstSelector, resultFromSecond, ...) => { // do your thing.. });
documentation

Categories

Resources