9

I'm building a react app and I can't make the routing work.

  1. I need one common layout (header, footer) for a few Auth routes (/login, sign-up, forgot-password, etc...)

  2. And I need need another common layout for the rest of the protected parts of the app (Home, Dashboard, etc...)

  3. I need another 404 page without any layout.

I've tried several techniques from those links:

But could reach working version.

This is what I'm currently have:

(Note: for now I'm ignoring the need to block non logged-in users into the private routes of AppLayout, I'll handle that right after)

const App: React.FC = () => {
    const history = createBrowserHistory();

    return (
        <div className="App">
            <Router history={history}>
                <Switch>
                    <AppLayout>
                        <Route path="/home" component={HomePage}/>
                        <Route path="/dashboard" component={DashboardPage}/>
                        ...
                    </AppLayout>
                    <AuthLayout>
                        <Route path="/login" component={LoginPage}/>
                        <Route path="/sign-up" component={SignUpPage}/>
                        ...
                    </AuthLayout>
                    <Route path="*" component={NotFoundPage} />
                </Switch>
            </Router>
        </div>
    );
};

export default App;

Both AuthLayout and AppLayout are simple and similar to that (just with different header/footer for each):

class AppLayout extends Component {
    render() {
        return (
            <div className="AppLayout">
                <header>...</header>
                {this.props.children}
                <footer>...</footer>
            </div>
        );
    }
}

export default AppLayout;

The problem is that only routes from the AppLayout are rendered. Other routes just showing the AppLayout header and footer without any content.

Those are the react versions I'm using:

    "react": "^16.8.6",
    "react-dom": "^16.8.6",
    "react-router-dom": "^5.0.0",

Any help would be appreciated.

Thanks in advance.

ET-CS
  • 6,334
  • 5
  • 43
  • 73

4 Answers4

20

Each of your layout should have a path component to differentiate from other layouts.

For example

Auth layouts could reside under /auth eg, login would /auth/login, signup would be /auth/signup

App layout could go under /app eg, dashboard would be /app/dashboard, home would be /app/home

Working Demo

Edit hungry-dubinsky-q1l62

App.js

import { Switch, BrowserRouter, Route, Redirect } from "react-router-dom";

function App() {
  return (
    <BrowserRouter>
      <Layouts />
    </BrowserRouter>
  );
}

Layouts.js

const NotFound = () => <h1>Not Found</h1>;

function Layouts() {
  return (
    <Switch>
      <Route path="/auth" component={AuthLayout} />
      <Route path="/app" component={AppLayout} />
      <Route path="/" component={NotFound} />
    </Switch>
  );
}

AuthLayout

const Signup = () => <p>Login</p>;
const Login = () => <p>Sign up</p>;

function AuthLayout() {
  return (
    <div>
      <h1>Auth Layout</h1>
      <Route path="/auth/signup" exact component={Signup} />
      <Route path="/auth/login" exact component={Login} />
      <Redirect from="/auth" to="/auth/login" exact />
    </div>
  );
}

AppLayout

const Home = () => <p>Home</p>;
const Dashboard = () => <p>Dashboard</p>;

function AppLayout() {
  return (
    <div>
      <h1>App Layout</h1>
      <Route path="/app/home" exact component={Home} />
      <Route path="/app/dashboard" exact component={Dashboard} />
      <Redirect from="/app" to="/app/home" exact />
    </div>
  );
}

Also if you want to protect certain routes from being rendered if not authenticated, then you can create a PrivateRoute component that would redirect to auth layout if not authenticated.

PrivateRoute.js

const PrivateRoute = ({ component: Component, ...rest }) => (
  <Route
    {...rest}
    render={props => sessionStorage.token // your auth mechanism goes here
      ? <Component {...props} />
      : <Redirect to={{ pathname: '/auth' }} />}
  />
);

You can use this PrivateRoute component instead of react-router's Route component.

Eg:

<PrivateRoute path="/app" component={AppLayout} />

johnny peter
  • 4,634
  • 1
  • 26
  • 38
  • 1
    Thank you! though I prefer a solution not restricting me to change the urls (/app, /auth) – ET-CS Jun 28 '19 at 20:56
  • 2
    This solution helped me figure it out. It's better than the accepted answer imo. – therebelcoder Oct 25 '19 at 16:01
  • 1
    As someone with angular background, this right here, is the most sensible, cleanest routing approach I've seen in React so far. There's too much gibberish approaches with React routing out there. This is clean, readable, obvious and easy to extend. Thank you – KhoPhi Jan 31 '20 at 10:24
  • 1
    if you go to something like `/app/somethingnotfound` it doesn't work, it doesn't go to `NotFound` component – Vencovsky Jun 20 '20 at 14:14
  • @Vencovsky by removing exact from root Layouts route you should be able to navigate to /app/somthing – rake7h Aug 08 '20 at 07:21
7

It took me some time to find the answer that I favoured. Both @johnny-peter & @gaurab-kc solutions were great and they tought me about React's routing mechanism.

@johnny-peter 's solution had disadvantage of forcing me to put a prefix for all auth related routes under /auth/... (e.g. /auth/login & auth/sign-up) which I didn't wanted.

