1

I'm trying to figure out how I could subscribe to a Redux action in a React component. I didn't find anything on Google so I'm opening an issue here to see if someone can help me out.

When an user tries to log in I dispatch an loginRequestAction() then I handle it using redux-saga (check out my saga.js file below), finally if everything is OK I dispatch LOGIN_REQUEST_SUCCESS action.

What I would like to do here find a way to subscribe to LOGIN_REQUEST_SUCCESS action in my React component so once the action is received I can update my React component local state and use history.push() to redirect the user to dashboard page.

This is my component code:

/**
 *
 * LoginPage
 *
 */

import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Helmet } from 'react-helmet';
import { createStructuredSelector } from 'reselect';
import { compose } from 'redux';
import { Container, Row, Col, Button, Alert } from 'reactstrap';
import injectSaga from 'utils/injectSaga';
import injectReducer from 'utils/injectReducer';
import { Link } from 'react-router-dom';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
import { Formik, Form, Field } from 'formik';
import * as Yup from 'yup';
import { ReactstrapInput } from 'reactstrap-formik';
import reducer from './reducer';
import saga from './saga';
import { loginRequestAction } from './actions';
import { makeSelectLoginPage } from './selectors';
import { makeSelectIsLogged } from '../Auth/selectors';

const LoginSchema = Yup.object().shape({
  userIdentifier: Yup.string().required('Required'),
  password: Yup.string().required('Required'),
});

/* eslint-disable react/prefer-stateless-function */
export class LoginPage extends React.PureComponent {
  constructor(props) {
    super(props);

    this.state = {
      formMsg: {
        color: '',
        text: '',
      },
    };
  }

  componentDidMount() {
    const { history, isLogged } = this.props;

    if (isLogged) history.push('/dashboard/index');
  }

  render() {
    const { formMsg } = this.state;
    const { onLoginFormSubmit } = this.props;

    return (
      <div>
        <Helmet>
          <title>Sign in</title>
          <meta name="description" content="Description of LoginPage" />
        </Helmet>
        <Container className="auth-container">
          <div className="form-page">
            <Row>
              <Col className="text-center">
                <img
                  className="mb-4"
                  src="https://getbootstrap.com/docs/4.1/assets/brand/bootstrap-solid.svg"
                  alt=""
                  width="72"
                  height="72"
                />
              </Col>
            </Row>
            <Row>
              <Col className="text-center">
                {' '}
                <h1 className="h3 mb-3 font-weight-normal">Authentication</h1>
                <Alert
                  color={formMsg.color}
                  role="alert"
                  className={formMsg.text ? '' : 'd-none'}
                >
                  <strong>{formMsg.text}</strong>
                </Alert>
              </Col>
            </Row>

            <Formik
              initialValues={{
                userIdentifier: '',
                password: '',
              }}
              validationSchema={LoginSchema}
              onSubmit={onLoginFormSubmit}
            >
              {({ isSubmitting }) => (
                <Form>
                  <Field
                    component={ReactstrapInput}
                    name="userIdentifier"
                    type="userIdentifier"
                    placeholder="john@acme.com"
                    label="E-mail address"
                  />
                  <Field
                    component={ReactstrapInput}
                    name="password"
                    type="password"
                    placeholder="Password"
                    label="Password"
                  />
                  <div>
                    <Button
                      type="submit"
                      block
                      size="lg"
                      color="primary"
                      disabled={isSubmitting}
                    >
                      <FontAwesomeIcon
                        pulse
                        icon={faSpinner}
                        className={isSubmitting ? 'mr-2' : 'd-none'}
                      />
                      Log in to access
                    </Button>
                  </div>
                </Form>
              )}
            </Formik>

            <Link to="/auth/reset">
              <Button size="sm" color="secondary" block className="mt-2">
                Forgot password?
              </Button>
            </Link>
            <p className="mt-5 mb-3 text-center">
              <Link to="/auth/register">
                Don&#39;t have an account? Sign up
              </Link>
            </p>
          </div>
        </Container>
      </div>
    );
  }
}

LoginPage.propTypes = {
  isLogged: PropTypes.bool,
  history: PropTypes.object,
  onLoginFormSubmit: PropTypes.func,
};

const mapStateToProps = createStructuredSelector({
  loginpage: makeSelectLoginPage(),
  isLogged: makeSelectIsLogged(),
});

