[Opinion]Today's frontend is easy to be messed up and we need to organize it
Source: Dev.to
Disclaimer: This post is a mixture of ranting about the complexity of the contemporary frontend and my thoughts on how to solve it. I have been away from the frontend/React world for a while, so this post might contain some out‑of‑date ideas. Please let me know if you find such ideas while reading. Although I only discuss React/Next.js here, I think the argument could be extended to the entire frontend ecosystem, regardless of framework. Therefore I will use the words frontend and React interchangeably throughout the post.
React is dang complex
Lately, I was involved in a task to develop an application where the frontend is built with Next.js. Although I have done many tasks that required React and Next.js, those times I didn’t have to deal with the design part; I was merely passing data from the backend to React components for presentation. This was my first time having to seriously consider visual design—from layout down to the font color of a small piece of text inside a “—and, man, it was super difficult!
Why?
It wasn’t just the overwhelming amount of CSS documentation on MDN (though that helped). The harder part was that it is very easy to write messy React components. By “messy code” I mean components whose purpose or responsibility is hard to recognize, and whose behavior and state updates are difficult to track.
Before you blame me for a skill issue, think about the last React/Next.js code you encountered. If you can’t recall one, browse the advanced topics on the official React documentation homepage. For example, the excerpt below is taken from the page about managing input and state:
export default function Form() {
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing');
if (status === 'success') {
return
## That's right!
;
}
async function handleSubmit(e) {
e.preventDefault();
setStatus('submitting');
try {
await submitForm(answer);
setStatus('success');
} catch (err) {
setStatus('typing');
setError(err);
}
}
function handleTextareaChange(e) {
setAnswer(e.target.value);
}
return (
<>
## City quiz
In which city is there a billboard that turns air into drinkable water?
Submit
{error !== null && (
{error.message}
)}
);
}
Even though this example is relatively simple, it already contains:
- Three pieces of state
- Nested HTML elements
- Two event handlers
- Conditional rendering
Why does something that looks so straightforward feel complex?
I believe there is an innate issue that is hard to overcome in React (and the frontend in general): a React component must represent information from a managed state in JSX, which is essentially a “nicer” version of HTML. It sounds natural, but it introduces a fundamental tension.
Frontend = State + Hierarchical Presentation
Any frontend technology—web or mobile—is essentially about managing the current state (whether on the client or the server) and rendering it in a hierarchical view. The hierarchical nature brings several challenges, such as:
- Layout – How do sibling HTML nodes relate to each other? How many children should a “ contain? How do we handle responsive design?
- State management – Should we fetch all data in a single place and distribute it to many components, or let each small component fetch its own data? How do we handle updates and re‑rendering? Should data fetching happen in a parent or in its children?
Because state management and layout are tightly coupled while the UI must remain hierarchical, complexity (or “messiness”) quickly escalates.
Consider the previous Form example. Suppose we want to replace the with a so users can choose an answer from a dynamic list fetched from the backend. A natural first step might be to add a useEffect inside the same Form component:
export default function Form() {
// …
const [countries, setCountries] = useState([]);
useEffect(() => {
async function fetchCountries() {
try {
const response = await fetch('GET_COUNTRY_LIST_API');
const data = await response.json();
setCountries(data.countries || []);
} catch (error) {
setError(error);
}
}
fetchCountries();
}, []);
// …
}
Is this a good solution? Some of you might say yes; others may disagree. Should the countries list be fetched in the same Form component that renders and submits the data, or…? (The discussion continues…)
Revisiting Container‑Presentational Pattern
Dan Abramov’s famous “Presentational‑Container” pattern provides a useful insight for organizing this mess (for an easier introduction, I recommend reading this post on patterns.dev).
From my understanding, you can have the following two patterns of writing React components:
| Type | Description |
|---|---|
| Stateful (or non‑functional) | Manages the application’s internal state. This can be pure client‑side state (e.g., the current text value in an input) or data fetched from a backend or third‑party API. In short, any component that uses useState, useEffect, or fetch is stateful. |
| Stateless (or purely functional) | Purely functional – the data it reads is immutable and there are no side‑effects caused by useEffect. It is only responsible for visualising the given data. |
Applying the pattern to the Form component
The Form component is obviously stateful, because it manages several states using useState. If we ever add useEffect here for fetching the list of candidate countries, then the component is also responsible for handling data fetched from the backend.
This separation of concerns is especially useful for maintenance:
- To add any additional data submission, tweak the
Formcomponent. - If there is a problem submitting the country text, the bug must be inside this
Form.
If we refactor the component according to the Presentational‑Container pattern, we separate the markup (the presentational part) from the state‑handling logic (the container part). The presentational component could look like this:
export const FormBox = ({
title,
description,
answer,
status,
error,
handleSubmit,
handleTextareaChange,
}: Props) => {
return (
<>
## {title}
{description}
Submit
{error !== null && (
{error.message}
)}
);
};
Note: The container component would import
FormBoxand pass the appropriate props (state values and callbacks).
Hierarchical concerns
Even with this logical separation, the frontend hierarchy can still blur the lines between stateful and stateless components. There is no limit to nesting a stateful element inside a stateless one. Consider the following example:
export function FormLayout() {
return (
{/* some other components */}
);
}
FormLayout itself does not contain any submission logic, yet you will likely inspect it while debugging because it conceptually groups the form. This shows that a more comprehensive mental model is needed for organizing frontend code.
Revisiting Atomic Design
Brad Frost’s Atomic Design offers another useful perspective for structuring a React project. While Frost defines five levels of components (atoms, molecules, organisms, templates, pages), my takeaway is that an entire frontend page can be thought of in two aspects:
| Aspect | Description |
|---|---|
| Layout | Concerns how visual components are placed on the screen (size, positioning, flexbox arrangement, etc.). This is primarily a CSS concern, but each component should be self‑contained so it does not unintentionally affect its siblings (e.g., via overflow or resizing). |
| Feature page | Concerns what the component is trying to convey to the user from a product standpoint. Each feature should follow the Single‑Responsibility Principle. A feature may consist of several sub‑features (e.g., a form page with text inputs, file uploads, etc.). Each sub‑feature handles its own UI and data state. |
Naming considerations
The name FormLayout can be misleading because it may contain not only the form itself but also unrelated elements such as a navigation bar or an advertisement banner. In such cases a more descriptive name—e.g., QuizPageLayout—might be appropriate.
Putting it all together
We now have a mental model for separating concerns:
- Hierarchical feature structure – The entire project is organized as a tree of features.
- Page‑level layout – Each top‑level feature is assigned its own page by a layout component.
- Feature‑level state – Each feature fetches and updates its own data, keeping state isolated.
By combining the Container‑Presentational pattern with the principles of Atomic Design, we can achieve a clean, maintainable, and scalable frontend architecture.
Layout – Page Model
Let’s discuss the Layout‑Page model in more detail, in conjunction with the Container‑Presentational pattern we covered previously.
What is a Layout?
- Layout corresponds to organisms, templates, and pages in Atomic Design.
- It is responsible only for arranging several components on the entire screen: deciding the position, display, and size of each component.
- It may contain visual helpers such as dividers, but those are rare.
- Layout never deals with how to render each individual component (i.e., the Page), nor with its margin or padding properties.
What is a Page?
- A Page represents a single feature in the product, following the Single‑Responsibility Principle.
- It is responsible for fetching and updating the data relevant to the feature in the backend, and for managing UI state on the client side when necessary.
- Stateless, purely functional pages that only visualize data passed to them are also valid.
- A Page consists of two elements: its own layout and sub‑pages.
- If a page contains only basic HTML elements and no other React components, it can be considered a leaf page.
Because each page can contain its own layout and sub‑pages, the structure is recursive: a tree of pages, each logically separated into layouts and sub‑pages.
Example Tree
QuizPage
├── @AdsBanner
│ ├── @Page
│ └── Layout
├── @QuizSubmitPage
│ ├── @Page # will be in this page
│ └── Layout
└── Layout
The tree mirrors the structure of the Next.js App Router, although Next.js does not enforce any particular design principles. The App Router’s file‑based routing fits naturally with the Layout‑Page model, and the model was partly inspired by it.
Mapping to Next.js File Routing
quiz
├── @adsbanner
│ ├── page.tsx
│ └── layout.tsx
├── submit
│ ├── page.tsx # will be in this page
│ └── layout.tsx
├── layout.tsx
└── page.tsx
- The ads banner uses a parallel route (see the Next.js parallel routes documentation) so that the banner is not exposed as a standalone route.
- Because it lives under
quiz/rather thanquiz/submit/, the banner appears on all sub‑routes ofquiz/, not just on the submission page. - Parallel routes are essential for the Layout‑Page model, as a product feature may contain multiple sub‑features.
Full Project Recursion
Project
├── Layout
├── Page0
│ ├── Layout
│ ├── Page00
│ │ ├── Layout
│ │ ├── Page000
│ │ …
│ ├── Page01
│ ├── Page02
│ …
├── Page1
├── Page2
├── Page3
…
Closing Thoughts
I may not be the original creator of this mental model; someone else might have already published a similar idea under a different name. After a long search for effective ways to organize messy front‑ends, this model emerged for me. I’d love to hear any comments or references you might have. Thank you!