A Practical React App Architecture

Doron Tohar
Sep 1, 2018 · 7 min read
Beautiful architecture

The basic architecture of an app is the base for the rest of the app.

If we want our app to be solid and strong, we need to build on a solid foundation. We also want our base to have easy extension points and above all to be simple.

The image above is nice and catches the eye but we don’t want something so intricate as the base of our app.

We aim to build a foundation which is boring and rock solid. Something like this:

A solid base

Code

The github repository has a minimal application with support for authorization flows and clear extension points. Checkout the readme.

Introduction

The requirements for any architecture are:

  • Simple
  • Built from common elements
  • Simple yet powerful extension points

This is quite general so let’s talk about react apps.

Most web apps have similar flows:

  • The user must login before she can do anything interesting with the app. This is mostly relevant to B2B apps. B2C apps usually have some content and functionality available to users that are not registered.
  • The app has a login flow which consists of authenticating the user, loading her data and redirecting to the main page (usually some kind of dashboard).
  • If the user logs out, the app should clear the user’s data and redirect to the login screen.

Let’s look at a simple react architecture that supports these flows and has very clear extension points.

To have a basic architecture setup we need to build a component hierarchy and routing. The first 2 sections will explain the design behind the hierarchy and routing. We will then code the architecture and after we have it working we will add redux and look at ways to make it even better.

I am using reactjs (although it can be easily adapted to react-native) and react-router.

The Component Hierarchy

The high level hierarchy is composed of 2 containers and 2 screens. In this article, ‘container’ is a react component whose main purpose is to contain other components.

RootContainer — Container for the entire app. It is mounted when the app starts and remains mounted for the entire app lifespan.

  • General initialization entry point — Initialize stuff that does not require authentication — e.g. redux store, translations and registering listeners. The initializations can be triggered in the component’s `didMount` lifecycle event since this component is mounted once in the entire app lifespan. The `willUnmount` lifecycle event can be used to clear stuff such as unregistering listeners.
  • Render the correct component according to the route and authorization status.

LoginScreen — A regular login screen. Get user’s credentials and try to login.

ForgotPasswordScreen — Obvious.

LoggedInContainer — This component is mounted after the user is logged in and unmounted when the user logs out.

It will contain all the components that require an authorized user to work.

  • User initialization entry point — Initialize logged in users such as registering event listeners and timers for refreshing data, as well as clearing stuff.
  • App entry point — Render the app’s main content, the content that is visible to logged in users.

This architecture has several benefits:

  • A clear boundary between authroized and un-authorized UI. The LoggedInContainer only exists after the user’s data is loaded. We don’t need to check that the user is initialized as long as we put our components inside the LoggedInContainer.
  • Clear entry points — initialization, rendering the app, logging out.
  • Simple — Composed of plain react components and some react-router components.

Routing

We want to allow some routes to get accessed only by logged in users, for example the / route.

We to “protect” these routes.

On the other hand, un authorized users should be automatically redirected to the /login route.

We do this by first introducing a new component — ConditionalRoute. The conditional route is very similar to react-router’s Route component but it contains 2 extra properties: isRouteValid and redirectInstead.

If the route does not match, the component will do nothing.

If the route matches, it will use isRouteValid to check if the route can be accessed. If the function returns true, the component will render it’s content. Otherwise it will redirect to the route in the redirectInstead property.

Logic of the ConditionalRoute component

With this component in hand we are ready to build the first version of the architecture.

Implementation

In the article I will show snippets of code. The full code can be found in the github repository.

RootContainer

The root container renders the rest of the app. It uses the new ConditionalRoute component we described before.

A quick explanation of what is going on here:

The RootContainer manages the user’s session in it’s state. A logged in user will have a non null session (which will probably hold the user’s details, it’s token, etc.)

The login and logout functions implement logic for the respective flows. In a redux application you will probably move this logic into a side-effects file like a saga or thunk.

Let’s take a closer look at the first ConditionalRoute element.

It has 2 tasks:

  • Render the LoginScreen for the /login route
  • Redirect to / if the user is already logged in and tried to access the /login route (imagine the user got an email with a link to login but she is already logged in and the session is saved in a cookie)

From this we can understand the values of the properties:

  • path — match the /login path
  • isRouteValid — the route is valid only if the session is null
  • redirectInstead — if the path is not valid (i.e. the user is authorized), redirect to / to render the rest of the app
  • render — if the path matches and the user is not logged in, render the LoginScreen

Any other path will resolve to the second ConditionalRoute:

  • If the user is logged in, render the LoggedInContainer
  • If the user is not logged in, redirect to /login

Now we have a declarative description of our desired login/logout flow.

Once you call login or initialize the session (e.g. from a cookie or SessionStorage), the RootContainer will render the LoggedInContainer, redirecting as needed.

When the session is cleared, the RootContainer will take care of redirecting.

ConditionalRoute

The conditional route is a composition of Route and Redirect:

We’re using the children prop of Route which is a func that gets called whether path matched or not.

If there’s a match and the route is valid, we render the content.

If there’s a match and the route is not valid, we redirect.

Otherwise we don’t render anything.

Note that Switch can work with our ConditionalRoute component and with any other component that has a path prop.

LoginScreen

The login screen component is straightforward.

For brevity, we always create a session with bogus values.

When we call the login function, our components re-arrange themselves:

  1. The session in the state gets initialized
  2. The RootContainer is updated and redirects to /
  3. The LoginScreen is unmounted and the MainScreen is mounted
  4. Our initialization logic in LoggedInContainer (see below) gets called

LoggedInContainer

The LoggedInContainer contains the most important extension points of the application:

In componentDidMount we can add initialization for user, e.g. start listening to socket or start a timer for refreshing data.

If we use componentWillUnmount to clear stuff we can be confident that our initialization logic will run whenever a user logs in and our cleanup will run on logout.

In render we can use Route's and Switch to render other screens of the application (e.g. dashboard, settings) and be certain that the user is logged in and the token is available.

MainScreen

This is just an example of how to use logout:

When we call the logout function, the session in the state will be cleared and our components will re-arrange:

  1. Session is cleared
  2. RootContainer updates due to state change. It will now redirect to LoginScreen
  3. LoggedInContainer get unmounted. Our cleanup code get called
  4. LoginScreen is rendered

Adding screens for authorized users

This depends on your use case and application but a very common UX is to have a dashboard screen and a settings screen.

To do this we need to add the proper Switch in LoggedInContainer:

Here we can see how to build a simple screen with navbar at the top (which probably displays a logout button), a settings screen and a dashboard.

Remember that since this is all under LoggedInContainer we know that the user is authenticated.

Building on this architecture

  1. Use the extension points to add components and logic to the app.
  2. If you use redux, it should be quite easy to modify the architecture for it.
  3. Code split between the public and private parts of the apps so you get faster time for first page.
  4. You can continue splitting stuff into containers. For example, both dashboard and settings can be containers. This will make it easy to bundle them separately (to provide for the common case of regular users that use only the dashboard and admins that use also the settings).

Summary

We’ve seen a simple architecture with no fancy stuff (except maybe for the ConditionalRoute component) and still provides us with the basic features every app needs:

  • Private and public routes
  • Initialization and cleanup extension points
  • Clear extension points for adding screens
  • Clear separation between private and public content

Some of you might say that there is not a lot to this article/architecture. But from my experience, lots of developers struggle with this because it is not explained anywhere. You have lots of tutorials about using cool features of react or advanced libraries but the basic stuff is usually ignored.

I believe that this is key to building robust applications — having a solid base that gives shape to the entire application.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade