Search code examples
reactjsreduxreact-reduxredux-thunk

Uncaught Error: Actions must be plain objects. Instead, the actual type was: 'undefined'


I'm building a web application Reactjs and Redux, where you can login to dashboard and then you can logout. I'm using "redux-react-session" to manage sessions, and i created an action called logoutUser in userActions.js. this action is imported and called in Dashboard.jsx where i added a logout button with onClick event. What i'm expecting is when i click on logout it should simply redirects me to home page ('/')

My problem is: Whenever i click on logout button, it redirects me to home page ('/') then refreshing back to dashboard.

Console.dev error:

Uncaught Error: Actions must be plain objects. Instead, the actual type was: 'undefined'. You may need to add middleware to your store setup to handle dispatching other values, such as 'redux-thunk' to handle dispatching functions.

userAction.js:

import axios from 'axios'
import { sessionService } from 'redux-react-session'


export const loginUser = (credentials, navigate, setFieldError, setSubmitting) => {
    //make checks and get some data
    return () => {
        axios.post("http://localhost:5000/api/form/signin",
            credentials,
            {
                headers: {
                    "Content-Type": "application/json"
                }
            }
        ).then((response) => {
            const { data } = response;

            if (data.status === "FAILED") {
                const { message } = data;

                //check for specific error
                if (message.includes("credentials")) {
                    setFieldError("email", message);
                    setFieldError("password", message);
                } else if (message.includes("password")) {
                    setFieldError("password", message);
                    alert("Wrong email or password")
                }
            } else if (data.status === "SUCCESS") {
                const userData = data.data;
                const token = userData._id;

                sessionService.saveSession(token)
                    .then(() => {
                        sessionService.saveUser(userData)
                            .then(() => {
                                if (userData.role === "admin") {
                                    navigate("/dashboard")
                                } else if (userData.role === "user") {
                                    navigate("/joblist")
                                } else {
                                    navigate("/")
                                    alert("Role is not known, please contact Admin or make a new account!")
                                    sessionService.deleteSession(token)
                                }
                            })
                            .catch(err => console.error(err))
                    })
                    .catch(err => console.error(err))
            }
        })
            .catch(err => { console.error(err) });
    }
}




export const signupUser = (credentials, navigate, setFieldError, setSubmitting) => {
    return (dispatch) => {

        axios.post("http://localhost:5000/api/form/signup",
            credentials,
            {
                headers: {
                    "Content-Type": "application/json"
                }
            }
        ).then((response) => {
            const { data } = response;

            if (data.status === 'FAILED') {
                const { message } = data;
                //check for specific error
                if (message.includes("name")) {
                    setFieldError("name", message);
                } else if (message.includes("email")) {
                    setFieldError("email", message);
                } else if (message.includes("date")) {
                    setFieldError("dateOfBirth", message);
                } else if (message.includes("password")) {
                    setFieldError("password", message);
                }

                //complete submission
                setSubmitting(false);

            } else if (data.status === "SUCCESS") {
                //Login user after successfull signup
                const { email, password } = credentials;

                dispatch(loginUser({ email, password }, navigate, setFieldError, setSubmitting))
            }
        })
            .catch(err => console.error(err));
    }
}




export const logoutUser = (navigate) => {
    return () => {
        sessionService.deleteSession();
        sessionService.deleteUser();
        navigate('/');
    }
}

Dashboard.jsx:

import {
  StyledTitle,
  StyledSubTitle,
  Avatar,
  StyledButton,
  ButtonGroup,
  StyledFormArea,
  colors
} from './../../components/Styles';

//Formik
import { Formik, Form } from 'formik';
import { TextInput } from '../../components/FormLib';

//Logo
import Logo from './../../assets/logo.png';

//auth & redux
import { connect } from 'react-redux';
import { logoutUser } from '../../auth/actions/userActions';

//React router
import { useNavigate } from 'react-router-dom';

import { useDispatch } from 'react-redux';




const Dashboard = ({ logoutUser }) => {
  const navigate = useNavigate();
  const dispatch = useDispatch();

  return (
    <div>
      <div>
        <Avatar image={Logo} />
      </div>
      <StyledFormArea bg={colors.dark2} style={{ marginLeft: '-400px' }}>
        <StyledTitle size={65}>Welcome to Dashboard </StyledTitle>
        <StyledSubTitle size={27}>Feel free to post new jobs</StyledSubTitle>
        <Formik
          initialValues={{
            title: '',
            description: ''
          }}
        >
          <Form>
            <TextInput
              name="title"
              type="text"
              label="Title :"
              placeholder="Job Title"
            />
            <TextInput
              name="description"
              type="text"
              label="Description :"
              placeholder="Job description"
            />
          </Form>
        </Formik>
        <ButtonGroup>
          <StyledButton to="#">Post Job</StyledButton>
          <StyledButton
            bg={colors.red}
            to="#"
            onClick={()=> dispatch(logoutUser(navigate))}
          >
            Logout
          </StyledButton>
        </ButtonGroup>
      </StyledFormArea>
    </div>
  );
};

export default connect(null, { logoutUser })(Dashboard);

Store.js:

import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/rootReducer';

import {sessionService} from 'redux-react-session';

const initialState = {}
const middlewares = [thunk]

const store = createStore(rootReducer, initialState, compose(applyMiddleware(...middlewares)));
sessionService.initSessionService(store);

export default store;

Edit:

If i add return() in the action:

export const logoutUser = (navigate) => {
    return () => {
        sessionService.deleteSession();
        sessionService.deleteUser();
        navigate('/');
    }
}

I get this error in Console.dev:

Uncaught TypeError: logoutUser(...) is not a function

Edit (2):

Removing return ( ) => { ... } and ( ) of logoutUser(navigate)( ) will keep the page refreshing then get me back to dashboard


Solution

  • It seems the issue is related to using both the connect Higher Order Component and manually dispatching actions to the store. The connect HOC automatically wraps the action creators in a call to dispatch and injects a dispatchable action as a prop into the decorated component. Your code is then wrapping logoutUser in another call to dispatch. Pick one or the other, but not both.

    Using connect HOC

    //auth & redux
    import { connect } from 'react-redux';
    import { logoutUser } from '../../auth/actions/userActions';
    
    //React router
    import { useNavigate } from 'react-router-dom';
    ...
    
    const Dashboard = ({ logoutUser }) => {
      const navigate = useNavigate();
    
      return (
        <div>
          ...
          <StyledFormArea bg={colors.dark2} style={{ marginLeft: '-400px' }}>
            <StyledTitle size={65}>Welcome to Dashboard </StyledTitle>
            <StyledSubTitle size={27}>Feel free to post new jobs</StyledSubTitle>
            ...
            <ButtonGroup>
              <StyledButton to="/">Post Job</StyledButton>
              <StyledButton
                bg={colors.red}
                to="/" // <-- fix target from "#" to "/"
                onClick={(e) => {
                  e.preventDefault(); // <-- prevent any default button actions
                  logoutUser(navigate); // <-- call decorated action
                }}
              >
                Logout
              </StyledButton>
            </ButtonGroup>
          </StyledFormArea>
        </div>
      );
    };
    
    const mapDispatchToProps = {
      logoutUser
    };
    
    export default connect(null, mapDispatchToProps)(Dashboard);
    

    Using useDispatch hook

    //auth & redux
    import { useDispatch } from 'react-redux';
    import { logoutUser } from '../../auth/actions/userActions';
    
    //React router
    import { useNavigate } from 'react-router-dom';
    ...
    
    const Dashboard = () => {
      const navigate = useNavigate();
      const dispatch = useDispatch();
    
      return (
        <div>
          ...
          <StyledFormArea bg={colors.dark2} style={{ marginLeft: '-400px' }}>
            <StyledTitle size={65}>Welcome to Dashboard </StyledTitle>
            <StyledSubTitle size={27}>Feel free to post new jobs</StyledSubTitle>
            ...
            <ButtonGroup>
              <StyledButton to="/">Post Job</StyledButton>
              <StyledButton
                bg={colors.red}
                to="/" // <-- fix target from "#" to "/"
                onClick={(e) => {
                  e.preventDefault(); // <-- prevent any default button actions
                  dispatch(logoutUser(navigate)); // <-- dispatch action
                }}
              >
                Logout
              </StyledButton>
            </ButtonGroup>
          </StyledFormArea>
        </div>
      );
    };
    
    export default Dashboard;
    

    The error message also seems to be saying that you haven't yet added any asynchronous action middleware to your redux store.

    You may need to add middleware to your store setup to handle dispatching other values, such as 'redux-thunk' to handle dispatching functions.

    I suggest following the redux and react-redux guides for adding this to your store configuration, or update to redux-toolkit (same maintainers) which includes the thunk middleware out-of-the-box since it is such a common use case.