Formik: How to use React-Router 4 history.push method inside withFormik HOC

Created on 14 Jan 2018  路  4Comments  路  Source: formium/formik

Bug, Feature, or Question?

Question

Current Behavior

When I try to call props.history.push('/home') from React-Router 4 I receive the error below:

index.js:2177 Warning: Can only update a mounted or mounting component. This usually means you called setState, replaceState, or forceUpdate on an unmounted component. This is a no-op.

Please check the code for the Formik component.
__stack_frame_overlay_proxy_console__ @ index.js:2177
printWarning @ warning.js:33
warning @ warning.js:57
warnAboutUpdateOnUnmounted @ react-dom.development.js:9766
scheduleWorkImpl @ react-dom.development.js:10737
scheduleWork @ react-dom.development.js:10689
enqueueSetState @ react-dom.development.js:6212
./node_modules/react/cjs/react.development.js.Component.setState @ react.development.js:237
Formik._this.setSubmitting @ formik.es6.js:2959
_callee$ @ LoginContainer.js:88
tryCatch @ runtime.js:62
invoke @ runtime.js:296
prototype.(anonymous function) @ runtime.js:114
step @ Register.js:101
(anonymous) @ Register.js:101
Promise resolved (async)
step @ Register.js:101
(anonymous) @ Register.js:101
(anonymous) @ Register.js:101
login @ LoginContainer.js:47
handleSubmit @ LoginContainer.js:30
C._this.handleSubmit @ formik.es6.js:2912
Formik._this.executeSubmit @ formik.es6.js:3079
(anonymous) @ formik.es6.js:2969
Promise resolved (async)
Formik._this.runValidationSchema @ formik.es6.js:2966
Formik._this.submitForm @ formik.es6.js:3072
Formik._this.handleSubmit @ formik.es6.js:3044
callCallback @ react-dom.development.js:542
invokeGuardedCallbackDev @ react-dom.development.js:581
invokeGuardedCallback @ react-dom.development.js:438
invokeGuardedCallbackAndCatchFirstError @ react-dom.development.js:452
executeDispatch @ react-dom.development.js:836
executeDispatchesInOrder @ react-dom.development.js:858
executeDispatchesAndRelease @ react-dom.development.js:956
executeDispatchesAndReleaseTopLevel @ react-dom.development.js:967
forEachAccumulated @ react-dom.development.js:935
processEventQueue @ react-dom.development.js:1112
runEventQueueInBatch @ react-dom.development.js:3607
handleTopLevel @ react-dom.development.js:3616
handleTopLevelImpl @ react-dom.development.js:3347
batchedUpdates @ react-dom.development.js:11082
batchedUpdates @ react-dom.development.js:2330
dispatchEvent @ react-dom.development.js:3421

Desired Behavior

Ideally I want to call react-router from within my login handler, but I've not been able to work out a way to do this.

Suggested Solutions

N/A

Info

Here is my App.js

import React, { Component } from 'react'
import {
  Switch,
  Route
} from 'react-router-dom'
import Authentication from './Authentication'
import Home from './Home'

class App extends Component {
  render () {
    return (
      <div className="App h-100">
        <Switch>
          <Route path="/home" component={Home} />
          <Route path="/" component={Authentication} />
        </Switch>
      </div>
    )
  }
}

export default App

The Authentication component is very simple, it just wraps around my LoginContainer:

import React from 'react'
import {
  Switch,
  Route,
  Redirect
} from 'react-router-dom'
import {
  Col,
  Container,
  Row,
  Jumbotron
} from 'reactstrap'

import LoginContainer from '../containers/LoginContainer'
import RegisterContainer from '../containers/RegisterContainer'

const Authentication = () => (
  <Jumbotron fluid className="h-100 mb-0 align-items-center d-flex ts-jumbotron">
    <Container>
      <Row>
        <Col md={8} lg={6} className="mx-auto">
          <h1 className="text-white text-center">TrackSuite</h1>
          <Switch>
            <Route path="/login" component={LoginContainer} />
            <Route path="/register" component={RegisterContainer} />
            <Redirect to="/login" />
          </Switch>
        </Col>
      </Row>
    </Container>
  </Jumbotron>
)

export default Authentication

My LoginContainer is a Container component that I wrap around my Login component:

import { withFormik } from 'formik'
import { connect } from 'react-redux'
import { compose } from 'redux'
import { withRouter } from 'react-router-dom'
import Yup from 'yup'
import AuthenticationService from '../services/AuthenticationService'
import Login from '../components/Login'
import { setToken } from '../actions'

const mapDispatchToProps = dispatch => {
  return {
    setToken: token => dispatch(setToken(token))
  }
}

const LoginContainer = compose(
  withRouter,
  connect(null, mapDispatchToProps),
  withFormik({
    mapPropsToValues ({ email, password }) {
      return {
        email: '',
        password: ''
      }
    },
    /**
     * @todo Make password requirements identical to API requirements.
     */
    handleSubmit (values, props) {
      login({
        email: values.email,
        password: values.password
      }, { ...props })
    },
    validationSchema: Yup.object().shape({
      email: Yup
        .string()
        .email('Email address is not valid.')
        .required('Email address is required.'),
      password: Yup
        .string()
        .required('Password is required.')
    })
  })
)(Login)

