How to emit multiple actions in one epic using redux-observable? - reactjs

I'm new to rxjs/redux observable and want to do two things:
1) improve this epic to be more idiomatic
2) dispatch two actions from a single epic
Most of the examples I see assume that the api library will throw an exception when a fetch fails, but i've designed mine to be a bit more predictable, and with Typescript union types I'm forced to check an ok: boolean value before I can unpack the results, so understanding how to do this in rxjs has been a bit more challenging.
What's the best way to improve the following? If the request is successful, I'd like to emit both a success action (meaning the user is authorized) and also a 'fetch account' action, which is a separate action because there may be times where I need to fetch the account outside of just 'logging in'. Any help would be greatly appreciated!
const authorizeEpic: Epic<ActionTypes, ActionTypes, RootState> = action$ =>
action$.pipe(
filter(isActionOf(actions.attemptLogin.request)),
switchMap(async val => {
if (!val.payload) {
try {
const token: Auth.Token = JSON.parse(
localStorage.getItem(LOCAL_STORAGE_KEY) || ""
);
if (!token) {
throw new Error();
}
return actions.attemptLogin.success({
token
});
} catch (e) {
return actions.attemptLogin.failure({
error: {
title: "Unable to decode JWT"
}
});
}
}
const resp = await Auth.passwordGrant(
{
email: val.payload.email,
password: val.payload.password,
totp_passcode: ""
},
{
url: "localhost:8088",
noVersion: true,
useHttp: true
}
);
if (resp.ok) {
return [
actions.attemptLogin.success({
token: resp.value
})
// EMIT action 2, etc...
];
}
return actions.attemptLogin.failure(resp);
})
);

The docs for switchMap indicate the project function (the lambda in your example) may return the following:
type ObservableInput<T> = SubscribableOrPromise<T> | ArrayLike<T> | Iterable<T>
When a Promise<T> is returned, the resolved value is simply emitted. In your example, if you return an array from your async scope, the array will be sent directly to the Redux store. Assuming you have no special Redux middlewares setup to handle an array of events, this is likely not what you want. Instead, I would recommend returning an observable in the project function. It's a slight modification to your example:
const authorizeEpic: Epic<ActionTypes, ActionTypes, RootState> = action$ =>
action$.pipe(
filter(isActionOf(actions.attemptLogin.request)), // or `ofType` from 'redux-observable'?
switchMap(action => {
if (action.payload) {
try {
const token: Auth.Token = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY) || "")
if (!token) {
throw new Error()
}
// return an observable that emits a single action...
return of(actions.attemptLogin.success({
token
}))
} catch (e) {
// return an observable that emits a single action...
return of(actions.attemptLogin.failure({
error: {
title: "Unable to decode JWT"
}
}))
}
}
// return an observable that eventually emits one or more actions...
return from(Auth.passwordGrant(
{
email: val.payload.email,
password: val.payload.password,
totp_passcode: ""
},
{
url: "localhost:8088",
noVersion: true,
useHttp: true
}
)).pipe(
mergeMap(response => response.ok
? of(
actions.attemptLogin.success({ token: resp.value }),
// action 2, etc...
)
: of(actions.attemptLogin.failure(resp))
),
)
}),
)
I don't have your TypeScript type definitions, so I can't verify the example above works exactly. However, I've had quite good success with the more recent versions of TypeScript, RxJS, and redux-observable. Nothing stands out in the above that makes me think you should encounter any issues.

You could zip your actions and return them.
zip(actions.attemptLogin.success({
token: resp.value
})
// EMIT action 2, etc...
So that, now both your actions will be called.

Related

How do i stop a dependency from re-rendering infinite times in a useEffect?

I have a react-select multiselect component which is required to have preselected values based on the user role. The react-select component is used to filter data in a react-table.
I have 2 user roles - Dev and tester.
If it the dev, I need to have open and Reopened issues to be filtered on load
If it is a tester, I need to have resolved issues on load
This is a part of the code that i am using to achieve preselected
async function loadInfo() {
const body = {
project_id: projDetails.id,
};
const response = await axios({
method: "post",
url: apilist.dropdownData,
data: body,
})
.catch((err) => console.log(err));
if (response) {
const getData = response.data.data;
// console.log("IsGeneralInfo:", getData)
setGeneralInfo(getData);
let filteredenv = getData.status.filter((item) => item.id == 8 || item.id == 6)
let envfiltered = filteredenv.map((k) => {
return ({ label: k.values, value: k.values });
})
// console.log("envfilter", envfiltered);
// handleMultiStatusFilter(envfiltered);
}
}
// const {current:myArray}=useRef([{ label: 'Closed', value: 'Closed' }])
useEffect(() => {
if(envfilter){
let myArray=[{ label: 'Closed', value: 'Closed' },{ label: 'Reopen', value: 'Reopen' }];
handleMultiStatusFilter(myArray);
}
}, [selectedOptions])
const handleStatusFilter = (e) => {
setFilterValue(e);
if (e.length > 0) {
dispatch(filterByValue({ type: e, viewIssue: viewIssue, }))
}
else {
dispatch(showAllStatus({ type: 'All', viewIssue: viewIssue, }))
}
}
const handleMultiStatusFilter = (e) => {
setFiltered([])
let arr = []
e.map((data) => {
setFiltered(prevState => [...prevState, data.value]);
arr.push(data.value);
})
setSelectedOptions(e)
handleStatusFilter(arr)
}
This is a part of the redux code used for filtering
extraReducers: (builder) => {
// Add reducers for additional action types here, and handle loading state as needed
builder.addCase(fetchIssueList.fulfilled, (state, action) => {
// Add user to the state array
state.issuesList = {
status: 'idle',
data: action.payload.data.data !== undefined ? action.payload.data.data : [],
dataContainer: action.payload.data.data !== undefined ? action.payload.data.data : [],
no_of_records: action.payload.data.data !== undefined ? action.payload.data.data.length : 0,
error: {}
}
})
The code works fine with the filtering once i login, but the rerendering keeps going to infinite loop
Is there any way i could stop the infinite rerendering of code and have the filtering happen on load of the screen?
while working with dependencies in useEffect, try to use the most primitive part you can find. no complex objects, as they change way too fast.
for example: use the length of an array not the array itself.
even though for arrays it's mostly safe to use itself.
sorry. correction: for arrays it's not safe either. complex objects are compared by reference not by value. for that you need primitive types like number, string or boolean.

Redux Query Dependent Mutations With Rollback Using createApi()

I have two mutations that need to happen one after another if the first one succeeds. As bonus I would like to undo the first mutation if the second fails.
I have the first part working but it feels clumsy and I'm wondering if there is better way.
Here is how I bring in my two mutations:
const [updateSubscription, {
isLoading: isLoadingUpdateSubscription,
isSuccess: isSuccessUpdatedSubscription,
isError: isErrorUpdateSubscription,
error: errorUpdateSubscription,
reset: resetUpdateSubscription
}] = useUpdateSubscriptionMutation();
const [updateDeviceListing, {
isLoading: isLoadingUpdateDeviceListing,
isSuccess: isSuccessUpdatedDeviceListing,
isError: isErrorUpdateDeviceListing,
error: errorUpdateDeviceListing,
reset: resetUpdateDeviceListing
}] = useUpdateDeviceListingMutation();
As part of a button click I run the first mutation where a subscription is updated:
const handleListIt = () => {
if (deviceListing && subscriptions) {
const updatedAvailableSeats = subscriptions[0].availableSeats - deviceListing.count;
const updatedUsedSeats = subscriptions[0].usedSeats + deviceListing.count;
updateSubscription({...subscriptions[0], availableSeats: updatedAvailableSeats, usedSeats: updatedUsedSeats});
}
};
I then use useEffect() to check isSuccessUpdatedSubscription and run the second mutation:
useEffect(() => {
if (isSuccessUpdatedSubscription && deviceListing) {
updateDeviceListing({...deviceListing, status: 'open'})
}
if (isSuccessUpdatedDeviceListing) {
onClosed();
}
}, [isSuccessUpdatedDeviceListing, isSuccessUpdatedSubscription, deviceListing, onClosed, updateDeviceListing]);
The same useEffect() is also used to check if the second mutation worked, isSuccessUpdatedDeviceListing, at which point onClosed() is called and the user is shown some different UI.
You can just handle both in handleListIt, there is really no good reason for the re-render with the useEffect. And then you can also handle the rollback as you want.
const handleListIt = async () => {
if (deviceListing && subscriptions) {
const updatedAvailableSeats = subscriptions[0].availableSeats - deviceListing.count;
const updatedUsedSeats = subscriptions[0].usedSeats + deviceListing.count;
try {
await updateSubscription({...subscriptions[0], availableSeats: updatedAvailableSeats, usedSeats: updatedUsedSeats}).unwrap();
await updateDeviceListing({...deviceListing, status: 'open'}).unwrap()
} catch (error) {
// do your rollback
}
}
};

Returning the result from dispatch to a component throws "undefined" exception

I have the following method inside the data-operations tsx file:
export function EnrollStudent(student, courseId) {
return dispatch => {
dispatch(enrollUserBegin());
axios
.post("api/Course/EnrollStudent/" + courseId + "/" + student.id)
.then(response => {
if (response.data === 1) {
dispatch(enrollUserSuccess(student, courseId));
console.log("Student is enrolled.");
toast.success("Student is enrolled successfully.", {
position: toast.POSITION.TOP_CENTER
});
} else {
dispatch(enrollUserSuccess(null, 0));
console.log("Failed to enroll.");
toast.warn(
"Failed to enroll the student. Possible reason: Already enrolled.",
{
position: toast.POSITION.TOP_CENTER
}
);
}
})
.catch(error => {
dispatch(enrollUserFailure(error));
toast.warn("An error occurred. Please contact the Admin.", {
position: toast.POSITION.TOP_CENTER
});
});
};
}
This method is called from another component by a button click:
const enroll_onClick = e => {
e.preventDefault();
const currentUserId = authenticationService.currentUserValue["id"];
var student: any = { id: currentUserId };
props.enrollStudent(student, st_courseId);
};
const mapDispatchToProps = dispatch => {
return {
enrollStudent: (student, courseId) => dispatch(EnrollStudent(student, courseId)),
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(CourseEnrollmentWithCode);
This works fine and the database is updated properly. But, I want to get the result of the enrollStudentand perform an action (e.g., navigate to another page).
I tried this but I received props.enrollStudent() is undefined error.
props.enrollStudent(student, st_courseId)
.then(() => {
console.log("enrolld");
history.push('/courses');
});
Any suggestions?
By looking at the code, I'm guessing you are using the redux-thunk middleware.
In the EnrollStudent function, you have to add a return statement before axios (line 5) so that your thunk function (the function returned by EnrollStudent) returns a promise.
Take a look at the examples here: https://github.com/reduxjs/redux-thunk, especially the makeASandwichWithSecretSauce function.
You must not use promises in action creators ... you can only return an action object ... https://redux.js.org/api/store/
In this case (side effects) you have to use a middleware (redux-thunk, redux-promise, redux-saga) ... https://redux.js.org/api/applymiddleware/
If middleware not used ... current behaviour (looking as partially working) is at least missleading.

Object is possibly undefined even with if statement

I've got a Typescript error TS2532: Object is possibly 'undefined'. I'm creating an SPA using Typescript, and on one of the pages, then you have the possibility of uploading a file. The backend is already written, so the file stored in state will be made into a binary code which the server side reads. The file setstate is set to undefined.
I've done a few searches, but the solutions don't seem to work for me. The first solution was to create an if statement:
if (this.state.uploadFile !== undefined) {
const terms = this.state.uploadFile;
// Logic
}
Whenever terms is used in the logic portion, then I get the error mentioned above.
The other solution is to tell the compiler that it is definetly defined:
const terms = this.state!.uploadFile;
This still throws the same error, and thought I might have placed the ! in the wrong place, but when I move it to const terms = this.state.termSheet!; then that causes a new error when calling terms.getAsBinary() I get the error Property 'getAsBinary' does not exist on type 'never'
Code in context:
// Imports
class SubmitIssue extends React.Component<StylesProps> {
state = {
alert: false,
amount: '',
uploadFile: undefined,
verify: false,
}
handleAmountChange(e) {
this.setState({ amount: e.target.value });
}
handleFileChange(e) {
this.setState({ uploadFile: e.target.files[0] });
}
handleVerifyChange() {
this.setState({ verify: !this.state.verify });
}
handleClick() {
const config = { headers: { 'Content-Type': 'multipart/form-data' } };
const bodyFormData = new FormData();
bodyFormData.set('Amount', this.state.amount);
bodyFormData.set('uploadFile', this.state.termSheet.getAsBinary());
bodyFormData.set('Verify', this.state.verify.toString());
axios.post(
`${process.env.API_URL}RegisterIssue/Create`,
bodyFormData,
config
).then(res => {
console.log(res);
this.setState({ alert: true }, function() { this.sendData(); });
}).catch((error) => {
console.log(error);
})
}
sendData() {
const alertState = this.state.alert;
this.props.parentAlertCallback(alertState);
}
render() {
return (
// Design
);
}
}
export default withStyles(styles)(SubmitIssue);
So, I'm a little stumped as to what the correct way to handle this error is.
It is because you check only state.uploadFile but use state.termSheet.
There are two possible solutions:
You cat set a default value (in case terms is not defined):
const terms = this.state.termSheet ? this.state.termSheet : defaultValue;
Or you check the terms in the if statement, too:
if (this.state.uploadFile && this.state.termSheet)

Cannot read property 'then' of undefined on Redux thunk action

I'm new to React, Redux and Thunk and have been following tutorials on the topic, most recently Building Applications with React and Redux in ES6 on Pluralsight. I've been transposing this tutorial from ES6 to Typescript 3 and React v3 to React v4.
I'm come across a lot of issues that I've been able to resolve following this pattern (most notably anything routing related) but I've come across an issue I can't resolve. From various sources inc Cannot read property '.then' of undefined when testing async action creators with redux and react
it sounds like I can't have a .then() function on a function that returns a void but it's an async function that (is supposed to) return a promise, however the intellisense says otherwise. Code below.
saveCourse = (event: any) => {
event.preventDefault();
this.props.actions.saveCourse(this.state.course)
.then(this.setState({ fireRedirect: true }));
}
The above code is on my component, props.actions are connected to the redux store via mapStateToProps and this function is called on the button click. It's the .then() on this above function that is erroring. This function calls the action below
export const saveCourse = (course: Course) => {
return (dispatch: any, getState: any) => {
dispatch(beginAjaxCall());
courseApi.saveCourse(course).then((savedCourse: Course) => {
course.id ? dispatch(updateCourseSuccess(savedCourse)) :
dispatch(createCourseSuccess(savedCourse));
}).catch(error => {
throw (error);
});
}
}
The above action is the asyc call, the .then() here is not erroring but VS Code says the whole saveCourse function returns void.
From the tutorial there really aren't any differences that should make a difference (arrow functions instead of regular etc...) so I'm wondering if there's an obscure change between versions I'm missing but not sure where to look and could be missing something fundamental. Can anyone see why I can't do a .then() on the saveCourse() function?
Let me know if you need anymore information.
EDIT: the courseApi is just a mock api, code below.
import delay from './delay';
const courses = [
{
id: "react-flux-building-applications",
title: "Building Applications in React and Flux",
watchHref: "http://www.pluralsight.com/courses/react-flux-building-applications",
authorId: "cory-house",
length: "5:08",
category: "JavaScript"
},
{
id: "clean-code",
title: "Clean Code: Writing Code for Humans",
watchHref: "http://www.pluralsight.com/courses/writing-clean-code-humans",
authorId: "cory-house",
length: "3:10",
category: "Software Practices"
},
{
id: "architecture",
title: "Architecting Applications for the Real World",
watchHref: "http://www.pluralsight.com/courses/architecting-applications-dotnet",
authorId: "cory-house",
length: "2:52",
category: "Software Architecture"
},
{
id: "career-reboot-for-developer-mind",
title: "Becoming an Outlier: Reprogramming the Developer Mind",
watchHref: "http://www.pluralsight.com/courses/career-reboot-for-developer-mind",
authorId: "cory-house",
length: "2:30",
category: "Career"
},
{
id: "web-components-shadow-dom",
title: "Web Component Fundamentals",
watchHref: "http://www.pluralsight.com/courses/web-components-shadow-dom",
authorId: "cory-house",
length: "5:10",
category: "HTML5"
}
];
function replaceAll(str: any, find: any, replace: any) {
return str.replace(new RegExp(find, 'g'), replace);
}
//This would be performed on the server in a real app. Just stubbing in.
const generateId = (course: any) => {
return replaceAll(course.title, ' ', '-');
};
class CourseApi {
static getAllCourses() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(Object.assign([], courses));
}, delay);
});
}
static saveCourse(course: any) {
course = Object.assign({}, course); // to avoid manipulating object passed in.
return new Promise((resolve, reject) => {
setTimeout(() => {
// Simulate server-side validation
const minCourseTitleLength = 1;
if (course.title.length < minCourseTitleLength) {
reject(`Title must be at least ${minCourseTitleLength} characters.`);
}
if (course.id) {
const existingCourseIndex = courses.findIndex(a => a.id == course.id);
courses.splice(existingCourseIndex, 1, course);
} else {
//Just simulating creation here.
//The server would generate ids and watchHref's for new courses in a real app.
//Cloning so copy returned is passed by value rather than by reference.
course.id = generateId(course);
course.watchHref = `http://www.pluralsight.com/courses/${course.id}`;
courses.push(course);
}
resolve(course);
}, delay);
});
}
static deleteCourse(courseId: any) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const indexOfCourseToDelete = courses.findIndex(course =>
course.id == courseId
);
courses.splice(indexOfCourseToDelete, 1);
resolve();
}, delay);
});
}
}
export default CourseApi;
The issue you have is that you're not returning the promise in your Action Creator.
Have a look at the example for redux-thunk here:
https://github.com/reduxjs/redux-thunk
function makeASandwichWithSecretSauce(forPerson) {
// Invert control!
// Return a function that accepts `dispatch` so we can dispatch later.
// Thunk middleware knows how to turn thunk async actions into actions.
return function (dispatch) {
return fetchSecretSauce().then(
sauce => dispatch(makeASandwich(forPerson, sauce)),
error => dispatch(apologize('The Sandwich Shop', forPerson, error))
);
};
}
They're returning the promise generated by fetchSecretSauce.
You need to similarly return the promise in your example:
export const saveCourse = (course: Course) => {
return (dispatch: any, getState: any) => {
dispatch(beginAjaxCall());
return courseApi.saveCourse(course).then((savedCourse: Course) => {
course.id ? dispatch(updateCourseSuccess(savedCourse)) :
dispatch(createCourseSuccess(savedCourse));
}).catch(error => {
throw (error);
});
}
}
Other than returning the promise in your Action Creator, you might also want to make sure that in your mapDispatchToProps, the corresponding function mapped should be declared async. It may await the dispatch statement inside.
I ran into a similar issue with exactly the same error message, but other than returning the promise, this is something else that I didn't notice in the first place.

Resources