6

I want to perform navigation on certain user actions, say onSubmit of a button. suppose a user clicks on the Add contact button I want react-router to redirect in "/" which is the home page. At the moment I am facing this problem--> TypeError: Cannot read properties of undefined (reading 'push'). As a beginner, I would really appreciate experts' help.

AddContacts.js

import React, { Component } from "react";
import { Consumer } from "../../context";
import TextInputGroup from "../layout/TextInputGroup";
import { v4 as uuidv4 } from "uuid";
import { useNavigate } from "react-router-dom";

class AddContacts extends Component {
  state = {
    name: "",
    email: "",
    phone: "",
    errors: {},
  };
  onSubmit = (dispatch, e) => {
    e.preventDefault();

    const { name, email, phone } = this.state;

    //Check for errors

    if (name === "") {
      this.setState({ errors: { name: "Name is required" } });
      return;
    }
    if (email === "") {
      this.setState({ errors: { email: "Email is required" } });
      return;
    }
    if (phone === "") {
      this.setState({ errors: { phone: "Phone is required" } });
      return;
    }

    const newContact = {
      id: uuidv4(),
      name,
      email,
      phone,
    };
    dispatch({ type: "ADD_CONTACT", payload: newContact });

    this.setState({
      name: "",
      email: "",
      phone: "",
      errors: {},
    });
    this.props.navigate.push("/");
  };

  onChange = (e) => this.setState({ [e.target.name]: e.target.value });
  render() {
    const { name, email, phone, errors } = this.state;

    return (
      <Consumer>
        {(value) => {
          const { dispatch } = value;

          return (
            <div className="card mb-3">
              <div className="card-header">Add Contacts</div>
              <div className="card-body">
                <form onSubmit={this.onSubmit.bind(this, dispatch)}>
                  <TextInputGroup
                    label="Name"
                    name="name"
                    placeholder="Enter Name..."
                    value={name}
                    onChange={this.onChange}
                    error={errors.name}
                  />
                  <TextInputGroup
                    label="Email"
                    name="email"
                    type="email"
                    placeholder="Enter Email..."
                    value={email}
                    onChange={this.onChange}
                    error={errors.email}
                  />
                  <TextInputGroup
                    label="Phone"
                    name="phone"
                    placeholder="Enter Phone..."
                    value={phone}
                    onChange={this.onChange}
                    error={errors.phone}
                  />
                  <input
                    type="submit"
                    value="Add Contact"
                    className="btn btn-light btn-block mt-3"
                  />
                </form>
              </div>
            </div>
          );
        }}
      </Consumer>
    );
  }
}

export default AddContacts;

Here is the App.js file

import React, { Component } from "react";
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";

import Contacts from "./components/contacts/Contacts";
import Header from "./components/layout/Header";
import AddContacts from "./components/contacts/AddContacts";
import About from "./components/pages/About";

import { Provider } from "./context";

import "bootstrap/dist/css/bootstrap.min.css";
import "./App.css";

function App() {
  return (
    <Provider>
      <BrowserRouter>
        <div className="App">
          <Header branding="Contact manager" />
          <div className="container">
            <Routes>
              <Route path="/" element={<Contacts />} />{" "}
              <Route path="/contact/add/*" element={<AddContacts />} />{" "}
              <Route path="about/*" element={<About />} />{" "}
            </Routes>{" "}
          </div>{" "}
        </div>{" "}
      </BrowserRouter>{" "}
    </Provider>
  );
}

export default App;
Oli Ullah
  • 161
  • 1
  • 2
  • 9
  • Have you looked through this example in the docs? It covers this exact use-case via the `useNavigation` hook. https://reactrouter.com/docs/en/v6/examples/auth – Jake Worth Nov 09 '21 at 15:01

2 Answers2

7

Issue

TypeError: Cannot read properties of undefined (reading 'push')

This is cause by you attempting to navigate from a navigate prop that doesn't exist, it's undefined.

this.props.navigate.push("/");

The useNavigate hook is only compatible with function components, so of you want/need to use navigate with a class component you must either convert AddContacts to a function component, or roll your own custom withRouter Higher Order Component to inject the "route props" like the withRouter HOC from react-router-dom v5.x did.

Solution

I won't cover converting a class component to function component. Here's an example custom withRouter HOC:

const withRouter = WrappedComponent => props => {
  const navigate = useNavigate();
  // etc... other react-router-dom v6 hooks

  return (
    <WrappedComponent
      {...props}
      navigate={navigate}
      // etc...
    />
  );
};

And decorate the AddContacts component with the new HOC.

export default withRouter(AddContacts);

This will now pass a navigate prop (and any others you set up) to the decorated components and this.navigate will now be defined.

Additionally, the navigation API changed from v5 to v6, it's no longer the direct history object being used. navigate is a function instead of an object. To use you invoke the function and pass 1 or 2 arguments, the first is the target path, the second is an optional "options" object with replace and/or state key/values.

interface NavigateFunction {
  (
    to: To,
    options?: { replace?: boolean; state?: State }
  ): void;
  (delta: number): void;
}

To navigate now as follows:

this.props.navigate("/");
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • Is there any other way to navigate programmatically other than converting to functional components, for class based components? – zaidfazil Feb 02 '22 at 09:33
  • There should be a way right? I mean, is there any chance of dropping support for class based components. – zaidfazil Feb 02 '22 at 09:34
  • @zaidfazil What other sort of way are you looking for? You could instead create a wrapper component that accesses the hooks and passes the props to a class component. What does your second comment/question about, "is there any chance of dropping support for class based components?" Do you have a specific issue that applying this solution to isn't working as expected? – Drew Reese Feb 02 '22 at 16:17
  • Earlier I was working with router v5, we could just navigate to another route using just `this.props.history.push()`. But now, in order to get the exact functionality, I'm required to write a wrapper HOC function, only just to pass the `navigate` object which is basically similar to `history` object from earlier case. My question is that, is there another solution which does not involve writing additional components to wrap my class based components and achieve programmatic routing in react-router v6? Hope I have phrased my query in an understandable way. – zaidfazil Feb 22 '22 at 13:46
  • 1
    @zaidfazil Not really. The RRDv6 higher-level routers all maintain their own internal `history` object, and only expose imperative navigation via the `navigate` function returned from the `useNavigate` hook. Other than converting the class components to function components, the other alternative I know of is to create a [custom router](https://stackoverflow.com/a/70000286/8690857) component so you can use a custom `history` object directly. This removes `history` as a passed prop and the need to use the wrapper or HOC. – Drew Reese Feb 23 '22 at 19:28
  • 1
    Thank you @Drew for your kind and very detailed explanation. I realize it now, the silliness of my question. It was a misunderstanding or TBH less understanding from my part. Thank you again for bearing with me. I get it now, how to work with functional components using v6 along with my code base. – zaidfazil Mar 01 '22 at 06:22
  • @DrewReese - You shown the `interface NavigateFunction {` to let us understand about the way `useNavigate` implemented? – Common Man Aug 26 '22 at 07:48
  • @CommonMan Affirmative. Knowing the function's interface allows you to know what can be passed to it, i.e. how you can call the function from your code. – Drew Reese Aug 26 '22 at 07:51
0

How to redirect in React Router v6

import {  useNavigate  } from "react-router-dom";

  const navigate = useNavigate();
  
  const handleClick = () => {
    navigate("/dashboard");
  };
Avnish Jayaswal
  • 161
  • 1
  • 4