redux saga and history.push

joe picture joe · Dec 18, 2017 · Viewed 14.1k times · Source

Backgrond:
I am creating a Login component.

saga.js is composed by 3 functions
1. rootSaga. It will execute the list of sagas inside
2. watchSubmitBtn. It will watch the click on the submit button and dispatch an action.
3. shootApiTokenAuth will receive dispatched action and process axios.post the return value is promise object

In action:
Backend returns 400 to the React. This case no problem I can read the payload and display in the render() easily. But when 200 is returned. I need to let user go to the url /companies.

Attempt:
I had tried put this.props.history.push('/companies'); in the componentWillUpdate(), but it does not work. I have to click Submit 2 times to get React understand that token has been saved.

Login.js

import React, {Component} from 'react';
import ErrorMessage from "../ErrorMessage";
import {Field, reduxForm} from 'redux-form';
import {connect} from 'react-redux';
import {validate} from '../validate';
import {SUBMIT_USERNAME_PASSWORD} from "../../constants";

class Login extends Component {

  constructor(props) {
    //Login is stateful component, but finally action will change
    //reducer state
    super(props);
    const token = localStorage.getItem('token');
    const isAuthenticated = !((token === undefined) | (token === null));
    this.state = {
      token,
      isAuthenticated,
      message: null,
      statusCode: null
    };
  }

  onSubmit(values) {
    const {userid, password} = values;
    const data = {
      username: userid,
      password
    };
    this.props.onSubmitClick(data);
  }

  componentWillUpdate(){
    console.log('componentWillUpdate');
    if(this.props.isAuthenticated){
      this.props.history.push('/companies');
    }
  }

  renderField(field) {
    const {meta: {touched, error}} = field;
    const className = `'form-group' ${touched && error ? 'has-danger' : ''}`;

    console.log('renderField');

    return (
      <div className={className}>
        <label>{field.label}</label>
        <input
          className="form-control"
          type={field.type}
          placeholder={field.placeholder}
          {...field.input}
        />
        <div className="text-help">
          {touched ? error : ''}
        </div>
      </div>
    );
  }

  render() {
    const {handleSubmit} = this.props;

    return (
      <div>
        <ErrorMessage
          isAuthenticated={this.props.isAuthenticated}
          message={this.props.message}
        />

        <form onSubmit={handleSubmit(this.onSubmit.bind(this))}>
          <Field
            name="userid"
            component={this.renderField}
            placeholder="User ID"
            type="text"
          />
          <Field
            name="password"
            component={this.renderField}
            placeholder="Password"
            type="password"
          />
          <button type="submit" className="btn btn-primary">Submit</button>
        </form>
        <a className='btn btn-primary' href="https://www.magicboxasia.com/">Sign up</a>
      </div>
    );
  }
}


const onSubmitClick = ({username, password}) => {
  return {
    type: SUBMIT_USERNAME_PASSWORD,
    payload: {username, password}
  };
};

const mapStateToProps = (state, ownProps) => {
  return {
    ...state.login
  }
};

export default reduxForm({
  validate,
  form: 'LoginForm'
})(
  connect(mapStateToProps, {onSubmitClick})(Login)
);

saga.ja

const shootApiTokenAuth = (values) =>{
  const {username, password} = values;
  return axios.post(`${ROOT_URL}/api-token-auth/`,
    {username, password});
};

function* shootAPI(action){
  try{
    const res = yield call(shootApiTokenAuth, action.payload);
    yield put({
      type: REQUEST_SUCCESS,
      payload: res
    });
  }catch(err){
    yield put({
      type: REQUEST_FAILED,
      payload: err
    });
  }
}

function * watchSubmitBtn(){
  yield takeEvery(SUBMIT_USERNAME_PASSWORD, shootAPI);
}

// single entry point to start all Sagas at once
export default function* rootSaga() {
  yield all([
    watchSubmitBtn()
  ])
}

Problem:
How can I set the component state and push to url /companies? after backend returns 200?

Answer

brub picture brub · Dec 18, 2017

I usually handle conditional navigation like that in the saga.

The simplest answer with the existing code is to pass a history object as a prop in the SUBMIT_USERNAME_PASSWORD action and do the history.push() call in the success case of the saga, something like:

const onSubmitClick = ({username, password}) => {
  const { history } = this.props;

  return {
    type: SUBMIT_USERNAME_PASSWORD,
    payload: {username, password, history}
  };
};

.......

function* shootAPI(action){
  try{
    const res = yield call(shootApiTokenAuth, action.payload);
    const { history } = action.payload;

    yield put({
      type: REQUEST_SUCCESS,
      payload: res
    });

    history.push('/companies');
  }catch(err){
    yield put({
      type: REQUEST_FAILED,
      payload: err
    });
  }
}