Conditionally dispatching side-effects - redux

Say I have a search component that searches based on tags selected by the user (Think Pocket). If the user deselects all tags, I know the result of the search is going to be null; there is no need to hit the server. How can I refactor this epic to conditionally run the side-effect?
const removeTagEpic = (action$: any, store: any) =>
action$
.ofType(types.REMOVE_SELECTED_TAG)
.map((action: any) => action.payload)
.mergeMap((tag: any) => {
//only do this is store.getState().tags > 0
ajax.getJSON(`https://api.github.com/users/dwaynelavon`)
.flatMap(response =>
Observable.concat(
Observable.of(actions.fetchingProfiles(true)),
Observable.of(actions.fetchProfilesFulfilled(profiles)),
Observable.of(actions.fetchingProfiles(false))
)
)
})

Can't you use filter operator and check the tag in there?
const removeTagEpic = (action$: any, store: any) =>
action$
.ofType(types.REMOVE_SELECTED_TAG)
.filter(_ => store.getState().tags > 0)
.map((action: any) => action.payload)
.mergeMap((tag: any) => {
ajax.getJSON(`https://api.github.com/users/dwaynelavon`)
.flatMap(response =>
Observable.concat(
Observable.of(actions.fetchingProfiles(true)),
Observable.of(actions.fetchProfilesFulfilled(profiles)),
Observable.of(actions.fetchingProfiles(false))
)
)
})
The source of types.REMOVE_SELECTED_TAG won't be emitted if the condition is not met.
https://www.learnrxjs.io/operators/filtering/filter.html

Related

NGRX selectors: factory selector within another selector without prop in createSelector method

Using the factory selector pattern const selectA = (id: number) => createSelector(...) I have an instance where I want to reuse this selector within another selector (that iterates through an array of IDs) but I don't know the value to pass into the factor selector when calling createSelector.
So I have a selector that I use whenever I want to get a slice of the state for component A.
const selectA = (id: number) =>
createSelector(
selector1.selectEntityMap,
selector2.selectEntityMap,
selector3ById(id),
(
thing1,
thing2,
thing3
) => {
return ...
});
Now I want to get a list of component A for each item in an array.
const selectListOfA = (ids: number[]) =>
createSelector(
selectA,
(selectorA) => {
return ids.map((id) => selectorA(id));
});
The problem is selectA, which is now a factory selector, expects a parameter, but I don't know it when calling createSelector.
I can get the code to compile by creating another factory onto of the factory
const selectAFactory = () => selectA;
And then reference the new factory in the createSelector
const selectListOfA = (ids: number[]) =>
createSelector(
selectAFactory, <<< here
(selectorA) => {
return ids.map((id) => selectorA(id));
});
But of course, what's now happening is the selector is returning a list of MemoizedSelector[].
This pattern doesn't seem like it should be this complicated, are people not reusing their selectors in this way, what am I missing?
The function returned by selectA is a standard function, ie nothing magical about it, as explained well in this blog post: https://dev.to/zackderose/ngrx-fun-with-createselectorfactory-hng
This means selectListOfA can simply call the function returned from selectA for each id and an array of the state slices for component A will be returned:
export const selectListOfA = (ids: number[]) =>
createSelector(
(state) => state,
(state) => ids.map((id) => selectA(id)(state))
);
This works as expected but since the projector function will be executed every time anything in the store changes (recreating the selector for every id) this solution will have major performance issues.
We could just as well simplify the code to this with equally poor performance:
const selectListOfA = (ids: number[]) =>
(state) => ids.map(
(id: number) => selectA(id)(state)
);
If we instead supply an array of selectors as input to the createSelector call then Ngrx will be able to correctly determine when it has to reevaluate the selectA selectors:
const selectListOfA = (ids: number[]) =>
createSelector(
ids.map((id) => selectA(id)), // This results in an array of selectors
(...resultArr) => resultArr
);
However, Typescript will complain since the createSelector method does not have a matching overload declared for an array of variable length so we need to loosen up the input type of the array (to any) as well as specify the return type of selectListOfA.
The answer to the question is thus:
const selectListOfA = (ids: number[]) =>
(createSelector(
ids.map((id) => selectA(id)) as any,
(...resultArr) => resultArr
) as (state) => string[]);

