javascriptreactjshttp-redirectreact-router-domreact-link

Why is `<Link>` not working in form submission in reactjs v.18, react router dom v6.3?


I will try to explain to the best where you can understand the issue.

If you see there is a button in both AddContact.js and EditContact.js, such as, Add button and Update button. They are wrapped by <Link to="/"></Link>. However, if I click on the button the event is not happening. If I comment the <Link> the event is being executed. I require both of my event handler and <Link> should work.

If you are going to comment or suggest me to put a event handler on button instead of onSubmit could you please explain why it is and why not it will work being the present way of code.

App.js

import React, { useState, useEffect } from "react";
import { BrowserRouter as Router, Route, Routes, useNavigate, useLocation } from 'react-router-dom';
import { v4 as uuid } from 'uuid';
import api from "../api/contacts";
import './App.css';
import Header from './Header';
import AddContact from './AddContact';
import EditContact from "./EditContact";
import ContactList from './ContactList';
import ContactDetail from './ContactDetail';

function App() {
  const [contacts, setContacts] = useState([]);
  const [searchTerm, setSearchTerm] = useState("");
  const [searchResults, setSearchResults] = useState("");
  const [editContactDetail, setEditContactDetail] = useState("");


  //Retrieve Contacts
  const retrieveContacts = async () => {
    const response = await api.get("contacts");
    return response.data;
  };

  const addContactHandler = async (singleContact) => {
    const addContact = {
      id: uuid(),
      ...singleContact
    };

  const addContactDone = await api.post("/contacts", addContact);
    setContacts([...contacts, addContactDone.data]);
  };

  const removeContactHandler = async (id) => {
        await api.delete(`/contacts/${id}`);
        const newContactList = contacts.filter((deleteContact) => {
        return deleteContact.id !== id;
     });
     setContacts(newContactList);
  };

  const searchHandler = (searchTerm) => {
      setSearchTerm(searchTerm);
      if (searchTerm !== "") {
        const newContactList = contacts.filter((contact) => {
            return Object.values(contact)
              .join(" ")
              .toLowerCase()
              .includes(searchTerm.toLowerCase());
        });
        setSearchResults(newContactList);
      } else {
        setSearchResults(contacts);
      }
  };

  const editChosenContact = (id) => {
    const newContactList = contacts.filter((editRecord) => {
      return editRecord.id === id;
    });
    setEditContactDetail(newContactList);
  };

  const updateContactPerson = async (selectedContactEdit) => {
    const editResponse = await api.put(`/contacts/${selectedContactEdit.id}`, selectedContactEdit);
    const {id, name, email} = editResponse.data;

    setContacts( contacts.map(contact => {
      return contact.id === id ? {...editResponse.data} : contact;
    })
    );
  };  

  useEffect(() => {
    const getAllContacts = async () => {
      const allContacts = await retrieveContacts();
      if(allContacts) setContacts(allContacts);
    }
    getAllContacts();
  }, []);

  useEffect(() => {
    console.log("useEffect happening!");
  }, [contacts]);

  return (
    <div>
      <Router>
          <Header/>
          <Routes>
            <Route exact path="/" element={ <ContactList contacts={ searchTerm.length < 1 ? contacts : searchResults } getContactId={ removeContactHandler } 
              getEditContact={editChosenContact} term={ searchTerm } searchKeyword={ searchHandler } />  }/>          
            <Route exact path="/add" element={ <AddContact addContactAction={ addContactHandler } />  }/>          
            <Route exact path="/edit/:id" element={ <EditContact editContactPerson={ editContactDetail } updateContactPerson={ updateContactPerson } />  }/>          
            <Route exact path="/contact/:id" element={ <ContactDetail /> }/>
          </Routes>
      </Router>
    </div>
  );
}

export default App;

AddContact.js

import React from "react";
import { Link } from "react-router-dom";

class AddContact extends React.Component {

    state = {
        name: "",
        email: ""
    }

    add = (e) => {

        e.preventDefault();
        if (this.state.name === "" || this.state.email === "") {
            alert("Enter name and email!");
            return;
        }
        this.props.addContactAction(this.state);
        this.setState({ name: "", email: ""});
    };

