redux-saga / redux-saga

An alternative side effect model for Redux apps
https://redux-saga.js.org/
MIT License
22.53k stars 1.97k forks source link

Actions must be plain objects. Use custom middleware for async actions #822

Closed stefensuhat closed 7 years ago

stefensuhat commented 7 years ago

I'm still learning about redux-saga but stuck at 2 problem.

Here is my code:

// store.js

import { createStore, applyMiddleware } from 'redux';
import { routerMiddleware } from 'react-router-redux';
import createSagaMiddleware from 'redux-saga';
import rootReducer from './reducers';
import rootSaga from './sagas';

const sagaMiddleware = createSagaMiddleware();

export default function configureStore(initialState = {}, history) {
    const store = createStore(rootReducer, initialState, applyMiddleware(sagaMiddleware, routerMiddleware(history)));

    // Extensions
    sagaMiddleware.run(rootSaga);

    return store;
}

//actions.js
import { AUTH_USER, AUTH_ERROR, SENDING_REQUEST, AUTH_REQUEST } from './constants';

export function sendingRequest() {
    return { type: SENDING_REQUEST };
}

export function authRequest(data) {
    return { type: AUTH_REQUEST, data };
}

export function loginUser(user) {
    return { type: AUTH_USER, user };
}

export function authError(error) {
    return { type: AUTH_ERROR, error };
}

// loginSagas.js
import { put, call } from 'redux-saga/effects';
import axios from 'axios';

import * as actions from './actions';

const url = 'http://example.dev';

export function* doSignIn() {
    try {
        yield put(actions.sendingRequest);

        const { email, password } = yield put(actions.authRequest);
        const signin = yield call(axios.post, url, { email, password });

        yield put(actions.loginUser(signin));
    } catch (err) {
        yield put(actions.authError(err));
    }
}

// rootSaga.js
import { takeLatest } from 'redux-saga/effects';
import { doSignIn } from 'containers/Login/loginSagas';
import * as actions from 'containers/Login/actions';

function* rootSaga() {
    yield [
        takeLatest(actions.authRequest, doSignIn),
    ];
}

export default rootSaga;

// componentLogin.js

import React, { Component } from 'react';
import { Field, reduxForm } from 'redux-form';
import { connect } from 'react-redux';

import { authRequest } from './actions';

import Button from 'components/Button';

class Login extends Component {
    constructor(props) {
        super(props);

        this.state = {
            buttonState: 'false',
        };

        this.handleFormSubmit = this.handleFormSubmit.bind(this);
    }

    handleFormSubmit({ email, password }) {
        this
            .props
            .authRequest(email, password);
    }

    toggle() {
        console.log(this.props);
    }

    render() {
        const { handleSubmit } = this.props;
        return (
            <div>
                <div className="content overflow-hidden">
                    <div className="row">
                        <div className="col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3 col-lg-4 col-lg-offset-4" >
                            <div className="block block-themed animated fadeIn">
                                <div className="block-header bg-primary">
                                    <h3 className="block-title">Login</h3>
                                </div>
                                <div className="block-content block-content-full block-content-narrow">
                                    <h1 className="h2 font-w600 push-30-t push-5">FreshBox</h1>
                                    <p>Welcome, please login.</p>
                                    <form
                                        className="form-horizontal push-30-t push-50"
                                        onSubmit={handleSubmit(this.handleFormSubmit)}
                                    >
                                        <div className="form-group">
                                            <div className="col-xs-12">
                                                <div className="form-material form-material-primary">
                                                    <Field
                                                        component="input"
                                                        className="form-control"
                                                        type="text"
                                                        id="email"
                                                        placeholder="user@fresbox.com"
                                                        name="email"
                                                    />
                                                    <label htmlFor="email">Email</label>
                                                </div>
                                            </div>
                                        </div>
                                        <div className="form-group">
                                            <div className="col-xs-12">
                                                <div className="form-material form-material-primary">
                                                    <Field
                                                        component="input"
                                                        className="form-control"
                                                        type="password"
                                                        placeholder="password"
                                                        id="password"
                                                        name="password"
                                                    />
                                                    <label htmlFor="password">Password</label>
                                                </div>
                                            </div>
                                        </div>
                                        <div className="form-group">
                                            <div className="col-xs-12 col-sm-6 col-md-4">
                                                <Button
                                                    action="submit"
                                                    text="Login"
                                                    className="btn btn-block btn-primary"
                                                    state={this.props.authenticating}
                                                />
                                            </div>
                                        </div>
                                    </form>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>

                <div className="push-10-t text-center animated fadeInUp">
                    <small className="text-muted font-w600">
                        2017 &copy; FreshBox</small>
                </div>
            </div>
        );
    }
}

