Search code examples
javascriptreactjsreact-routerreact-router-dom

Why is React Router not routing to a nested route?


I am using react-router-dom version 6.11.2 for my front-end routing in my App.js file:

import "./App.css";
import {
  Route,
  RouterProvider,
  createBrowserRouter,
  createRoutesFromElements,
} from "react-router-dom";
import RootLayout from "./layouts/RootLayout";
import { UserContextProvider } from "./store/user-context";
import NotFound404 from "./components/Utility/NotFound404";
import Login from "./pages/general/Login";
import CreateUser from "./pages/general/CreateUser";

const clientRouter = createBrowserRouter(
  createRoutesFromElements(
    <>
      <Route path="/auth" element={<Login />}>
        <Route path="new-user" element={<CreateUser />} />
      </Route>
      <Route path="/" element={<RootLayout />} errorElement={<NotFound404 />}>
        {/* children routes */}
      </Route>

    </>
  )
);

function App() {
  return (
    <>
      <UserContextProvider>
        <RouterProvider router={clientRouter}></RouterProvider>
      </UserContextProvider>
    </>
  );
}

export default App;

However, when routing to "/auth/new-user", the CreateUser component is not rendered.

Login:

import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import styles from "./Login.module.css";
import Title from "../../components/Layout/Title";

const Login = () => {
  console.log("login rendered");
  const navigate = useNavigate();
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  const handleSubmit = async (event) => {
    event.preventDefault();

    console.log("Login submitted:", email, password);

    try {
      const response = await fetch("http://localhost:8000/login", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email, password }),
      });

      if (response.ok) {
        //do something
      } else {
        //do something
      }
    } catch (error) {
      console.log(error);
      //handle error
    }
  };

  return (
    <>
      <Title />
      <div className={styles.loginForm}>
        <h2>Login</h2>
        <form onSubmit={handleSubmit}>
          <div className={styles.formControl}>
            <label htmlFor="email">Email:</label>
            <input
              type="email"
              id="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              required
            />
          </div>
          <div className={styles.formControl}>
            <label htmlFor="password">Password:</label>
            <input
              type="password"
              id="password"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              required
            />
          </div>
          <button type="submit">Login</button>
        </form>
        <Link to={"new-user"}>Create New User</Link>
      </div>
    </>
  );
};

export default Login;

CreateUser:

import styles from "./Login.module.css";
import react, { useState } from "react";

const CreateUser = () => {
  console.log("create user rendered");
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [confirmPassword, setConfirmPassword] = useState("");

  const handleSubmit = async (event) => {
    event.preventDefault();

    try {
      // Validation: Check if passwords match
      if (password !== confirmPassword) {
        // Handle password mismatch error
        return;
      }

      // Perform new user registration logic here

      console.log("New user registered:", email, password);
    } catch (error) {
      console.log(error);
      // Handle error
    }
  };

  return (
    <>
      <div className={styles.loginForm}>
        <h2>Create New User</h2>
        <form onSubmit={handleSubmit}>
          <div className={styles.formControl}>
            <label htmlFor="email">Email:</label>
            <input
              type="email"
              id="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              required
            />
          </div>
          <div className={styles.formControl}>
            <label htmlFor="password">Password:</label>
            <input
              type="password"
              id="password"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              required
            />
          </div>
          <div className={styles.formControl}>
            <label htmlFor="confirmPassword">Confirm Password:</label>
            <input
              type="password"
              id="confirmPassword"
              value={confirmPassword}
              onChange={(e) => setConfirmPassword(e.target.value)}
              required
            />
          </div>
          <button type="submit">Register</button>
        </form>
      </div>
    </>
  );
};

export default CreateUser;

All my other front-end routing that is defined after the "/auth" route, including the children routes work without any issues, and when I define the "/new-user" route as an absolute path sibling to the "/auth" route, CreateUser renders properly. Is there some specific issue with nesting routes that I am not seeing?


Solution

  • If you would like CreateUser to be rendered as a nested child route of the Login component on "/auth" then Login necessarily should render an Outlet component for the nested route(s) to render its/their element content to.

    See Layout Routes and Outlets for more details.

    import React, { useState } from "react";
    import {
      Link,
      Outlet, // <-- Import Outlet component
      useNavigate
    } from "react-router-dom";
    import styles from "./Login.module.css";
    import Title from "../../components/Layout/Title";
    
    const Login = () => {
      ...
    
      return (
        <>
          <Title />
          <div className={styles.loginForm}>
            <h2>Login</h2>
            <form onSubmit={handleSubmit}>
              ...
            </form>
            <Link to={"new-user"}>Create New User</Link>
          </div>
          <Outlet /> {/* <-- Render Outlet for nested routes */}
        </>
      );
    };
    
    export default Login;
    

    If you happen to instead only want Login and CreateUser to be "sibling" components, each rendered on their own page then demote the Login component down to a nested route.

    const clientRouter = createBrowserRouter(
      createRoutesFromElements(
        <>
          <Route path="/auth"> {/* <-- Renders an Outlet by default */}
            <Route
              index {/* <-- Renders on parent's path, e.g. "/auth"  */}
              element={<Login />}
            />
            <Route path="new-user" element={<CreateUser />} />
          </Route>
          <Route
            path="/"
            element={<RootLayout />}
            errorElement={<NotFound404 />}
          >
            {/* children routes */}
          </Route>
        </>
      )
    );