Search code examples
javascriptreactjsreact-routerfrontendreact-router-dom

Valid route path is displaying both valid path component and non existing path component


When i hit locathost:3000/faq or any other valid routes React is displaying both valid routes content and NotFound(basically 404 custom page) component at the bottom of page. I want to display NotFound component content only if I hit invalid route Ex: localhost:3000/not-existing-url

const routes = [
  {
    path: '/',
    component: HomePage,
    is_private: false,
  },
  {
    path: '/faq',
    component: RedirectFaqToSupport,
    is_private: false,
  },
  {
    path: '/about-us',
    component: About,
    is_private: false,
  },
  {
    path: '/contact-us',
    component: Contact,
    is_private: false,
  },
 {
    component: NotFound,
    is_private: false,
    path: '*'
  },
]

Here is my App.js component code.

class App extends Component {
  constructor() {
    super()
    this.state = {
      redirectToReferrer: false, // may not need this anymore
    }
  }

  render() {
    const { isMobile } = this.props

    const renderLoader = () => (
      <Columns.Column size={12}>
        <div className="has-text-centered" style={{ marginTop: '15%' }}>
          <i className="fas fa-3x fa-cog fa-spin"></i>
        </div>
      </Columns.Column>
    )

    return (
      <Suspense fallback={renderLoader()}>
        <BrowserRouter>
          <Provider>
            <ScrollToTop />
            <Subscribe to={[AppContainer]}>
              {(auth) => {
                return (
                  <>
                    <Header
                      isAuth={auth.state.isAuth}
                      modalViewType={auth.state.modalViewType}
                      unreadMessageCount={auth.state.unreadMessageCount}
                      auth={auth}
                    />
                    <ErrorBoundary>
                      <div className="page-master-container">
                        <Switch>
                          <BuildRoutes
                            isMobile={isMobile}
                            isAuth={auth.state.isAuth}
                            auth={auth}
                          />
                        </Switch>
                      </div>
                    </ErrorBoundary>
                    <Footer isAuth={auth.state.isAuth} />
                  </>
                )
              }}
            </Subscribe>
          </Provider>
        </BrowserRouter>
      </Suspense>
    )
  }
}

Here is BuidRoutes component code

const PrivateRoute = ({ component: Component, ...rest }) => (
  <Route
    {...rest}
    render={(props) =>
      rest.isAuth === true ? (
        <Component {...props} {...rest} />
      ) : (
        <Redirect
          to={{
            pathname: '/login',
            state: { from: props.location },
          }}
        />
      )
    }
  />
)

export const BuildRoutes = ({ isMobile, isAuth, auth }) => {
  const location = useLocation()

  useEffect(() => {
    const queryParams = QueryString.parse(location.search)
    setFeatureFlag(queryParams)
  }, [location])

  return (
    <>
      {routes.map(
        ({ path, component: Component, is_private, is_mother }, key) =>
          !is_private ? (
           
              <Route
                exact={is_mother ? false : true}
                path={path}
                key={key}
                render={(props) => (
                  <Component
                    {...props}
                    isMobile={isMobile}
                    isAuth={isAuth}
                    auth={auth}
                  />
                )}
              />
           
          ) : (
            
              <PrivateRoute
              isAuth={isAuth}
              isMobile={isMobile}
              auth={auth}
              exact={is_mother ? false : true}
              path={path}
              key={key}
              component={Component}
            />
    
            
          )
      )}
      <Route
        exact
        path="/brand/:brandName"
        render={(props) => {
          const { brandName } = props.match.params
          if (bikeBrands.includes(brandName)) {
            return <Redirect to={`/brand/${brandName}-rental`} />
          }
          return <BrandLandingPage brandName={brandName} />
        }}
      />
    </>
  )
}

Note: Also I tried below method but it didn't help. Still displays NotFound component along with valid routes component

export const BuildRoutes = ({ isMobile, isAuth, auth }) => {
  const location = useLocation()

  useEffect(() => {
    const queryParams = QueryString.parse(location.search)
    setFeatureFlag(queryParams)
  }, [location])

  return (
    <>
      {routes.map(
        ({ path, component: Component, is_private, is_mother }, key) =>
          !is_private ? (
            <Route
              exact={is_mother ? false : true}
              path={path}
              key={key}
              render={(props) => (
                <Component
                  {...props}
                  isMobile={isMobile}
                  isAuth={isAuth}
                  auth={auth}
                />
              )}
            />
          ) : (
            <PrivateRoute
              isAuth={isAuth}
              isMobile={isMobile}
              auth={auth}
              exact={is_mother ? false : true}
              path={path}
              key={key}
              component={Component}
            />
          )
      )}
      <Route
        component={NotFound}
      />
      <Route
        exact
        path="/brand/:brandName"
        render={(props) => {
          const { brandName } = props.match.params
          if (bikeBrands.includes(brandName)) {
            return <Redirect to={`/brand/${brandName}-rental`} />
          }
          return <BrandLandingPage brandName={brandName} />
        }}
      />
    </>
  )
}

I am using "react-router-dom": "^5.2.0",


Solution

  • Issue

    The Switch component renders the first child <Route> or <Redirect> that matches the location. This has the unfortunate result of also rendering the first non-Route and non-Redirect child it comes across, BuildRoutes in this case.

    <Switch>
      <BuildRoutes // <-- first child/match and always rendered
        isMobile={isMobile}
        isAuth={auth.state.isAuth}
        auth={auth}
      />
    </Switch>
    

    Key points to keep in mind:

    • All routes rendered within a router are always inclusively matched and rendered. In other words, anything that matches the URL path is rendered.
    • All routes rendered within a Switch component are exclusively matched and rendered, e.g. the "first matching child...".

    The BuildRoutes component is inclusively rendering the routes it renders since the routes are not directly children of a Switch component. This means that the NotFound component rendered on path="*" will always match and always get rendered.

    Solution

    Move the Switch into BuildRoutes so that it directly renders the routes as children and so can apply exclusive route matching and rendering.

    <ErrorBoundary>
      <div className="page-master-container">
        <BuildRoutes
          isMobile={isMobile}
          isAuth={auth.state.isAuth}
          auth={auth}
        />
      /div>
    </ErrorBoundary>
    
    const PrivateRoute = ({ isAuth, ...props }) => {
      const from = useLocation();
    
      return isAuth
        ? <Route {...props} />
        : <Redirect to={{ pathname: '/login', state: { from } }} />;
    };
    
    export const BuildRoutes = ({ isMobile, isAuth, auth }) => {
      const location = useLocation();
    
      useEffect(() => {
        const queryParams = QueryString.parse(location.search);
        setFeatureFlag(queryParams);
      }, [location]);
    
      return (
        <Switch>
          {routes.map(({ path, component, is_private, is_mother }) => {
            const ChildRoute = is_private ? PrivateRoute : Route;
            const Component = component;
    
            return (
              <ChildRoute
                key={path}
                exact={!is_mother}
                path={path}
                render={(props) => (
                  <Component {...props} {...{ isMobile, isAuth, auth }} />
                )}
              />
            );
          })}
          <Route
            exact
            path="/brand/:brandName"
            render={(props) => {
              const { brandName } = props.match.params
              if (bikeBrands.includes(brandName)) {
                return <Redirect to={`/brand/${brandName}-rental`} />
              }
              return <BrandLandingPage brandName={brandName} />
            }}
          />
        </Switch>
      );
    };