Pass a whole object to redux reselect selector, but change it only if one property of the object changes

Started working with Reselect and there's one thing I can't seem to find an answer for.
Say I have a helper fn (getVehicleList) which does some heavy calculations, so I don't want it to re-run too much. I use state selectors to get the properties I need, something like:
const getVehicles = (state) => state.vehicles.vehicles;
const getVehicle = (state) => state.vehicles.vehicle;
const getUserId = (state) => state.auth.user.id;
I then have implemented the createSelector:
const getVehicles = createSelector(
[getVehicles,
getVehicle,
getUserId],
(vehicles, vehicle, id) => getVehicleList(
vehicles,
vehicle,
id,
),
);
Now, vehicle returns an object with multiple fields. If any of these fields change, the object changes and so everything is recomputed again. Is there a way to stop this recomputing until the id and only the id of the vehicle changes?
I tried doing a state selector for the id, like
const getVehicle = (state) => state.vehicles.vehicle.id;
But that doesn't work for me, cause I need the whole vehicle object inside my helper fn and not just the id.
Thanks in advance for the help!
You can try the following:
const getVehicles = (state) => state.vehicles.vehicles;
const getVehicle = (state) => state.vehicles.vehicle;
const getUserId = (state) => state.auth.user.id;
const selectVhicleId = createSelector(
[getVehicle],
({ id }) => id //return only the id
);
const selectVehicles = createSelector(
[getVehicles, selectVhicleId, getUserId],
(vehicles, vehicleId, id) =>
getVehicleList(vehicles, { id: vehicleId }, id)
);
Here is some information about how I use reselect with React.
Here is an example that re calculate vehicles when vehicle.id changes (or any of the other dependencies). It will not re calculate if other values of vehicle change so the vehicle used getVehicleList gets a stale vehicle passed to it that is only refreshed when vehicle.id changes:
const getVehicles = (state) => state.vehicles.vehicles;
const getVehicle = (state) => state.vehicles.vehicle;
const getUserId = (state) => state.auth.user.id;
const createSelectVehicles = (vehicle) =>
createSelector([getVehicles, getUserId], (vehicles, id) =>
getVehicleList(vehicles, vehicle, id)
);
const Component = () => {
//only re calculate vehicle if vehicle.id changes
const vehicle = useSelector(
getVehicle,
(a, b) => a?.id === b?.id
);
//only create the selector when vehicle changes
// vehicle only changes when vehicle.id changes
const selectVehicles = React.useMemo(
() => createSelectVehicles(vehicle),
[vehicle]
);
//vehicles is re calculated when vehicle.id changes
// or when state.vehicles.vehicles changes or
// when state.auth.user.id changes
const vehicles = useSelector(selectVehicles);
};

How can I subscribe and unsubscribe from individual firestore queries?

