简体   繁体   中英

How to subscribe to a Redux action in a React component

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

PS 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).

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).

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM