ReactJS - setState in parent component submits form despite using preventDefault()

Viewed 33

I'm trying to show alert when validating form. The problem is when error occures during validation I want to show alert defined in parent component - this causes submiting/reloading form and e.preventDefault() is not enough to stop from submiting. I've figured out that setState passed by props causes submission but I don't know how to setState and prevent from submission. What can I do with this case? I suppose that React Router Dom has something in common because this mechanism works without using routes. Here's some code:

App.js (parent component)

import React, { Fragment, Component } from "react";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import { Alert } from "react-bootstrap";
import Login from "./components/pages/Login";
import Home from "./components/pages/Home";
class App extends Component {
    state = {
        alert: "",
    };
    render() {
        const { alert } = this.state;
        return (
            <Fragment>
                <Router>
                    {alert && <Alert variant="danger">{alert}</Alert>}
                    <Switch>
                        <Route exact path="/" component={() => <Home />} />
                        <Route exact path="/login" component={() => <Login onShowAlert={(alert) => this.setState({ alert: alert })} />} />
                    </Switch>
                </Router>
            </Fragment>
        );
    }
}
export default App;

Login.js (child component)

import React, { Component, Fragment } from "react";
import { Form, Button } from "react-bootstrap";
class Login extends Component {
    state = {
        login: "",
        password: "",
    };
    handleCheckValidation = (e) => {
        e.preventDefault();
        const { onShowAlert } = this.props;
        onShowAlert("Show some error message"); //this causes submitting form
    };
    render() {
        const { login, password } = this.state;
        return (
            <Fragment>
                <Form onSubmit={this.handleCheckValidation} autoComplete="off">
                    <Form.Control type="email" name="login" value={login} onChange={(e) => this.setState({ [e.target.name]: e.target.value })}></Form.Control>
                    <Form.Control type="password" name="password" value={password} onChange={(e) => this.setState({ [e.target.name]: e.target.value })}></Form.Control>
                    <Button type="submit" variant="primary">Sign in</Button>
                </Form>
            </Fragment>
        );
    }
}
export default Login;
1 Answers

Issue

For the given handler:

handleCheckValidation = (e) => {
  e.preventDefault();
  const { onShowAlert } = this.props;
  onShowAlert("Show some error message");
};

I don't see a way for this to submit the form.

I think the issue is with how you render the Login component in the Route.

<Route
  exact
  path="/login"
  component={() => (
    <Login onShowAlert={(alert) => this.setState({ alert: alert })} />
  )}
/>

Route component

When you use component (instead of render or children, below) the router uses React.createElement to create a new React element from the given component. That means if you provide an inline function to the component prop, you would create a new component every render. This results in the existing component unmounting and the new component mounting instead of just updating the existing component. When using an inline function for inline rendering, use the render or the children prop.

this.state.alert state is updated and App is rerendered.

Solution

Render the component on the Route's render prop:

<Route
  exact
  path="/login"
  render={() => (
    <Login onShowAlert={(alert) => this.setState({ alert: alert })} />
  )}
/>

Or as a child component:

<Route
  exact
  path="/login"
>
  <Login onShowAlert={(alert) => this.setState({ alert: alert })} />
</Route>

You'll want to go ahead and fix your home route as well. This can still use the component prop since you are not injecting additional props.

<Route exact path="/" component={Home} />
Related