@gaurab-kc solution was supporting only one set of routes.. so if user was already signed up, he couldn't visit the /login route anymore.

Up till recently I used my own solution which had the problem of "defeating the whole purpose of a common header and footer." as @johnny-peter mentioned and it was down-voted few times as it should be.

Now I'm using another solution:

<Router history={browserHistory}>
    <Switch>
        <Redirect exact from="/" to="/home"/>
        <Route exact path={["/login", "/sign-up", ...]}>
            <AuthLayout>
                <Switch>
                    <Route
                        path="/login"
                        component={LoginPage}
                    />
                    <Route
                        path="/sign-up"
                        component={SignUpPage}
                    />
                </Switch>
            </AuthLayout>
        </Route>
        <Route exact path={[
            "/home",
            "/dashboard",
            ...
        ]}>
            <SiteLayout>
                <Switch>
                    <Route
                        path="/home"
                        component={HomePage}
                    />
                    <Route
                        path="/dashboard"
                        component={DashboardPage}
                    />
                </Switch>
            </SiteLayout>
        </Route>
        <Route path="*" component={NotFoundPage}/>
    </Switch>
</Router>

which prevents all the above disadventages. It's allows me to:

  1. Use a layout for each section which isn't re-rendered on route change.
  2. Doesn't forcing me to add any prefix to routes.
  3. All routes are working at the same time, letting users to route back into the /login or other auth routes without logout first.

The only disadvantage of this solution is having more code and duplicating the routes, but it's a cost I'm willing to pay.

ET-CS
  • 6,334
  • 5
  • 43
  • 73
4

You could try having two different switch statements to handle your Auth and Protected routes. I had a similar use case at work and having two sets of switch blocks with only one running at one time was the way for me.

const App: React.FC = () => {
    const history = createBrowserHistory();

    return (
        <div className="App">
            <Router history={history}>
                {isLoggedIn ? <PrivateRoutes /> : <AuthRoutes />}
            </Router>
        </div>
    );
};


const PrivateRoutes: React.FC = () => {
    return (
        <>
            <Header />
            <Switch>
                <Route path="/home" component={HomePage} />
                <Route path="/dashboard" component={DashboardPage} />
                <Route path="*" component={NotFoundPage} />
            </Switch>
            <Footer />
        </>
    );
};

const AuthRoutes: React.FC = () => {
    return (
        <>
            <Header />
            <Switch>
                <Route path="/login" component={LoginPage} />
                <Route path="/sign-up" component={SignUpPage} />
                <Route path="*" component={NotFoundPage} />
            </Switch>
            <Footer />
        </>
    );
};
Gaurab Kc
  • 841
  • 9
  • 9
  • This is nice solution! thank you! I ended up using another solution which I'll cover in an answer of my own but I learned a lot from yours! – ET-CS Jun 28 '19 at 20:57
-3

EDIT: I have answered this question with another solution.


Both solutions by @Gaurab Kc & @johnny peter are great though I've ended up doing something like that:

<Router history={history}>
    <Switch>
        <PrivateRoute
            path="/home"
            component={HomePage}>
        </PrivateRoute>
        <PrivateRoute
            path="/dashboard"
            component={DashboardPage}>
        </PrivateRoute>
        <AuthRoute
            path="/login"
            component={LoginPage}>
        </AuthRoute>
        <AuthRoute
            path="/sign-up"
            component={SignUpPage}>
        </AuthRoute>
        <Route path="*" component={NotFoundPage}/>
    </Switch>
</Router>

The AuthRoute & PrivateRoute are something like that:

interface PrivateRouteProps extends RouteProps {
    component: any;
}

const PrivateRoute = (props: PrivateRouteProps) => {
    const {component: Component, ...rest} = props;

    return (
        <Route
            {...rest}
            render={(routeProps) =>
                localStorage.getItem('user') ? (
                    <div>
                       ... // here is the app header
                       <Component {...routeProps} />
                       .. // here is the app footer
                    </div>
                ) : (
                    <Redirect
                        to={{
                            pathname: '/login',
                            state: {from: routeProps.location}
                        }}
                    />
                )
            }
        />
    );
};

export default PrivateRoute;
interface AuthRouteProps extends RouteProps {
    component: any;
}

const AuthRoute = (props: AuthRouteProps) => {
    const {component: Component, ...rest} = props;

    return (
        <Route
            {...rest}
            render={(routeProps) =>
                (
                    <div>
                       ... // here is the auth header
                       <Component {...routeProps} />
                       .. // here is the auth footer
                    </div>
                )
            }
        />
    );
};

export default AuthRoute;
ET-CS
  • 6,334
  • 5
  • 43
  • 73
  • 3
    This approach is totally wrong, reason being it defeats the whole purpose of layouts in an SPA. Your layout contains ui elements common to specific screens which don’t re render when routes change within a layout. In your approach when you switch between different routes, within the same layout, all your common UI too gets destroyed and rerendered, defeating the whole purpose of a common header and footer. – johnny peter Jun 29 '19 at 02:15
  • @johnnypeter This is valid and important point. But at this stage this is something I would rather pay over a solution that make me set routes to be `/auth` & `/app`. I would happy to know if there is any other solution. thank you. – ET-CS Jun 29 '19 at 12:29