function mapDispatchToProps(dispatch) {
  return {
    onLoginFormSubmit: values => dispatch(loginRequestAction(values)),
  };
}

const withConnect = connect(
  mapStateToProps,
  mapDispatchToProps,
);

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

export default compose(
  withReducer,
  withSaga,
  withConnect,
)(LoginPage);

This is my saga.js file:

import { put, call, takeLatest } from 'redux-saga/effects';
import {
  LOGIN_REQUEST,
  LOGIN_REQUEST_SUCCESS,
  LOGIN_REQUEST_FAILED,
} from './constants';
import { AuthApi } from '../../api/auth.api';

export function* loginRequest(action) {
  const { userIdentifier, password } = action.values;

  try {
    const tokens = yield call(AuthApi.login, userIdentifier, password);
    yield put({ type: LOGIN_REQUEST_SUCCESS, tokens });
  } catch (err) {
    let errMsg;

    switch (err.status) {
      case 403:
        errMsg = 'Invalid credentials';
        break;
      case 423:
        errMsg = 'Account desactivated';
        break;
      default:
        errMsg = `An server error ocurred. We have been notified about this error, our devs will fix it shortly.`;
        break;
    }

    yield put({ type: LOGIN_REQUEST_FAILED, errMsg });
  }
}

export default function* defaultSaga() {
  yield takeLatest(LOGIN_REQUEST, loginRequest);
}

P.S. I'm coming from this GitHub issue: https://github.com/react-boilerplate/react-boilerplate/issues/2360 (Please take a look at it as there is a possible solution but IMHO I think it isn't the correct way to go).

Alfonso
  • 1,125
  • 1
  • 13
  • 23
  • If someone is coming from Google trying to solve a similar issue and reads this, what I did is follow Jed Richards advice. Also I'm doing something more: when a component is being removed from the DOM I `dispatch` a `reset` action in `componentWillUnmount` to clear the Redux state, so it releases states that are not going to be used in another parts of the app and are useless. – Alfonso Oct 01 '18 at 15:34

2 Answers2

3

As a rule you never "subscribe" to Redux actions in components, since it breaks some of React's declarative benefits. Most of the time you'll just connect() to Redux state via props and simply render based off those. Subscribing to actions, and then calling routing functions in response is imperative programming, rather than declarative, so you need to avoid that within components.

So when working with sagas you have a couple of options for this sort of scenario. Instead of emitting LOGIN_REQUEST_SUCCESS from your saga you could instead just push a new route from inside the saga itself:

yield call(push, '/foo')

Or alternatively maintain a authenticated: boolean property in your Redux state, flip it to true in the saga when the user logs in, and then use it to conditionally render a react-router <Redirect /> in your component (or perhaps conditionally render the entire authenticated tree of your app, up to you).

Jed Richards
  • 12,244
  • 3
  • 24
  • 35
  • This is wrong. It doesn't break React's declarative benefits. Doing a fetch in a component is subscribing to a stream of size 1. When that stream emits, one does a `setState` to update the component. Using connect is subscribing to a stream of states of infinite size, and `connect` itself calls `setState` to update the component. Similarly one can subscribe to redux' stream of actions and do some side effects and it doesn't break any benefit of react at all. Just because `react-redux` doesn't provide that API doesn't make it wrong. – mostruash Jul 19 '19 at 08:11
  • I never said that subscribing to _anything_ is wrong in React components - I said that subscribing to _actions_ is wrong. It's a common beginner mistake to return a fetch promise from a redux action creator or thunk, and work with that in the component rather than going on the proper round trip via the store, so I wanted to steer the OP away from that terminology. Obviously it's fine to subscribe to things generally and make side effects in components - my answer is narrowly relating to redux+saga patterns. – Jed Richards Jul 19 '19 at 12:26
  • And that’s what I pointed at. Subscribing to redux’ actions from inside a component is completely fine. I talked about other side effects and other subscribeable channels simply because they have zero difference to subscribing to actions. Anyone saying otherwise would have to come up with a very good argument and a proof backing the argument up. – mostruash Jul 20 '19 at 16:55
1

To be honest, I think the answer in the github make sense. You subscribe the dispatch action in redux by using the reducer. and the reducer responsible for setting redux state and return it to your component by using connect function.

Adding a flag property called ‘loginSuccess’ or whatever and pass to you component would not cause performance issues from my point of view because it is the way how redux work with react

Jiachen Guo
  • 281
  • 1
  • 8