How to Make Your Colleagues Fall in Love with Your React Applications.


Since its first public release in May 2013, React has offered tools for building web applications in a more modular way. However, the famous library doesn’t provide much guidance on how to structure code, bringing great flexibility — for the best and the worst.

Let’s start with an indiscreet question (apologies in advance): Do your React components look like the one below?

const YourComponent = ({ props1, ..., props7 }: MyComponentProps) => {
const { state1, setState1 } = useState<...>(...)
const { state2, setState2 } = useState<...>(...)
const { state3, setState3 } = useState<...>(...)
// More states stacking here...
// ...

useEffect(() => {
// About twenty lines (or more) responsible for fetching
// and mapping data that will be used in the JSX
// section just below.
},[/* A list of several variables to watch. */])

// Instructions to process data, to handle user actions.
// ...
// Maybe another useEffect() to make things more confusing?
// ...
// What line are we at? Maybe line 105 already?
// ...
// Helpers to generate JSX codes.
// ...
// Line 145 :(
// ...
return (
<div>
<...
(...)
Thirty (or more) lines of JSX...
(...)
...>
</div>
)
}

 

If yes, let’s reflect on how we can improve things...

I’ll try to sell you something — it will cost a bit of your time and attention, but (hopefully) save you and your colleagues hours of struggling with debugging overly coupled code that handles too many responsibilities at once.

We can create a diagram to represent the above React component.

How your React component may look like…

As you can see, our React component handles at least five different responsibilities:

  1. Managing React states
  2. Handling user actions
  3. Fetching data from APIs, catching exceptions, and managing loading phases
  4. Transforming and mapping data from different sources
  5. Defining the UI and DOM with JSX

It’s a lot, isn’t it? Beyond readability issues (it’s often hard to understand what’s going on), the logic is difficult to test and extend. Moreover, applications relying on these kinds of components lack consistency in data flow. Information is randomly spread across the app, increasing confusion.

So, let’s try to fix this by separating concerns. How could it look like?

Separation of concerns helps make your React App readable, testable and maintainable. Your colleagues will love it ❤️.

Let’s understand the responsibilities of entities introduced in the diagram above. From the left to the right:

Clients

  • Clients are responsible for interfacing with external data sources, typically through APIs. They expose typed structures that controllers can directly process.

  • They are decoupled from the React lifecycle, meaning they don’t depend on React entities at all. They are simple classes (or functions) that contain information about APIs.

  • They handle API errors and re-throw errors that can be handled by the app (typically through an Error Fallback).

Stateful Controllers

  • Stateful Controllers are responsible for handling React states.

  • They expose data interfaces to View Components, and the exposed data is directly usable in the JSX code where it’s injected. This mechanism can be called data hydration.

  • Stateful Controllers also expose callback functions to allow users to modify the state and trigger API calls.

  • Stateful Controllers can be built by associating React hooks and React contexts. React hooks allow you to easily connect controllers with View Components, while React contexts make states global. This way, you can easily connect any View Component with any Stateful Controller, ensuring the state is shared across.

  • Stateful Controllers do not deal with a single line of JSX, which makes testing easier, as the output is a set of structured data and callback functions.

Stateful (and Stateless) Controllers deserve a dedicated article. Please let me know in the comments if you’re interested in diving deeper into these concepts.


Data Mappers and Transformers

  • In some cases, data fetched through APIs (or data sent to APIs) requires advanced transformations. These can be extracted into dedicated modules to improve testability.

  • Data Mappers and Transformers are decoupled from the React lifecycle.

View Components

  • View Components are simple components that connect to controllers and inject data into JSX code.

  • They don’t process, transform, or handle data. Instead, they delegate these tasks to controllers.

  • They don’t use props anymore because they get data from controllers by consuming hooks. This clarifies data flow and ensures consistency.

  • They don’t handle states.

  • They can be easily tested through snapshots or basic Jest assertions.

 

With this approach, prop drilling, callback hell, and high cognitive complexity (and even Redux 🎉) become bad memories.

Here we are. This short article aimed to give you a quick overview of how to improve your React application. If you’re interested in diving deeper into specific concepts with more concrete examples, I’d be happy to write more articles on this topic. Please leave your requests and feedback in the comments.
Thanks for reading!