const login = async (credentials, { setSubmitting, setErrors, setStatus, props }) => {
  // console.log('history', history)
  console.log('props: ', props)
  // const { history } = props
  try {
    setSubmitting(true)
    const res = await AuthenticationService.login({
      email: credentials.email,
      password: credentials.password
    })
    if (res.status === 201) {
      setToken(res.data.token)
      setStatus({
        message: 'Successfully logged in! Redirecting...'
      })
      props.history.push('/home')
    }
  } catch (error) {
    console.log('error: ', error)

    if (error.response.data.error) {
      const errKeys = Object.keys(error.response.data.error)

      switch (errKeys[0]) {
        case 'email':
          setErrors({
            email: error.response.data.error[errKeys[0]]
          })
          break
        case 'password':
          setErrors({
            password: error.response.data.error[errKeys[0]]
          })
          break
        default:
          setStatus({
            error: error.response.data.error
          })
      }
    }
  } finally {
    setSubmitting(false)
  }
}

export default LoginContainer

And lastly my Login component is a simple presentational component:

import {
  Alert,
  Button,
  Card,
  CardBody,
  CardText,
  CardTitle,
  Row,
  Col,
  Form,
  FormGroup,
  FormFeedback,
  Input
} from 'reactstrap'
import React from 'react'
import { NavLink } from 'react-router-dom'

const Login = (props) => {
  const {
    values,
    errors,
    status,
    touched,
    handleChange,
    handleBlur,
    handleSubmit,
    isSubmitting
  } = props
  return (
    <Card className="Login">
      <CardBody>
        <CardTitle className="text-center">Login</CardTitle>
        <Form onSubmit={handleSubmit}>
          <Row>
            <Col xs={10} className="mx-auto">
              {status && status.error ? (
                <Alert color="danger">{status.error}</Alert>
              ) : (
                status && status.message && <Alert>{status.message}</Alert>
              )}
              <FormGroup>
                <Input
                  autoFocus
                  type="email"
                  name="email"
                  value={values.email}
                  onChange={handleChange}
                  onBlur={handleBlur}
                  id="email"
                  className={touched.email && ((status && status.email) || errors.email) ? 'is-invalid' : null}
                  placeholder="Email"
                />
                {status && status.email ? (
                  <FormFeedback>{status.email}</FormFeedback>
                ) : (
                  errors.email && touched.email && <FormFeedback>{errors.email}</FormFeedback>
                )}
              </FormGroup>
              <FormGroup>
                <Input
                  type="password"
                  name="password"
                  value={values.password}
                  onChange={handleChange}
                  onBlur={handleBlur}
                  id="password"
                  className={touched.password && ((status && status.password) || errors.password) ? 'is-invalid' : null}
                  placeholder="Password"
                />
                {status && status.password ? (
                  <FormFeedback>{status.password}</FormFeedback>
                ) : (
                  errors.password && touched.password && <FormFeedback>{errors.password}</FormFeedback>
                )}
              </FormGroup>
              <Button
                color="success"
                className="d-block mx-auto"
                type="submit"
                disabled={isSubmitting}>
                  Sign In
              </Button>
            </Col>
          </Row>
        </Form>
        <CardText className="text-right">Don't have an account? You'll need to <NavLink to="/register">register</NavLink>.</CardText>
      </CardBody>
    </Card>
  )
}

export default Login

Is there a recommended way to do this? I tried to check https://github.com/jaredpalmer/formik/issues/299 but it does not recommend how to access the react-router props, only how to inject them. The props object seems to contain a history object, but when I try to use it, I receive the error above.

Any help would be appreciate tremendously.

The source is available at https://github.com/JheeBz/traqsuite/tree/master/client if that helps, but obviously the code above relating to this issue is uncommitted.

  • Formik Version: 0.10.5
  • OS: Ubuntu 16.04
  • Node Version: v8.9.4
  • Package Manager and version: yarn 1.3.2

Most helpful comment

This is what worked for me:

<WrappedFormComp {...props} />

Now that you passed history via prop drilling, you can write:

const formEnhancer = withFormik({
   [...]
    handleSubmit(values, { props }) {
      props.history.push('/some/funky/path')
    }
  })

All 4 comments

I came across the same problem. it has to do with setSubmitting function which force changes the state.

`
try {
setSubmitting(true) <===========================REMOVE THIS
const res = await AuthenticationService.login({
email: credentials.email,
password: credentials.password
})

`

I have the same problem. Am not using setSubmitting(true).

UPDATE:
For me the problem was using resetForm(); after the props.history.push.

This is what worked for me:

<WrappedFormComp {...props} />

Now that you passed history via prop drilling, you can write:

const formEnhancer = withFormik({
   [...]
    handleSubmit(values, { props }) {
      props.history.push('/some/funky/path')
    }
  })

For those facing the same problem... Here is what's worked for me https://github.com/jaredpalmer/formik/issues/299#issuecomment-451758739

Was this page helpful?
0 / 5 - 0 ratings