I am trying to modify an effect I have made into letting me start and stop multiple firestore queries by using two actions. Currently the effect allows me to start and stop a single firestore query by listening for two separate actions in the effect. I simply use a switchMap to switch into an empty observable when there is a stop action. This works just fine.
#Effect()
startStopQuery$ = this.actions$.pipe(
ofType(
ActionTypes.START,
ActionTypes.STOP
),
switchMap(action => {
if (action.type === ActionTypes.STOP) {
return of([]);
} else {
return this.afs.collection('collection', ref => {
return ref.where('field', '==', 'x');
}).stateChanges();
}
}),
mergeMap(actions => actions),
map(action => {
return {
type: `[Collection] ${action.type}`,
payload: { id: action.payload.doc.id, ...action.payload.doc.data() }
};
})
);
What I actually want to do is to have multiple queries ongoing that I can start and stop with those same two actions, but where it depends on the action payload. When I modified it everytime I performed a new query the last one stops working. I think it is because the switchMap operator switches away from my last query observable. This is the best I have come up with:
#Effect()
startStopQueryById$ = this.actions$.pipe(
ofType(
ActionTypes.START_BY_ID,
ActionTypes.STOP_BY_ID
),
switchMap(action => {
if (action.type === ActionTypes.STOP_BY_ID) {
return of([]);
} else {
return this.afs.collection('collection', ref => {
return ref.where('field', '==', action.id);
}).stateChanges();
}
}),
mergeMap(actions => actions),
map(action => {
return {
type: `[Collection] ${action.type}`,
payload: { id: action.payload.doc.id, ...action.payload.doc.data() }
};
})
);
As I said, I think the issue is the switchMap operator. But that is also what I depended on to make the "stop" work in the first place. I cant seem to come up with another solution as I am not very well versed in the style yet.
Any help would be greatly appreciated!
I came up with a solution. I make an object that maps ID's to the firestore statechanges observables. On the start action I make the listener and adds it to the object. I make sure that it automatically unsubscribe by piping takeUntil with the corresponding stop action. It returns a merge of all the observables in the object and I silply do as before. I also have a seperate effect triggered by the stop action to remove the observable from the object. It looks like so:
queriesById: {[id: string]: Observable<DocumentChangeAction<Element>[]>} = {};
#Effect()
startQuery$ = this.actions$.pipe(
ofType(ActionTypes.START_BY_ID),
switchMap(action => {
this.queriesByPlay[action.pid] = this.afs.collection<Element>('requests', ref => {
return ref.where('field', '==', action.id);
}).stateChanges().pipe(
takeUntil(
this.actions$.pipe(
ofType(ActionTypes.STOP_BY_ID),
filter(cancelAction => action.id === cancelAction.id),
)
)
);
return merge(
Object.values(this.queriesByPlay)
);
}),
mergeMap(actions => actions),
mergeMap(actions => actions),
map(action => {
return {
type: `[Collection] ${action.type}`,
payload: { id: action.payload.doc.id, ...action.payload.doc.data() }
};
})
);
Effect({dispatch: false})
stopQuery$ = this.actions$.pipe(
ofType(ActionTypes.STOP_BY_ID),
map(action => delete this.queriesByPlay[action.id]),
);
This seems to work and have no problems except for being convoluted hard to understand.

can't unsubscribe on my ngrx selector call

I have the following code
selectProduct(id){
const sub = this.store$.select(ProductStoreSelector.selectProductByID(id)).subscribe((product) => {
this.store$.dispatch(new ProductStoreAction.SetSelectedProduct({productID: product.id}));
sub.unsubscribe();
});
}
Basically, I would like to get my list of product, and get one by ID, then change my store state so that the selectedProduct become the one I just selected
export const featureAdapter: EntityAdapter<IProduct> = createEntityAdapter<IProduct>({
selectId: model => model.id,
});
export const selectAllProducts: (state: object) => Array<IProduct> = featureAdapter.getSelectors(selectProductsState).selectAll;
export const selectProductByID = (id: string) => createSelector(
selectAllProducts,
(products) => products.find((product) => product.id === id)
);
and my store is an entityState of products with one selected
export interface State extends EntityState<IProduct> {
selectedProduct: IProduct;
}
but the problem is,
althougt I do get my productId back, I can't unsubscribe to sub.unsubscribe() because it is undefined.
You can use either take(1) to listen for values only one time. Otherwise try to unsubscribe like below:
selectProduct(id){
this.store$.select(ProductStoreSelector.selectProductByID(id)).subscribe((product) => {
this.store$.dispatch(new ProductStoreAction.SetSelectedProduct({productID: product.id}));
}).unsubscribe();
}
using take(1):
selectProduct(id){
this.store$.select(ProductStoreSelector.selectProductByID(id))
.take(1)
.subscribe((product) => {
this.store$.dispatch(new ProductStoreAction.SetSelectedProduct({productID: product.id}));
});
}

