React does not need any introduction at all, and here I am not going to include a description of what and how it works. I tried to cover in more detail the main aspects of the React application architecture, which in the future will help you build an easily scalable project very quickly. We have developed exactly the architectural part of building applications using React Router v6. Also, I am going to use and at the end of the article, you can get the GitHub link to the repository. styled-components Application Structure Before starting any programming manipulations, it is better to accurately determine all routes in the application that will be involved. The research is better with some diagrams plan where you can see the whole general picture of your application. Also, it’s important to be clear about which routes have to be protected. Protection means when the user has access to a specific route only if he has authorization. If you are not authorized, other words are not logged in, then you can’t have that access to that route. And also a very important part of redirections. For example, if you have the authorization, what has to happen if you will go to the home route? Should you be redirected and where? All of those are to be identified at the beginning. Let’s take a look at the diagram with the application routing schema. redirection rules Initially, it’s important to divide protected and public routes. Here the green color means public and red is protected. Also, you can find some green boxes with protected paths, which means those protected routes will be with a different logic of redirection. Redirection has to happen to the first step if you came to the second step of registration without providing dates on the first step. Composition Navigation is something common among all routes but depends on the . The stack of links will be different if the user got authorization. In that way, all routes are better to wrap up to where you gonna have the logic of lifecycle and session injection, and, of course, what to show up in header navigation. It’s important to understand the state depends on the authorization. session MainLayout In our case, all components except and component depends on the . That means there is going to show before exposing the component in order to be clear on what to render. The folder structure contains a bunch of components, constants, and fakes for making API calls. View NotFound Navigation session Loader application-architecture-boilerplate ├── package.json └── src ├── App.js ├── __mocks__ │ └── db.mock.js ├── components │ ├── Footer.js │ ├── Loading.js │ ├── MainLayout.js │ ├── Navigation.js │ ├── ProtectedRoute.js │ └── PublicRoute.js ├── constants │ └── index.js ├── fake │ ├── fakeApi.js │ └── fakeCache.js ├── hooks │ └── useSession.js └── index.js Data Request As you can see that we going to use a fake API for our application. In reality, it can be an Apollo Client or REST API call, but in our case, we going to use a with for request delay. We have to take care of our authentication for login and logout and request the current session from a fake API. Eventually, it’s a simple class with amount useful methods. Promise Timeout 1s import { db } from 'src/__mocks__/db.mock'; import { fakeCache } from './fakeCache'; class FakeApi { static REQUEST_TIME = 1000; static KEY_CACHE = 'cache'; static DB = db; context = {}; constructor() { const context = fakeCache.getItem(FakeApi.KEY_CACHE); if (context) { this.context = context; } } static userById = id => FakeApi.DB.find(user => user.id === id); #asyncRequest = callback => new Promise(resolve => { setTimeout(() => { const result = callback(); resolve(result); }, FakeApi.REQUEST_TIME); }); getSession() { return this.#asyncRequest(() => this.context.session); } login() { this.context.session = FakeApi.userById(1); fakeCache.setItem(FakeApi.KEY_CACHE, this.context); return this.getSession(); } logout() { this.context = {}; fakeCache.clear(); return this.#asyncRequest(() => null); } } export const fakeApi = new FakeApi(); You can find out that in the we are using cache. It’s because our request has to have a cache for responses and use the cache as an advance for the next requests to improve performance. This implementation is quite crude and simple, but it’s easy to get the gist of it. constructor The flow is, that once we call, we have to create a session, and clears the session and cache as well. Each should have an as fake delay for our request. But what about the cache? logout asyncRequest REQUEST_TIME export const fakeCache = { getItem(key) { return JSON.parse(localStorage.getItem(key)); }, setItem(key, value) { localStorage.setItem(key, JSON.stringify(value)); }, clear() { localStorage.clear(); }, }; For the storing / caching data, we going to use the . This is just a simple object with methods, nothing else. localStorage Routing The router part has to care our and routes. Redirections have to happen from when we trying to access and when if the user goes to some private route without a session, it has to have the redirection to . Private Public Private /login /login "/" // -> Home view depends on session "/about" // -> About show with session and without "/login" // -> Login show with without session only "/signup" // -> Sign Up show without session only "/forgot-password" // -> Forgot Password show without session only "/account" // -> User show with session only "/settings" // -> Settings show with session only "/posts" // -> Posts and nested routes show with session only "*" // -> Not Found show without dependencies at all You can see the comment against each route described the behavior of accessibility. What’s the reason to be in if you have already a session? I’ve seen many times that issue in other projects. So, in our case, we going to have a redirection and from and from . Only should have full access in the end. SignUp ProtectedRoute PublicRoute NotFoundView <Routes> <Route element={<MainLayout />}> <Route path="/" element={<HomeView />} /> <Route path="/about" element={<AboutView />} /> <Route path="/login" element={<PublicRoute element={<LoginView />} />} /> <Route path="/signup" element={<PublicRoute element={<SignUpView />} />} /> <Route path="/forgot-password" element={<PublicRoute element={<ForgotPasswordView />} />} /> <Route path="/account" element={ <ProtectedRoute element={<ProtectedRoute element={<UserView />} />} /> } /> <Route path="/settings" element={<ProtectedRoute element={<SettingsView />} />} /> <Route path="/posts" element={<ProtectedRoute />}> <Route index element={<PostsView />} /> <Route path=":uuid" element={<PostView />} /> </Route> </Route> <Route path="*" element={<NotFoundView />} /> </Routes> As you can see that we added protection for both flows. going to navigate to in the case when no session and will redirect to , because has to check for authorization. ProtectedRoute /login PublicRoute / HomeView const HomeView = () => { const session = useOutletContext(); return session.data ? <ListsPostsView /> : <LandingView />; }; The possible to get the right form it’s because will provide that . session useOutletContext() MainLayout context Main Layout Everything is wrapped in which going to provide the of the and other global-related data. For we going to use the common and under will expose all routes. Let’s take a look at the setup. MainLayout context session MainLayout Route Outlet const MainLayout = ({ navigate }) => { const session = useSession(); return ( <StyledMainLayout> {!session.loading ? ( <div> <Navigation session={session} navigate={navigate} /> <StyledContainer isLoggedIn={!!session.data}> <Outlet context={session} /> </StyledContainer> </div> ) : ( <Loading /> )} <Footer /> </StyledMainLayout> ); }; The has no dependencies of state and we going to render it all the time. But and all nested routes have to have access the . Here is the which render the child route elements and where to pass the to all children. Footer Navigation session Outlet context The request which provides us with the data has a delayed response and in that case, we show the component. session Loading Session When the application is mounted, we must request the current . The hook will fire on the mount and get the either from the cache or from the API. session useSession session export const useSession = () => { const cache = fakeCache.getItem(SESSION_KEY); const [data, setData] = useState(cache); const [loading, setLoading] = useState(false); useEffect(() => { if (!cache) { setLoading(true); } fakeApi .getSession() .then(session => { if (session) { setData(session); fakeCache.setItem(SESSION_KEY, session); } else { setData(null); fakeCache.clear(); } }) .finally(() => { setLoading(false); }); }, []); return { data, setData, loading }; }; Every request we are storing the response to cache, like cookies stored in real websites. Then it’s time to show depends on the with / and child components. FakeApi session Navigation Login Logout Navigation The state for is important to enable or disable buttons during the current request. If to hit better to disable all buttons in order to prevent other operations during session clearance. Navigation Logout export const Navigation = ({ navigate, session }) => { const [isLoading, setLoading] = useState(false); const onLogin = async () => { setLoading(true); const sessionData = await fakeApi.login(); session.setData(sessionData); fakeCache.setItem(SESSION_KEY, sessionData); setLoading(false); navigate('/'); }; const onLogout = async () => { setLoading(true); fakeCache.clear(); await fakeApi.logout(); session.setData(null); setLoading(false); navigate('/'); }; return ( <StyledNavigation> <Link to="/">Home</Link> {session.data ? ( <div> <Link to="/posts">Posts</Link> <Link to="/settings">Settings</Link> <Link to="/account">My Profile</Link> <button disabled={isLoading} onClick={onLogout}> {isLoading ? 'loading...' : 'Logout'} </button> </div> ) : ( <div> <Link to="/about">About</Link> <Link to="/signup">Sign Up</Link> <button disabled={isLoading} onClick={onLogin}> {isLoading ? 'loading...' : 'Login'} </button> </div> )} </StyledNavigation> ); }; Private and Public It is clear that the routing should be protected depending on the , but how do get the in each Route Component? We will still come to the aid of the context provided by API. session context useOutletContext() react-router export const ProtectedRoute = ({ element }) => { const session = useOutletContext(); return session.data ? ( element || <Outlet /> ) : ( <Navigate to="/login" replace /> ); }; For the everything is almost the same but another way around with a different redirection route. PublicRoute export const PublicRoute = ({ element }) => { const session = useOutletContext(); return session.data ? <Navigate to="/" replace /> : element || <Outlet />; }; Possibly you can see, that better to have something like a where is better to provide only the route of redirection and prop to identify is it a or . I prefer to separate such logic for future scalability. And this is pretty much that’s it. SmartRoute public private Conclustion React Router is the most popular routing solution for React applications and provides the most obvious and clear API for developers today. Since amazing updates in the last version building a routing architecture has become much easier and more convenient. Hope this structure of the application aims to help quickly to build up the body of future React projects. Thank you. : GitHub Repository architecture-application-boilerplate