Skip to content
Logo Theodo

One Hook Per Screen: a simple architecture for scalable React Native apps

Mo Khazali7 min read

React Native logo with a hook icon

“Architecture is the decisions that you wish you could get right early in a project”

Ralph Johnson

When I built my first site, the web felt like a simpler place. I would create an html file for my markup, a css file to have all of my styles, and js files if I had any interactivity or logic. This design choice intentionally defined a clear separation of concerns (SoC).

Fast forward to today, and we use React & JSX to intentionally break this barrier and bring our markup into Javascript. If you want to make it even wilder, throw in a styling framework like tailwind and you’ll have your content, business logic, and presentation layers all in one file.

While using frameworks, like React, definitely has its benefits, muddling SoC can come at a cost when you start to scale your application and codebase.

This article looks at the importance of separation of concerns, how we can apply it to React apps, and proposes a pragmatic architecture to keep your logic and rendering separated. Adopting an approach like this will help your application scale smoothly as your codebase grows.

Motivations

This section goes into the theory and motivation for the proposed architecture. If you’re just interested in the solution & architecture, jump to the One Hook Per Screen Pattern section of the article.

Separation of Concerns

The web has traditionally been built with a very clear separation of concerns. HTML was used for content and structure, CSS for presentation and styling, and JavaScript for interactivity and business logic. This was done for many reasons, but among them was the simplification of each part of the app. If you encapsulate parts of your application with well defined boundaries, it should make it easier to understand and maintain.

Separation of Concerns on web implementation layer

In theory, with perfectly separated code, changes to one layer should not have an impact on the other layers. For example, if a developer wants to make changes to the presentation of the page, they do not need to worry about how it will affect the content or the business logic.

In today’s age of frameworks, we’ve strayed as far as possible from this. Let’s look at a typical React Native file for a simple screen:

Example of simple react native code for a button with updating label

This is a simple example with a screen that has an interactive button. As you press the button, a counter is incremented that updates the button text. This single JSX file has all three layers inside of it. The structural and presentation layers are intertwined together inside the return and the interactivity/logic is being handled by the hooks at the top of the file (useState and useCallback).

That’s separation of concerns out the window… Now arguably frameworks, like React, let us develop interactive UIs quicker than ever before with massive speed boosts. That comes as a double edged sword - if you’re constantly building at a high speed and not taking a step back to analyse the foundations, your app will start to feel like it’s built on a house of cards.

Separating Business Logic from Rendering

A good starting point for understanding separation of concerns in traditional UI frameworks is the Model-View-Controller (MVC) pattern:

MVC Split Diagram Source: FreeCodeCamp

This pattern is common in older frameworks, like Django and Ruby on Rails, and while it can become a bit boilerplate-y, it’s a very logical split of an app’s architecture and helps your codebase to scale well.

Let’s try and apply this to React now. In a common React file, you’ll have the following components:

Let’s go back to the original example we had above with the orange button. We’ve added some data fetching from a REST API to get an article title, and then increment the button text.

An expanded version of the earlier example, with the button fetching information from an API and setting some text in the screen

Functional Demo of the code above - when the button gets pressed, the title changes and the counter is incremented

If we try to separate out the code for this screen, we find that it can pretty easily be split along the MVC-like split:

  1. At the top, the fetchArticle method is getting the model from the external API and handling the business logic. (Model)
  2. In the middle of the file we have a bunch of hooks handling the local state of the button and the text that’s going to be displayed in a screen. (Controller)
  3. Lastly, we have the rendering logic, which includes the actual components being rendered, along with any styling. (View)

Since there’s a clear separability, each of these responsibilities can be taken care of in isolation.

One Hook Per Screen Pattern

As you can see in the example above, even simple screens/components can become very long and filled with a bunch of functions and hooks that have nothing to do with rendering. Initially, we attempted to combat this by introducing a linting rule to limit the number of lines per file. Over time, we found that this rule forced us to abstract away the business logic into separate files to adhere to this rule.

Ultimately, we landed on a simpler approach:

Each component/screen should only be calling a single hook. This hook would contain all of the UI state and data fetching, and would be making calls to retrieve business domain data.

Example

Example of simple react native code for a button with updating label

Let’s start abstracting away each part of the Screen file with this approach.

Business/Domain Logic

Firstly, we want to abstract away the domain/business logic - “articles” are entities here, so these aren’t specific to screens/components. We can abstract these away to a folder called domain or modules that contains all abstracted business logic.

Folder structure of domain, with an articles subfolder

Each subfolder within domain should be a single subdomain/class/entity in your business logic. These should roughly map to the split present on your application’s backend.

We’ll define a useArticles hook that handles any logic related to fetching, updating, or transforming articles on the frontend.

useArticles Hook which handles fetching an article

Note: in this case, we don’t need it to be a hook, but in many cases, where you need to use application state (for example, accessing user details from an AuthProvider), you will need a hook. We use hooks for all of our business logic to keep things consistent.

UI State & Data Handling

Screens folder structure with a hook and rendering file within each screen subfolder

Inside the screens/pages folder, each screen should have a subfolder that contains a <Screen Name>.hook.ts file which contains a use<Screen Name> hook inside of it. This hook will act as a controller between the business domain and what’s being rendered in the UI.

In the case of our ArticleTitleScreen example, we import the fetchArticle domain defined in the useArticles hook and use other hooks like useState, useCallback, and useEffect to handle the screen’s state.

The hook will return everything that needs to be accessed by the rendering logic in the UI.

Screen level hook

Rendering Logic

After abstracting away the hooks into separate files, our actual screen/component file should just contain rendering logic for the UI elements:

Rendering logic in file by itself

The final folder structure should look something like this:

Final Folder Structure

Wrap Up

Separation of Concerns is important for creating scalable apps. This is often neglected in React/React Native.

We looked at a pragmatic architecture to keep business logic and rendering separate. The ‘One Hook Per Screen’ pattern abstracts business logic away to a separate folder and has each component/screen call a single hook containing UI state and data fetching. This will help the application scale smoothly as the codebase grows.

Feel free to reach out

Feel free to reach out to me on Twitter @mo__javad. 🙂

Liked this article?