    render() {

        return (
            <div className="container">
                <form onSubmit={ this.add }>

                    <div className="row">
                        <div className="col-sm-12 mt-5">
                            <h2>Add Contact</h2>
                        </div>
                            <div className="col-sm-6">
                                <label for="name">Name</label>
                                <input type="text" id="name" className="form-control" placeholder="name" aria-label="name" value={this.state.name} onChange={ (e) => this.setState({name: e.target.value}) }/>
                            </div>
                            <div className="col-sm-6">
                                <label for="email">Email</label>
                                <input type="text" id="email" className="form-control" placeholder="email" aria-label="email" value={this.state.email } onChange={ (e) => this.setState({email: e.target.value}) }/>
                            </div>
                            <div className="col-sm-12 mt-3">
                                <Link to="/">
                                    <button className="btn btn-primary">Add</button>
                                </Link>
                            </div>
                    </div>

                </form>

            </div>
        );
    }

}

export default AddContact;

EditContact.js

import React from "react";
import { Link, useLocation } from 'react-router-dom';
import ContactCard from "./ContactCard";
import ContactDetail from "./ContactDetail";

class EditContact extends React.Component {

    constructor(props){
        super(props);

        this.state =  { 
            id: props.editContactPerson[0].id,
            name: props.editContactPerson[0].name,
            email: props.editContactPerson[0].email
         };

    }
    update = (e) => {
        e.preventDefault();
        if(this.state.name !== "" && this.state.email !== "") {
            this.props.updateContactPerson(this.state);
        } else {
            alert("All fields are mandatory!");
        }
    };

    render() {

        return (
            <div className="container">
                <form onSubmit={ this.update }>

                    <div className="row">
                        <div className="col-sm-12 mt-5">
                            <h2>Edit Contact</h2>
                        </div>
                            <div className="col-sm-6">
                                <label for="name">Name</label>
                                <input type="text" id="name" className="form-control" placeholder="name" aria-label="name" value={this.state.name} onChange={ (e) => this.setState({name: e.target.value}) }/>
                            </div>
                            <div className="col-sm-6">
                                <label for="email">Email</label>
                                <input type="text" id="email" className="form-control" placeholder="email" aria-label="email" value={this.state.email } onChange={ (e) => this.setState({email: e.target.value}) }/>
                            </div>
                            <div className="col-sm-12 mt-3">
                                <Link to="/">
                                    <button className="btn btn-primary">Update</button>                                
                                </Link>
                            </div>
                    </div>

                </form>

            </div>
        );
    }

}

export default EditContact;

Solution

  • The issue as I see it is that the click event from the button element propagates up to the Link component and that navigation to "/" effectively kills anything the current page/component is processing. This means the form element's onSubmit handler isn't called.

    You've a couple options:

    1. Add an onClick event handler to the button and call stopPropagation on the click event object to prevent it from bubbling up to the Link.
    2. Add an onClick event handler to the Link component and call preventDefault on the click event object.

    In either case the goal here is to prevent the immediate navigation from occurring, so the add and update handlers will need to issue an imperative redirect.

    An example:

    import { Link, useNavigate } from "react-router-dom";
    
    const AddContact = ({ addContactAction }) => {
      const navigate = useNavigate();
    
      const [{ email, name }, setState] = React.useState({
        name: "",
        email: ""
      });
    
      const changeHandler = (e) => {
        const { name, value } = e.target;
        setState(state => ({
          ...state,
          [name]: value,
        }));
      };
    
      const add = (e) => {
        e.preventDefault(); // <-- prevents default form action
        if (name === "" || email === "") {
          alert("Enter name and email!");
          return;
        }
        addContactAction(state);
        navigate("/", { replace: true }); // <-- navigate upon successful submit
      };
    
      return (
        <div className="container">
          <form onSubmit={add}>
            <div className="row">
              <div className="col-sm-12 mt-5">
                <h2>Add Contact</h2>
              </div>
              <div className="col-sm-6">
                <label htmlFor="name">Name</label>
                <input
                  type="text" 
                  id="name"
                  name="name"
                  className="form-control" 
                  placeholder="name"
                  aria-label="name"
                  value={name} 
                  onChange={changeHandler}
                />
              </div>
              <div className="col-sm-6">
                <label htmlFor="email">Email</label>
                <input
                  type="text"
                  id="email"
                  name="email"
                  className="form-control"
                  placeholder="email"
                  aria-label="email" 
                  value={email}
                  onChange={changeHandler}
                />
              </div>
              <div className="col-sm-12 mt-3">
                <Link
                  to="/"
                  onClick={e => e.preventDefault()} // <-- prevent default link action
                >
                  <button className="btn btn-primary">Add</button>
                </Link>
              </div>
            </div>
          </form>
        </div>
      );
    };