Composing and sequencing multiple epics in redux-observable - javascript

I have a problem that I don't know how to resolve.
I have two epics that do requests to api and update the store:
const mapSuccess = actionType => response => ({
type: actionType + SUCCESS,
payload: response.response,
});
const mapFailure = actionType => error => Observable.of({
type: actionType + FAILURE,
error,
});
const characterEpic = (action$, store) =>
action$.ofType(GET_CHARACTER)
.mergeMap(({ id }) => {
return ajax(api.fetchCharacter(id))
.map(mapSuccess(GET_CHARACTER))
.catch(mapFailure(GET_CHARACTER));
});
const planetsEpic = (action$, store) =>
action$.ofType(GET_PLANET)
.mergeMap(({ id }) => {
return ajax(api.fetchPlanet(id))
.map(mapSuccess(GET_PLANET))
.catch(mapFailure(GET_PLANET));
});
Now I have a simple scenario where I would like to create the third action that combines the two above, let's call it fetchCharacterAndPlanetEpic. How can I do it?
I think in many cases (and in my) it's important that result of the first action is dispatched to the store before the second begins. That would be probably trivial to do with Promises and redux-thunk, but I can't somehow think of a way to do it with rxjs and redux-observable.
Thanks!

Tomasz's answer works and has pros and cons (it was originally suggested in redux-observable#33). One potential issue is that it makes testing harder, but not impossible. e.g. you may have to use dependency injection to inject a mock of the forked epic.
I had started typing up an answer prior to seeing his, so I figured I might as well post it for posterity in case it's interesting to anyone.
I also previously answered another question which is very similar that may be helpful: How to delay one epic until another has emitted a value
We can emit the getCharacter(), then wait for a matching GET_CHARACTER_SUCCESS before we emit the getPlanet().
const fetchCharacterAndPlanetEpic = (action$, store) =>
action$.ofType(GET_CHARACTER_AND_PLANET)
.mergeMap(({ characterId, planetId }) =>
action$.ofType(GET_CHARACTER_SUCCESS)
.filter(action => action.payload.id === characterId) // just in case
.take(1)
.mapTo(getPlanet(planetId))
.startWith(getCharacter(characterId))
);
One potential negative of this approach is that theoretically the GET_CHARACTER_SUCCESS this epic receives could be a different one the exact one we were waiting for. The filter action.payload.id === characterId check protects you mostly against that, since it probably doesn't matter if it was specifically yours if it has the same ID.
To truly fix that issue you'd need some sort of unique transaction tracking. I personally use a custom solution that involves using helper functions to include a unique transaction ID. Something like these:
let transactionID = 0;
const pend = action => ({
...action,
meta: {
transaction: {
type: BEGIN,
id: `${++transactionID}`
}
}
});
const fulfill = (action, payload) => ({
type: action.type + '_FULFILLED',
payload,
meta: {
transaction: {
type: COMMIT,
id: action.meta.transaction.id
}
}
});
const selectTransaction = action => action.meta.transaction;
Then they can be used like this:
const getCharacter = id => pend({ type: GET_CHARACTER, id });
const characterEpic = (action$, store) =>
action$.ofType(GET_CHARACTER)
.mergeMap(action => {
return ajax(api.fetchCharacter(action.id))
.map(response => fulfill(action, payload))
.catch(e => Observable.of(reject(action, e)));
});
const fetchCharacterAndPlanetEpic = (action$, store) =>
action$.ofType(GET_CHARACTER_AND_PLANET)
.mergeMap(action =>
action$.ofType(GET_CHARACTER_FULFILLED)
.filter(responseAction => selectTransaction(action).id === selectTransaction(responseAction).id)
.take(1)
.mapTo(getPlanet(action.planetId))
.startWith(getCharacter(action.characterId))
);
The key detail is that the initial "pend" action holds a unique transaction ID in the meta object. So that initial action basically represents the pending request and is then used when someone wants to fulfill, reject, or cancel it. fulfill(action, payload)
Our fetchCharacterAndPlanetEpic code is kinda verbose and if we used something like this we'd be doing it a lot. So let's make a custom operator that handles it all for us.
// Extend ActionsObservable so we can have our own custom operators.
// In rxjs v6 you won't need to do this as it uses "pipeable" aka "lettable"
// operators instead of using prototype-based methods.
// https://github.com/ReactiveX/rxjs/blob/master/doc/pipeable-operators.md
class MyCustomActionsObservable extends ActionsObservable {
takeFulfilledTransaction(input) {
return this
.filter(output =>
output.type === input.type + '_FULFILLED' &&
output.meta.transaction.id === input.meta.transaction.id
)
.take(1);
}
}
// Use our custom ActionsObservable
const adapter = {
input: input$ => new MyCustomActionsObservable(input$),
output: output$ => output$
};
const epicMiddleware = createEpicMiddleware(rootEpic, { adapter });
Then we can use that custom operator in our epic nice and cleanly
const fetchCharacterAndPlanetEpic = (action$, store) =>
action$.ofType(GET_CHARACTER_AND_PLANET)
.mergeMap(action =>
action$.takeFulfilledTransaction(action)
.mapTo(getPlanet(action.planetId))
.startWith(getCharacter(action.characterId))
);
The transaction-style solution described here is truly experimental. In practice there are some warts with it I've noticed over the years and I haven't gotten around to thinking about how to fix them. That said, overall it's been pretty helpful in my apps. In fact, it can also be used to do optimistic updates and rollbacks too! A couple years ago I made this pattern and the optional optimistic update stuff into the library redux-transaction but I've never circled back to give it some love, so use at your own risk. It should be considered abandoned, even if I may come back to it.