how to pipe functions, when a promise in the promise in the middle checks authorization?

i'm trying to compose some functions together:
compose = (...fns) => fns.reduce((f, g) => (...args) => f(g(...args)));
checkAuthorization returns a promise that check if a user is authorized.
buildParams receives someRequestData, and pipes the result to searchItem.
checkAuthorization()
.then(() => {
compose(
searchItem,
buildParams
)(someRequestData)
}, (e) => {
handleError(e)
})
I think it's OK, but I wish to have a more elegant look for readability, something like:
compose(
searchItem,
checkAuthorization
buildParams
)(someRequestData)
so what will happen is:
1) build params
2) checkAuth
3) search item
Any suggestions?
No, that's not possible, since checkAuthorisation does not receive and pass through the params. And even if you would rewrite it to do that, it still would be weird and a reader would assume that you're building the params whose authorisation should be checked. So don't do that - you have a non-linear flow, and trying to force it into some linear composition is no good.
Btw, I would recommend to avoid compose when you're calling the function immediately anyway:
checkAuthorization().then(() =>
searchItem(buildParams(someRequestData))
, e =>
handleError(e)
);
or maybe
checkAuthorization().then( compose(searchItem, buildParams, ()=>someRequestData)
, handleError ); // ^^^^ "const"
Here's a composer to handle both sync functions and Promises. Looks like it works correctly maintaining the order:
// Async pipe try. Pass functions left to right
const pipePromises = (...fns) => x => fns.reduce((p, fn) => p.then(fn), Promise.resolve(x));
// functions for the test
const me = x => new Promise(function(resolve, reject) {
setTimeout(() => resolve(x), 10)
})
const double = x => new Promise(function(resolve, reject) {
setTimeout(() => resolve(x * 2), 30)
})
const inc = x => new Promise(function(resolve, reject) {
setTimeout(() => resolve(x + 1), 400)
})
const log = x => { console.log('log: ', x); return x }
const syncTriple = x => x * 3; // sync function
// let's call our chain
pipePromises(
me, log, // 3
double, log, // 6
syncTriple, log, // 18 -- SYNC
inc, log, // 19
double, log, // 38
inc, log, // 39
syncTriple, log, // 117 -- SYNC
inc, log // 118
)(3) // 3
I just made an npm module to handle elegant Promise composition.
It's still in early stage, but you're welcome to check out the code and change it as it fits your needs and standards.
Basically it offers two methods which might meet your needs:
Combine
With Promise.combine({...}) you can combine several Promises by providing an object with a series of functions returning Promises and accepting the result of previous ones as input like this:
Promise.combine({
item: () => searchItem,
auth: ({item}) => checkAuth,
params: ({item, auth}) => buildParams
}).then(({item, auth, params}) => {
// here you can do what you need
})
Reduce
With Promise.reduce([...]) you can chain Promises in an array of functions returning Promises and accepting as input the output of the previously executed Promise:
Promise.reduce([
() => searchItem,
(item) => checkAuth,
(auth) => buildParams
]).then((params) => {
// here you can do what you need
})
Notice in this case you won't have access to item in the .then() function, but you could always compose the result of the checkAuth Promise in order to pass the item downstream as well:
Promise.reduce([
() => searchItem,
(item) => checkAuth.then((auth) => {
return {auth, item}
}),
({auth, item}) => buildParams.then((params) => {
return {params, item}
}),
]).then(({params, item}) => {
// here you can do what you need
})
Input
You can also add some input data from the request like this:
Promise.reduce([
(requestData) => searchItem,
(item) => checkAuth,
(auth) => buildParams
], requestData).then((params) => {
// here you can do what you need
})
See I passed the requestData as second parameter of Promise.reduce([...], requestData) and it gets passed as parameter to the first function.
Here you can see the functions code.
Hope this helps.

Resources