Search code examples
reactjsreduxreact-reduxredux-thunk

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;

Solution

  • 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);
            });
        }
    }