I've found a help in this github topic.
First I had to create helper method that will allow me to combine epics together:
import { ActionsObservable } from 'redux-observable';
const forkEpic = (epicFactory, store, ...actions) => {
const actions$ = ActionsObservable.of(...actions);
return epicFactory(actions$, store);
};
Which allows me to call any epic with stubbed actions like:
const getCharacter = id => ({ type: GET_CHARACTER, id });
forkEpic(getCharacterEpic, store, getCharacter(characterId))
...and will return result Observable of that epic. This way I can combine two epics together:
export const getCharacterAndPlanet = (characterId, planetId) => ({
type: GET_CHARACTER_AND_PLANET,
characterId,
planetId,
});
const fetchCharacterAndPlanetEpic = (action$, store) =>
action$.ofType(GET_CHARACTER_AND_PLANET)
.mergeMap(({ characterId, planetId }) =>
forkEpic(characterEpic, store, getCharacter(characterId))
.mergeMap((action) => {
if (action.type.endsWith(SUCCESS)) {
return forkEpic(planetsEpic, store, getPlanet(planetId))
.startWith(action);
}
return Observable.of(action);
})
);
In this example second request is called only if first ends with SUCCESS.

Related

Passing the return value of a useAppSelector to a variable in React-Redux

I might have missed something super obvious when refactoring my implementation of Redux in a React application, but when I'm trying to access the value of one of my slices I get thrown some errors by the Typescript Compiler about not being able to assign a (func) => string to a parameter of type string.
For context, here's my implementation:
Slice:
export const environmentSlice = createSlice({
name: 'environment',
initialState,
reducers: {
updateEnvironment: (state, action:PayloadAction<string>) => {
state.value = action.payload
}
}
});
export const { updateEnvironment } = environmentSlice.actions;
export const selectEnvironment = (state: RootState) => state.environment.value;
How i've defined the interface for my environment:
// Defining type for state
interface EnvironmentState {
value: string,
};
// define the initial state using that type
const initialState: EnvironmentState = {
value: 'live',
}
How RootState is defined in my store:
export const store = configureStore({
reducer: {
loggedIn: loggedInReducer,
environment: environmentReducer,
token: tokenReducer,
},
})
export type RootState = ReturnType<typeof store.getState>;
How I'm trying to get the value into one of my React Components:
let environment = useAppSelector((state: RootState) => {
return state.environment.value
});
I've also tried following the implementation in the redux docs here but had no luck with that: https://react-redux.js.org/tutorials/typescript-quick-start#use-typed-hooks-in-components
When assigning this value, i'm using useAppDispatch() assigned to a variable inside of the response section of a fetch request:
fetch('/api/authenticate', requestOptions)
.then(async response => {
if (response.status === 200) {
let data = await response.json();
dispatch({ type: toggle });
dispatch({ type: updateToken, payload: data.token });
webHelpers.get('/api/user', 'default', 'auth', data.token, (data: any) => {
dispatch({ type: updateUser, payload: data.full_name })
});
//
navigate('../management/staff');
Please note: The environment isn't updated upon sign-in but only once the user selects an option from a drop-down menu in the DOM. It's directly after this sign-in and navigation that the application crashes, however, as it states it cannot read the 'value' on the following:
const token = useAppSelector(state => {
return state.token.value
});
The above is reached after the navigate('../management/staff'); is called.
Edit: Accidently included wrong code snippet when showing useAppSelector in use. Update to fix.
Edit2: Added in section about the dispatches that assigns these values.
Edit3: Managed to resolve the solution but not in the exact way I'd hoped so I'll leave this open. The issue appeared to be that the attempts to dispatch data via the slices I'd added to my store's reducer didn't work, having all of those methods on one sole slice resolved the issue. This isn't ideal as I'd wanted 3 separate slices to manage each of these states separately. There must be some issue in my redux store with setting these up to work independently.

How to implement this without triggering an infinite loop with useEffect

So I have a situation where I have this component that shows a user list. First time the component loads it gives a list of all users with some data. After this based on some interaction with the component I get an updated list of users with some extra attributes. The thing is that all subsequent responses only bring back the users that have these extra attributes. So what I need is to save an initial state of users that has a list of all users and on any subsequent changes keep updating/adding to this state without having to replace the whole state with the new one because I don't want to lose the list of users.
So far what I had done was that I set the state in Redux on that first render with a condition:
useEffect(() => {
if(users === undefined) {
setUsers(userDataFromApi)
}
userList = users || usersFromProp
})
The above was working fine as it always saved the users sent the first time in the a prop and always gave priority to it. Now my problem is that I'm want to add attributes to the list of those users in the state but not matter what I do, my component keeps going into an infinite loop and crashing the app. I do know the reason this is happening but not sure how to solve it. Below is what I am trying to achieve that throws me into an infinite loop.
useEffect(() => {
if(users === undefined) {
setUsers(userDataFromApi)
} else {
//Users already exist in state
const mergedUserData = userDataFromApi.map(existingUser => {
const matchedUser = userDataFromApi.find(user => user.name === existingUser.name);
if (matchedUser) {
existingUser.stats = user.stats;
}
return existingUser;
})
setUsers(mergedUserData)
}
}, [users, setUsers, userDataFromApi])
So far I have tried to wrap the code in else block in a separate function of its own and then called it from within useEffect. I have also tried to extract all that logic into a separate function and wrapped with useCallback but still no luck. Just because of all those dependencies I have to add, it keeps going into an infinite loop. One important thing to mention is that I cannot skip any dependency for useCallback or useEffect as the linter shows warnings for that. I need to keep the logs clean.
Also that setUsers is a dispatch prop. I need to keep that main user list in the Redux store.
Can someone please guide me in the right direction.
Thank you!
Since this is based on an interaction could this not be handled by the the event caused by the interaction?
const reducer = (state, action) => {
switch (action.type) {
case "setUsers":
return {
users: action.payload
};
default:
return state;
}
};
const Example = () => {
const dispatch = useDispatch();
const users = useSelector(state => state.users)
useEffect(() => {
const asyncFunc = async () => {
const apiUsers = await getUsersFromApi();
dispatch({ type: "setUsers", payload: apiUsers });
};
// Load user data from the api and store in Redux.
// Only do this on component load.
asyncFunc();
}, [dispatch]);
const onClick = async () => {
// On interaction (this case a click) get updated users.
const userDataToMerge = await getUpdatedUserData();
// merge users and assign to the store.
if (!users) {
dispatch({ type: "setUsers", payload: userDataToMerge });
return;
}
const mergedUserData = users.map(existingUser => {
const matchedUser = action.payload.find(user => user.name === existingUser.name);
if (matchedUser) {
existingUser.stats = user.stats;
}
return existingUser;
});
dispatch({ type: "setUsers", payload: mergedUserData });
}
return (
<div onClick={onClick}>
This is a placeholder
</div>
);
}
OLD ANSWER (useState)
setUsers can also take a callback function which is provided the current state value as it's first parameter: setUsers(currentValue => newValue);
You should be able to use this to avoid putting users in the dependency array of your useEffect.
Example:
useEffect(() => {
setUsers(currentUsers => {
if(currentUsers === undefined) {
return userDataFromApi;
} else {
//Users already exist in state
const mergedUserData = currentUsers.map(existingUser => {
const matchedUser = userDataFromApi.find(user => user.name === existingUser.name);
if (matchedUser) {
existingUser.stats = user.stats;
}
return existingUser;
});
return mergedUserData;
}
});
}, [setUsers, userDataFromApi]);

Combining observables in series & parallel to fetch data from multiple APIs

I am trying to check the validity of a function I have written in Typescript, in congruence with RxJS observables, that fetches some bookings from one service and then for each booking fetches its corresponding location and activity from another service.
I am simply writing this post to verify the validity of what I have written and to ask if there is anything I could have done more efficiently.
let params = new HttpParams();
params = params.append('status', 'C');
params = params.append('offset', offset.toString());
params = params.append('limit', limit.toString());
return this.http.get(`${this.environment.booking.url}/my/bookings`, { params }).pipe(
mergeMap((bookings: Booking[]) => {
if(bookings.length > 0) {
return forkJoin(
bookings.map((booking: Booking) =>
forkJoin(
of(booking),
this.activityService.getActivity(booking.activity),
this.locationService.getLocation(booking.finalLocation),
).pipe(
map((data: [ Booking, Activity, Location ]) => {
let booking = data[0];
booking.activityData = data[1];
booking.finalLocationData = data[2];
return booking;
})
)
)
)
}
return of([]);
}),
catchError((err: HttpErrorResponse) => throwError(err))
);
I am expecting for this function to return a list of bookings alongside their corresponding location and activity. However more importantly I want to verify that what I am doing is correct and sensible. Is there anything I could have done differently to make it cleaner/ more human-readable (not nit-picking, please 😁 )?
On a different note, that of performance, I also have a follow-up question with regards to performance. Given that a list of bookings has common activities and locations. Is there a way to only fetch activities and locations without any duplicate HTTP requests? Is this already handled under the hood by RxJS? Is there anything I could have done to make this function more efficient?
I'm not sure about the efficiency, but, at least for me, it was a little hard to read
Here's how I'd do it:
I used a dummy API, but I think it correlates with your situation
const usersUrl = 'https://jsonplaceholder.typicode.com/users';
const todosUrl = 'https://jsonplaceholder.typicode.com/todos';
const userIds$ = of([1, 2, 3]); // Bookings' equivalent
userIds$
.pipe(
filter(ids => ids.length !== 0),
// Flatten the array so we can avoid another nesting level
mergeMap(ids => from(ids)),
// `concatMap` - the order matters!
concatMap(
id => forkJoin(ajax(`${usersUrl}/${id}`), ajax(`${todosUrl}/${id}`))
.pipe(
map(([user, todo]) => ({ id, user: user.response, todo: todo.response }))
)
),
toArray()
)
.subscribe(console.log)
Here is a StackBlitz demo.
With this in mind, here is how I'd adapt it to your problem:
this.http.get(`${this.environment.booking.url}/my/bookings`, { params }).pipe(
filter(bookings => bookings.length !== 0),
// Get each booking individually
mergeMap(bookings => from(bookings)),
concatMap(
b => forkJoin(
this.activityService.getActivity(b.activity),
this.locationService.getLocation(b.finalLocation),
)
.pipe(
map(([activity, location]) => ({ ...b, activity, location }))
)
),
// Getting the bookings array again
toArray()
catchError((err: HttpErrorResponse) => throwError(err))
);
This is how I would tackle this using RxJS:
Fetch all the Bookings
For Each Booking fetch Location and Activities cuncurrently
const { from, of, forkJoin, identity } = rxjs;
const { mergeMap, tap, catchError } = rxjs.operators;
const api = 'https://jsonplaceholder.typicode.com';
const endpoints = {
bookings: () => `${api}/posts`,
locations: (id) => `${api}/posts/${id}/comments`,
activities: (id) => `${api}/users/${id}`
};
const fetch$ = link => from(fetch(link)).pipe(
mergeMap(res => res.json()),
catchError(() => from([])),
);
fetch$(endpoints.bookings()).pipe(
mergeMap(identity),
mergeMap(booking => forkJoin({
booking: of(booking),
locations: fetch$(endpoints.locations(booking.id)),
activities: fetch$(endpoints.activities(booking.userId)),
})),
).subscribe(console.log);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.3/rxjs.umd.js" integrity="sha256-Nihli32xEO2dsnrW29M+krVxoeDblkRBTkk5ZLQJ6O8=" crossorigin="anonymous"></script>
note:
Reactive Programming, and more generically declarative approaches, focus on avoiding imperative control flows... You should try to write your pipes without conditions (or any other control flow). To discard empty bookings you can use the filter operator.
Avoid nesting streams because this comes at the cost of readability.
forkJoin also takes a spec object which is very useful (part of the overloads)

Parent-child process messaging with result

I want to isolate some login inside of a child process. The idea is pretty simple:
I wait for some event inside of master process
send a message to child process
if child process are able to handle it, then receive a result
if child process fails then, log erros and fork a new process
The problem here: messaging. So, I wrote a prototype solution bellow:
const minion = fork('./minion')
const setupSend = (emmiter) => {
const pool = {}
emmiter.on('message', ({id, msg}) => {
pool[id](msg)
delete pool[id]
})
const send = (msg) => {
const id = getId()
const refObj = {}
const p = new Promise((resolve) => {
refObj.resolve = resolve
})
pool[id] = refObj.resolve
emmiter.send({id , msg})
return p
}
return send
}
const send = setupSend(minion)
send('message to reverse').then((result) => {
console.log(result)
})
and sample minion code:
process.on('message', ({id, msg}) => {
process.send({id, msg: msg.split("").reverse().join("")})
});
It works but it doesn't handle the errors and exit cases. probably I will manage to write all the required logic, but it feels like I am inventing a wheel.
So, is there an easier way to achieve this functionality?

Functions in epic are firing out of order and I can't understand why

*SOLVED
The problem was in how I was creating and responding to the observable created by the firebase callback.
I also had way too much stuff going on inside my firebase callbacks.
I ended up splitting it up a bit more, using the firebase promise structure: https://firebase.googleblog.com/2016/01/keeping-our-promises-and-callbacks_76.html
and creating an Observable.fromPromise for the firebase callback within what is now called firebaseAPI.checkForUser.
*
Working epic:
export const contactFormFirebaseSubmitEpic = (action$) =>
action$.ofType(START_CONTACT_FORM_FIREBASE_SUBMIT)
.flatMap((firebaseSubmitAction) => {
const values = firebaseSubmitAction.values;
const formattedEmail = firebaseAPI.getFormattedEmail(values);
const contactsRef = firebaseAPI.getContactsRef(formattedEmail);
return firebaseAPI.checkForUser(values, formattedEmail, contactsRef);
})
.flatMap((data) => concat(
of(firebaseAPI.recordUserAndUpdateDetails(data))
))
.flatMap((data) => concat(
of(firebaseAPI.setQuoteData(data))
))
.switchMap((x) => merge(
of(stopLoading()),
of(contactFormFirebaseSuccess())
));
// original question
Ok so, what I'm trying to achieve is to perform the first action (firebaseAPI.checkUserAndUpdate), then the next, then when both of them are done essentially discard what's there and send out two actions (contactFormFirebaseSuccess and stopLoading).
This all works fine except for one weird thing, the setQuoteData function always runs before the checkUser function. Does anyone know why this might be?
Also if there's a better way to lay this out I'd be very open to suggestions! Cheers. Also I've taken out quite a few variables and things that would make it even more complicated. Basically I just wanted to show that in each case I'm returning an observable from 'doing something with firebase'. But I don't think that's the problem as I have console logs in each of the firebase functions and the setQuoteData one just fires first and then executes the firebase stuff then when it's done the checkUserAndUpdate one runs.
export const contactFormFirebaseSubmitEpic = action$ =>
action$.ofType(START_CONTACT_FORM_FIREBASE_SUBMIT)
.flatMap((firebaseSubmitAction) => {
const values = firebaseSubmitAction.values;
return merge(
firebaseAPI.checkUserAndUpdate(values),
firebaseAPI.setQuoteData(values),
)
.takeLast(1)
.mergeMap((x) => {
return merge(
of(contactFormFirebaseSuccess()),
of(stopLoading()),
);
});
});
const firebaseAPI = {
checkUserAndUpdate: (values) => {
const checkUserAndUpdateDetails = firebaseRef.once('value', snapshot => {
const databaseValue = snapshot.val();
checkUserExistsAndUpdateDetails(databaseValue, values);
});
return Observable.from(checkUserAndUpdateDetails);
},
setQuoteData: (value) => {
const setQuote = setQuoteData(values);
return Observable.from(setQuote);
},
};
const stopLoading = () => ({ type: STOP_BUTTON_LOADING });
const contactFormFirebaseSuccess = () => ({ type: SUCCESS });
checkUserAndUpdate: (values, contactsRef) => {
const checkUser$ = Observable.from(contactsRef.once('value').then(
snapshot => {
const databaseValue = snapshot.val();
checkUserExistsAndUpdateDetails(
values,
databaseValue,
contactsRef,);
})
);
return checkUser$;
},
const checkUserExistsAndUpdateDetails = (
values,
databaseValue,
contactsRef,
) => {
if (databaseValue) { console.log('user exists'); }
else {
console.log('new user, writing to database');
contactsRef.set({
name: values.name,
email: values.email,
phone: values.phone,
});
}
};
The problem is that merge does not maintain the order of the streams that you subscribe to, it simply emits events from any of the source streams regardless of what order they emit.
If you need to maintain order you should use concat instead of merge
i.e.
const values = firebaseSubmitAction.values;
return concat(
firebaseAPI.checkUserAndUpdate(values),
firebaseAPI.setQuoteData(values),
)
Side note, I don't know why you are using the of operator there, you already have Observables returned from your API so you can just pass those to merge or concat in this case.

Categories