function mapStateToProps(state) {
    const { authenticating } = state.auth;
    return { authenticating };
}

Login = reduxForm({
    form: 'loginForm',
})(Login);

export default connect(mapStateToProps, { authRequest })(Login);

The problem is:

  1. Actions must be plain objects. Use custom middleware for async actions
  2. Function doSignIn keep getting call even though the action never call. Action is can be call by button click.

Any solution?

Andarist commented 7 years ago

Hey. I dont rly understand ur first problem. Your actions seems to be plain objects and you have not specified anything more about the problem.

As to the second one - is that all of your code? Sagas are not scoped per component but rather acting like a global listeners + handlers. Your doSignIn should be called whenever authRequest action happens. I have a little problem with reading the code on my mobile but I think u pass to the takeLatest a function (action creator) and not an action type which might lead to unwanted calls in your scenario

stefensuhat commented 7 years ago

@Andarist yeah I still stuck and can't solve it. If I change to fork the method never get call even the button is pressed.

Any code you want to look at? I will post it if you need more

Andarist commented 7 years ago

Looking at the originally posted code, you need to change this takeLatest(actions.authRequest, doSignIn) to takeLatest(AUTH_REQUEST, doSignIn).

And also this part:

yield put(actions.sendingRequest);

const { email, password } = yield put(actions.authRequest);

Looks really weird. Doing like so you are dispatching functions and not actions.

Probably what you were looking for is something along theese lines:

// action parameter is available here thanks to takeLatest running your 'doSignIn' with it
export function* doSignIn(action) { 
    const { email, password } = action;  
    try {
        yield put(actions.sendingRequest()); // changed here actions.sendingRequest to actions.sendingRequest()

        const signin = yield call(axios.post, url, { email, password });

        yield put(actions.loginUser(signin));
    } catch (err) {
        yield put(actions.authError(err));
    }
}
Andarist commented 7 years ago

If you still need a help, please reopen this. Closing for now, as there was no activity here for over a month

estefanojctimoteo commented 6 years ago

Thanks @Andarist because now I know "export function* doSignIn(action) // action parameter is available here thanks to takeLatest running your 'doSignIn' with it".

MuhammadSulman commented 5 years ago
 *
 * DeleteConfirmationModal
 *
 */
import React from 'react';
import saga from './saga';
import { compose} from 'redux';
import reducer from './reducer';
import { connect } from 'react-redux';
import injectSaga from "../../utils/injectSaga";
import { createStructuredSelector } from 'reselect';
import injectReducer from "../../utils/injectReducer";
import {submitDeleteConfirmationModalForm, toggleDeleteConfirmationModal} from "./actions";
import {
    deleteConfirmationRecordId,
    deleteConfirmationRecordIndex,
    makeSelectDeleteConfirmationModalShow
} from './selectors';
import DeleteConfirmationForm from "./form";

/* eslint-disable react/prefer-stateless-function */
class DeleteConfirmationModal extends React.PureComponent {

  constructor(props) {
      super(props);
      this.toggleModal = this.toggleModal.bind(this);
  }

  toggleModal() {
      const {toggleModal} = this.props;
      toggleModal();
  }

