React Router Routing three levels deep, Only works in App.js file

Viewed 987

I am building a website with the following link structure:

  1. /blog
  2. /blog/post1
  3. /preview

The /blog url lists all the blog posts, the /blog/post1 shows the post in edit view and the preview button in the same edit view links to /preview url that shows the preview of the edited version. My initial idea was to use a /blog/post1/preview url but with the use of <Switch> it became complicated. It did not work because of the whole exact issue where I had to correctly match three urls that start with the same address. So I switched to /preview. Now, I can get the first to urls working but the /preview renders nothing on the screen. Is it because of where my <Link> and <Route> are defined in my app. I have following structure:

  <Switch>
     <Route path='/blog'>
       <Blog/>
     </Route>
   <Switch>

in the main app.js

     <Route exact path='/blog'>
       //div with some helper views like search bar
     </Route>

     //it is inside map function that renders a card view of many posts, post1 for simplicity
     <Route path='/blog/post1'>
       <PostEditor/>
     </Route>

in the main Post.js component

     <Route path='/preview'>
       //Content
     </Route>

in the main PostEditor.js The PostEditor.js has the Link and Route tags in the same file but not others. Is it a reason that it renders nothing. When I move the Route tag from PostEditor, to App.js, everything works. Is it a nesting issue or something like that. I tried searching online but did not have much luck with multiple levels of nesting, usually it is two. Any help is greatly appreciated.
I am using react-router 5.2.0 and HashRouter as it works better with github

Update: I have added a sample sandbox to mimic my scenario. Why is the /preview link not rendering anything in this case? I would like to know what I am doing wrong in this case, cause I feel there is a knowledge gap about something about react-router that I am missing about creating Links and Routes at different level? Additionally, what would be the best alternative to handle what I am currently doing.


Code SandBox

2 Answers

I don't see any route nesting here, but I suspect the issue you are running into is with path specificity and order.

Within the Switch you should order your routes to list more specific paths before less specific paths. Recall that the Switch returns and renders the first match is finds, so by trying to match more specific paths first you avoid accidentally matching a less specific path prefix. In other words, "/blog" is a path prefix for the other sub-routes so you want to match it last.

  1. "/blog/:postId/preview"
  2. "/blog/:postId"
  3. "/blog"

Code:

<Switch>
  <Route path='/blog/:postId/preview'>
    //Content
  </Route>
  <Route path='/blog/:postId'>
    <PostEditor/>
  </Route>
  <Route path='/blog'>
    <Blog/>
  </Route>
<Switch>

Edit

Why is the /preview link not rendering anything in this case? I would like to know what I am doing wrong in this case, cause I feel there is a knowledge gap about something about react-router that I am missing about creating Links and Routes at different level?

The "/preview" link renders nothing because the component rendered on "/blog/post1" (i.e. Post) is now no longer being rendered. In other words, the URL is updated to "/preview" and Post is unmounted. Since you've no "/preview" route in the root nothing is matched and rendered.

Solution based on your added codesandbox

You can certainly nest routes. Here are the changes I suggest to properly nest blog routes.

App.js - Specificity and order matters, place more specific root paths before less specific paths.

<Switch>
  <Route path="/blog">
    <Blog />
  </Route>
  <Route path="/">
    <Home />
  </Route>
</Switch>

Blog.js - Render nested routes into another Switch component, again paying attention to path specificity and order. Use the useRouteMatch hook to access the current path and url props, path for nested route matching and url for nested links. Notice the route from Post for the preview is moved here.

const Blog = () => {
  const { path, url } = useRouteMatch();
  return (
    <div className="blog">
      <Switch>
        <Route path={`${path}/:postId/preview`} exact>
          Blog Preview Before Save
          <br />
        </Route>
        <Route path={`${path}/post1`}>
          <Post />
        </Route>
        <Route path={path}>
          Blog Search Bar
          <br />
          <Link to={`${url}/post1`}>Post 1</Link>
        </Route>
      </Switch>
    </div>
  );
};

Post.js - Again, access the url from useRouteMatch to construct the nested link to the preview route.

const Post = () => {
  const { url } = useRouteMatch();
  return (
    <div className="post">
      <Link to={`${url}/preview`}>Preview</Link>
    </div>
  );
};

Demo

Edit react-router-routing-three-levels-deep-only-works-in-app-js-file

Note: Whether or not you "nest" Route components doesn't really matter in the grand scheme of things, the overall path structure will be identical. You can just as easily (probably easier in fact) define all your routes at the root level, and this is common and perfectly acceptable. The trade off may be in maintenance if you want to "nest" a sub-route elsewhere, you'll need to "find-and-replace" all the particular intermediate path segments versus the "nesting solution" you only need to update the path root/prefix. The "nesting solution" is more dynamic in this sense.

With route /preview we can not identify which blog we are viewing. Pasting a minimum working route configuration bellow

import React, {useState, useEffect} from 'react'
import {Link, BrowserRouter, Switch, Route, useParams} from 'react-router-dom'

#app.jsx
<Switch>
  <Route path="/blog" exact>
    <Blogs />
  </Route>
  <Route path="/blog/:id" exact>
    <PostEditor />
  </Route>
  <Route path="/blog/:id/preview">
    <Blog />
  </Route>
 </Switch>


const Blogs =  () => {
  return(
    <h1>Blog list</h1>
  )
}

const Blog = () => {
  const {id} = useParams()
  return(
    <h1>Blog {id}</h1>
  )
}

const PostEditor = () => {
  const {id} = useParams()
  return(
    <h1>Edit Blog {id}</h1>
  )
}

Update: As I uderstand, You need to display preview right after editing form without a routing switch. For that a button wich trigger show preview element can be used.

import React, {useState} from "react";
import { Route } from "react-router-dom";
import { HashLink as Link } from "react-router-hash-link";

const Post = () => {
  const [showPreview, setShowPreview] = useState(false);
  const [editData, setEditData] = useState('Some Post attributes')
  return(<div className="post">
      
    {showPreview ? <Preview editData={editData} /> : null}
    
    <button onClick={() => {setShowPreview(true)}}>Preview</button>
  </div>)
};
Preview = ({editData}) => {
  return(
    <h1>{editData}</h1>
  )
}
export default Post;
Related