Better React app architectures


Since its debut in 2013, React JS has been widely adopted as a technology favourite for web application development. JSX and by extension the virtual DOM have fundamentally shifted the way developers reason about building responsive interfaces. A 'batteries not included' philosophy has empowered front-end engineering teams to make the decisions about how to compose apps, however, there are architectural anti-patterns to be avoided when laying out a codebase. Applying simple principles will help build foundations that will last as technologies evolve.

Decoupled concerns

React JS is popular, so too is Svelte, as is VueJS. What would happen if your team decided to pivot from one solution to another? Do you have a suitable separation of concerns that means you can refactor a portion of your app, or are you likely to find a wholesale rewrite is necessary?

Defining boundaries around the various concerns of your application will help isolate what needs to import React. Take for example interacting with an API surface, does this need to be achieved with a React import statement?  An adapter pattern can return data to React modules. The benefits are two fold:

  1. Your API handler, and test files won't need to import React, or React Testing Library.
  2. If you pivot away from React you can write a new adapter to communicate with the new view layer.



Despite numerous misconceptions about Monorepos, fundamentally they represent a means to co-locate logical packages inside the same repo, they allow teams to independently build constituent parts of a project without blocking colleagues. Stability challenges often occur in distributed teams where there is no robust process to ensure dependency management is understood by producers and consumers of those packages.

If a Monorepo is right for your problem space, identify the areas of concern up-front, use the single responsibility principle to help define what these are:

- package.json
- ...
---- /packages
-------- /components
        |- package.json
        |- ...
-------- /routes
        |- package.json
        |- ...
-------- /api-handler
        |- package.json
        |- ...
-------- /api-react-adapter
        |- package.json
        |- .

each package can run unit tests, perform linting, and create build artefacts etc, in isolation, which will speed up build times, and streamline developer workflows, this will also aid the first point ☝️ that your api-handler should not need to import React, and so your package.json file can omit this.

Technical scorecard

As a codebase grows and evolves, it will inevitably accrue technical debt, this can manifest for a range of reasons, some being:

  • Applying DRY principles, that introduce abstractions, without being able to refactor all previous implementations.
  • Discovering a preferred approach, such as using functional components over classes.
  • Identifying new methods of testing, for example using MSW instead of mocking fetch.

Technical debt presents several challenges including:

  • Onboarding new starters - where it requires context from mentors who must point to the preferred approach.
  • Changing business logic, where it requires touching and refactoring 'outdated' patterns.

A technical scorecard can help to track this, and be a useful input when understanding pain points. In the right hands it can highlight priority. A good technical scorecard can measure other dimensions too, such as developer satisfaction, time spent fixing vs building, as well as general codebase health. This is a worthwhile endeavour if your codebase is sufficiently growing and you are ramping up development resource.

Further reading