  render() {
      const {show, itemName, onModalFormSubmit, id, index} = this.props;
      return (
          <div id="modal_default" className="modal fade">
              <div className="modal-dialog">
                  <div className="modal-content">
                      <div className="modal-header">
                          <button type="button" className="close" data-dismiss="modal">&times;</button>
                          <h5 className="modal-title">Delete {itemName}</h5>
                      </div>

                      <div className="modal-body">
                          <p>You have selected to delete this {itemName}</p>
                          <p>If this was the action that you wanted to do, please confirm your choice, or cancel and return to the
                              page</p>
                          <br/>
                          <DeleteConfirmationForm
                            id={id}
                            index={index}
                            onModalFormSubmit={onModalFormSubmit}
                          />
                      </div>
                      <div className="modal-footer">
                      </div>
                  </div>
              </div>
          </div>
      );
  }
}

const mapStateToProps = createStructuredSelector({
    id: deleteConfirmationRecordId(),
    index: deleteConfirmationRecordIndex(),
    show: makeSelectDeleteConfirmationModalShow(),

});

function mapDispatchToProps(dispatch) {
  return {
      toggleModal: () => dispatch(toggleDeleteConfirmationModal()),
      onModalFormSubmit:(data) => dispatch(submitDeleteConfirmationModalForm(data)),
  };
}

const withConnect = connect(
  mapStateToProps,
  mapDispatchToProps
);

const withReducer = injectReducer({ key: 'deleteConfirmationModal', reducer });
const withSaga = injectSaga({ key: 'deleteConfirmationModal', saga });

export default compose(
  withReducer,
  withSaga,
  withConnect,
)(DeleteConfirmationModal);
/*
 *
 * DeleteConfirmationModal actions
 *
 */

import {
    SETUP_DELETE_CONFIRMATION_MODAL,
    TOGGLE_DELETE_CONFIRMATION_MODAL,
    SUBMIT_DELETE_CONFIRMATION_MODAL_FORM,

} from './constants';

export function toggleDeleteConfirmationModal(forceAction = null) {
    return {
        type: TOGGLE_DELETE_CONFIRMATION_MODAL,
        forceAction,
    }
}

export function setupDeleteConfirmationModal(payload) {
    return {
        type: SETUP_DELETE_CONFIRMATION_MODAL,
        payload,
    }
}

export function submitDeleteConfirmationModalForm(params) {
    return {
        type: SUBMIT_DELETE_CONFIRMATION_MODAL_FORM,
        params,
    }
}
import {
    SUBMIT_DELETE_CONFIRMATION_MODAL_FORM,
} from "./constants";

import LocationsApi from "../../api/LocationsApi";
import {put, all, takeLatest,} from 'redux-saga/effects';
import {submitDeleteConfirmationModalFormSuccess} from "../Location/actions";

function* submitDeleteConfirmationModalForm(action) {

    try {
        const id = action.params.id;
        const index = action.params.index;
        const response = yield LocationsApi.delete(id);
        if (response.status && response.status === 'ok') {
            localStorage.removeItem('location' + index);
            localStorage.setItem('locations_length', parseInt(localStorage.getItem('locations_length')) - 1);
            yield put($('#modal_default').modal('hide'));
            yield put(submitDeleteConfirmationModalFormSuccess(id));
        }
    }catch (error) {

    }
}

function* watchAll() {
    yield all([takeLatest(SUBMIT_DELETE_CONFIRMATION_MODAL_FORM, submitDeleteConfirmationModalForm)]);
}

export default watchAll;

I am getting this error since yesterday, trying to resolve it but still failed.

utils.js?56bd:225 uncaught at submitDeleteConfirmationModalForm Error: Actions must be plain objects. Use custom middleware for async actions.
    at Object.performAction (<anonymous>:1:42634)
    at liftAction (<anonymous>:1:36073)
    at dispatch (<anonymous>:1:40184)
    at eval (webpack-internal:///823:22:18)
    at eval (webpack-internal:///1041:70:22)
    at dispatch (webpack-internal:///41:619:28)
    at eval (webpack-internal:///42:282:12)
    at eval (webpack-internal:///491:498:52)
    at exec (webpack-internal:///420:22:5)
    at flush (webpack-internal:///420:63:5)