16

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?

joe
  • 8,383
  • 13
  • 61
  • 109
  • I had read https://stackoverflow.com/questions/42893161/how-to-provide-a-history-instance-to-a-saga but not understand – joe Dec 18 '17 at 08:47

4 Answers4

23

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
    });
  }
}
brub
  • 1,083
  • 8
  • 18
  • I just know that I can pass history! Thank you very much. I always this every properties that prefix by `this` can not be able to passed to the other files. But I was wrong. – joe Dec 18 '17 at 13:35
  • 4
    I wish there was a better way to do this. As in, in angular you can directly inject router/history in your saga/effect classes. So there we dont need to pass history around. – dasfdsa May 19 '19 at 22:42
  • you are genius @brub – Mahendra Pratap Dec 07 '21 at 14:47
  • @brub don't you have a feeling that its rather a hack? I mean sending functions and object like history in actions seems like a not expected thing to do. Unfortunately, I can't justify it with my own reasons (though Redux-toolkit has an opinion about that - https://redux.js.org/style-guide/style-guide#do-not-put-non-serializable-values-in-state-or-actions) – Vladislav Sorokin Jan 28 '22 at 11:20
  • as I mentioned before in the response to @dasfdsa , i don't pass history thru the functions, it was just s simple example based on the code provided. normally i initialize my router with a history that i can then import into the sagas. – brub Jan 29 '22 at 12:26
9
import { push } from 'react-router-redux';    

yield put(push('/path-to-go'));

solved my problem

Inyoung Kim 김인영
  • 1,434
  • 1
  • 17
  • 38
Demetrio Guilardi
  • 325
  • 1
  • 5
  • 12
1

I am not as experienced in react but you could achieve it as follows:

1.Create a new Module: history-wrapper.ts

export class HistoryWrapper {
  static history;
  static init(history){
    HistoryWrapper.history = history;
  }
}

2.In you login.jsx

  HistoryWrapper.init(history);//initialize history in HistoryWrapper

3.Subsequently anywhere in your app

  HistoryWrapper.history.push('/whatever');
dasfdsa
  • 7,102
  • 8
  • 51
  • 93
  • In practice I don't pass the history around as props, I use a similar approach as you are doing using https://github.com/ReactTraining/history. I initialize my Router using this history object, and then I import it into my sagas and use it directly. – brub May 20 '19 at 17:11
-4

React has additional lifecycle. I just know it today. They are many of them.

componentDidUpdate() {
    this.props.history.push('/companies');
  }
joe
  • 8,383
  • 13
  • 61
  • 109