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

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.

Related

How to effectively do optimistic update for deeply nested data in react query?

So I'm making a kanban board style task manager using react and react query. My current implementation of the data fetching is like the following:
const { data } = useQuery('listCollection', getListCollection)
and the content of data is something like this:
// data
{
listOrder: number[]
lists: IList[]
}
interface IList {
id: number
title: string
todoOrder: number[]
todos: ITodo[]
}
interface ITodo {
id: number
text: string
checked: boolean
}
So basically a list collection contains multiple lists and each lists contain multiple todos.
Now, I want this application to do optimistic update on each mutation (add a new todo, delete, check a todo, etc).
Here is my current implementation of optimistic update when toggling a todo check:
const mutation = useMutation(
(data: { todoId: number; checked: boolean }) =>
editTodo(data),
{
onMutate: async (data) => {
await queryClient.cancelQueries('listCollection')
const previousState = queryClient.getQueryData('listCollection')
queryClient.setQueryData('listCollection', (prev: any) => ({
...prev,
lists: prev.lists.map((list: IList) =>
list.todoOrder.includes(data.todoId)
? {
...list,
todos: list.todos.map((todo) =>
todo.id === data.todoId
? { ...todo, checked: data.checked }
: todo,
),
}
: list,
),
}))
return { previousState }
},
onError: (err, newTodo, context) => {
queryClient.setQueryData(parent, context?.previousState)
},
onSuccess: () => queryClient.invalidateQueries(parent),
},
)
As you can see, that's overly complicated. How should I approach this?
The best way to update the deeply nested data in react query is by using "Immer" library. It is very light weight and it uses proxies to change the reference of only updated data, and reducing the cost of rendering for non-updated data.
import produce from "immer";
const mutation = useMutation(
(data: { todoId: number; checked: boolean }) =>
editTodo(data),
{
onMutate: async (data) => {
await queryClient.cancelQueries('listCollection')
const previousState = queryClient.getQueryData('listCollection')
const updData = produce(previousState, (draftData) => {
// Destructing the draftstate of data.
let {lists} = draftData;
lists = lists.map((list: IList) => {
// Mapping through lists and checking if id present in todoOrder and todo.
if(list.todoOrder.includes(data.todoId) && list.todo[data.id]){
list.todo[data.id].checked = data.checked;
}
return list.
}
// Updating the draftstate with the modified values
draftData.lists = lists;
}
// Setting query data.
queryClient.setQueryData("listCollection", updData);
return { previousState }
},
onError: (err, newTodo, context) => {
queryClient.setQueryData(parent, context?.previousState)
},
onSuccess: () => queryClient.invalidateQueries(parent),
},
)
This will solve your case. You can modify the listOrder if needed just the way I updates lists.

Why my setState returns an array, but the state is a promise when the component rerender?

The code below try to check if an url is reachable or not.
The urls to check are stored in a state called trackedUrls
I update this state with an async function checkAll.
The object just before being updated seems fine, but when the component rerender, it contains a promise !
Why ?
What I should change to my code ?
import React from "react"
export default function App() {
const [trackedUrls, setTrackedUrls] = React.useState([])
// 1st call, empty array, it's ok
// 2nd call, useEffect populate trackedUrls with the correct value
// 3rd call, when checkAll is called, it contains a Promise :/
console.log("trackedUrls :", trackedUrls)
const wrappedUrls = trackedUrls.map(urlObject => {
return (
<div key={urlObject.id}>
{urlObject.label}
</div>
)
})
// check if the url is reachable
// this works well if cors-anywhere is enable, click the button on the page
async function checkUrl(url) {
const corsUrl = "https://cors-anywhere.herokuapp.com/" + url
const result = await fetch(corsUrl)
.then(response => response.ok)
console.log(result)
return result
}
// Checks if every url in trackedUrls is reachable
// I check simultaneously the urls with Promise.all
async function checkAll() {
setTrackedUrls(async oldTrackedUrls => {
const newTrackedUrls = await Promise.all(oldTrackedUrls.map(async urlObject => {
let isReachable = await checkUrl(urlObject.url)
const newUrlObject = {
...urlObject,
isReachable: isReachable
}
return newUrlObject
}))
// checkAll works quite well ! the object returned seems fine
// (2) [{…}, {…}]
// { id: '1', label: 'google', url: 'https://www.google.Fr', isReachable: true }
// { id: '2', label: 'whatever', url: 'https://qmsjfqsmjfq.com', isReachable: false }
console.log(newTrackedUrls)
return newTrackedUrls
})
}
React.useEffect(() => {
setTrackedUrls([
{ id: "1", label: "google", url: "https://www.google.Fr" },
{ id: "2", label: "whatever", url: "https://qmsjfqsmjfq.com" }
])
}, [])
return (
<div>
<button onClick={checkAll}>Check all !</button>
<div>
{wrappedUrls}
</div>
</div>
);
}
Konrad helped me to grasp the problem.
This works and it's less cumbersome.
If anyone has a solution with passing a function to setTrackedUrls, I'm interested just for educational purpose.
async function checkAll() {
const newTrackedUrls = await Promise.all(trackedUrls.map(async urlObject => {
let isReachable = await checkUrl(urlObject.url)
const newUrlObject = {
...urlObject,
isReachable: isReachable
}
return newUrlObject
}))
setTrackedUrls(newTrackedUrls)
}
You can only put data into setState.

How can i auto refresh or render updated table data form database in material UI table after doing any operation in React?

Here useVideos() give us all videos form database. After adding a new video the new entry is not append in the Material UI table , but if I refresh the page then it's showing that new entry. Now I want to show this new entry after add operation. Please help me to do this! Thanks in Advance.
const initialState = [{}];
const reducer = (state, action) => {
switch (action.type) {
case "videos":
const data = [];
let cnt = 1;
action.value.forEach((video, index) => {
data.push({
sl: cnt,
noq: video.noq,
id: index,
youtubeID: video.youtubeID,
title: video.title,
});
cnt = cnt + 1;
});
return data;
default:
return state;
}
};
export default function ManageVideos() {
const { videos, addVideo, updateVideo, deleteVideo } = useVideos("");
const [videoList, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
dispatch({
type: "videos",
value: videos,
});
}, [videos]);
const columns = [
{ title: "Serial", field: "sl" },
{ title: "Title", field: "title" },
{ title: "Number of Qusetion", field: "noq" },
{ title: "Youtube Video Id", field: "youtubeID" },
];
return (
<div>
<MaterialTable
title="Videos Table"
data={videoList}
columns={columns}
editable={{
onRowAdd: (newData) =>
new Promise((resolve, reject) => {
setTimeout(() => {
addVideo(newData);
resolve();
}, 1000);
}),
onRowUpdate: (newData) =>
new Promise((resolve, reject) => {
setTimeout(() => {
updateVideo(newData);
resolve();
}, 1000);
}),
}}
/>
</div>
);
}
Since the information provided is a bit lacking, I'll assume that the useEffect hook is not working when you update your videos (check it with consle.log("I'm not working") and if it does work then you can just ignore this answer).
You can define a simple state in this component, let's call it reRender and set the value to 0, whenever the user clicks on the button to add a video you should call a function which adds 1 to the value of reRender (()=>setReRender(prevState=>prevState+1)). In your useEffect hook , for the second argument pass reRender. This way, when the user clicks to submit the changes , reRender causes useEffect to run and dispatch to get the new data.
If this doesn't work , I have a solution which takes a bit more work. You will need to use a state manager like redux or context api to store your state at a global level. You should store your videos there and use 1 of the various ways to access the states in this component (mapStateToProps or store.subscribe() or ...). Then pass the video global state as the second argument to useEffect and voilà, it will definitely work.

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.

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

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.

Resources