DEV Community

UponTheSky
UponTheSky

Posted on

[Opinion]Today's frontend is easy to be messed up and we need to organize it

Disclaimer: This post is a mixture of ranting about the complexity of the contemporary frontend, and my thought 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 this post. Also, although I only discuss React/Next.js here, I think the argument could be extended to the entire frontend system as of now, 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 to be developed in Next.js. Although I have done a bunch of tasks where I had to use React and Next.js, at those times I didn't have to deal with the design part, and all I was doing was tossing and turning the data from the backend and making React components to present it. However, this was my first time that I had to seriously consider the visual design part as well, from the layout to the font color of a small text under <div> tag. And man, it was super difficult!

But why? Why was it that difficult? For sure the amount of CSS document pages from MDN was overwhelming, but that was a pretty minor issue. The harder part was that it was very very easy to write messy React components (Here, what I mean by "messy code" is that it is difficult to recognize what the purpose or responsibility of this component is, and very hard to track the behavior and the state updates).

But before you are about to blame me for my skill issue, calm down and think up of the last React/Next.js code that you encountered. If you can't, visit a few pages about advanced topics in the official React documentation homepage. For example, the following is an excerpt of code from the page about managing input and states:

export default function Form() {
  const [answer, setAnswer] = useState('');
  const [error, setError] = useState(null);
  const [status, setStatus] = useState('typing');

  if (status === 'success') {
    return <h1>That's right!</h1>
  }

  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 (
    <>
      <h2>City quiz</h2>
      <p>
        In which city is there a billboard that turns air into drinkable water?
      </p>
      <form onSubmit={handleSubmit}>
        <textarea
          value={answer}
          onChange={handleTextareaChange}
          disabled={status === 'submitting'}
        />
        <br />
        <button disabled={
          answer.length === 0 ||
          status === 'submitting'
        }>
          Submit
        </button>
        {error !== null &&
          <p className="Error">
            {error.message}
          </p>
        }
      </form>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Of course, this is a relatively simple and straightforward example, but there are still lots of things to consume. There are already three states, nested HTML elements, two event handlers, and even a conditionally rendered component, even in a fairly simple and decent example. Why is this so complex?

Well, I believe that there is an innate issue which is hard to overcome in React (or frontend in general). That is, a React component needs to represent information from a managed state in the form of JSX, which is simply a "nicer" version of HTML. But doesn't this sound way too natural? How could it be an essential problem of React and frontend?

Frontend = State + hierarchical presentation

I believe any frontend technologies including mobile ones are essentially about how to manage the current state (either on the frontend or on the backend) and render it in hierarchical views. Being hierarchical implies several issues, but to name a few:

  1. How to make the layout: What is the relationship between potentially conflicting sibling HTML nodes? How many children would you put under the current <form>? How to deal with responsive design?
  2. How to manage the states: For rendering this particular dataset, are we going to fetch all the resources in a single place and distribute them to several HTML elements, or would it be better to make several API calls from each of the small elements themselves? How about data update and re-rendering? Would it be better to make data fetching in a parent or in its children?

Because state management and layout are essentially coupled but the HTML elements must be hierarchical at the same time, things are very likely to become complex (or as I expressed in the title, messed). Think about the above excerpt as an example. Suppose you want to switch the <textarea> element to <selector> for enabling users to choose the answer rather than typing it manually, but the answer candidates are dynamic, which means you have to fetch the list of the answers from the backend. Then you might "naturally" think of adding useEffect in the same Form component:

export default function Form() {
    // [...]
    const [countries, setCountries] = useState([]);

    useEffect(async () => {
      try {
        const response = await fetch("GET_COUNTRY_LIST_API");
        const data = await response.json();

        setCountries(data.countries || []);
      } catch (error) {
          setError(error);
      }
    }, []);

    // [...]
}
Enter fullscreen mode Exit fullscreen mode

Do you think this is a good solution? Some of you might say yes, and others may not. Would it be simpler to fetch countries here in the same Form where the data is to be rendered and submitted again, or somewhere else so that Form is purely responsible for submitting data? There is no absolutely correct or incorrect answer, and it is totally up to the developer to decide. However, whereas there are so-called "best practices" or "design patterns" for the backend, for the frontend there seem to be no such widely used or accepted patterns to the best of my knowledge.

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 pattern.dev). From my understanding, you can have the following two patterns of writing React components: stateful (or non-functional) and stateless (or purely functional).

  • Stateful components manage the application's internal states. They could be either pure client-side states such as the current text value in an input element, or something to be fetched from the backend or third-party APIs. In short, any component with useState, useEffect, or fetch calls is stateful.
  • Stateless components are purely functional, in that the data they read is immutable and there is no internal side effect caused by useEffect. They are only responsible for how to visualize the given data.

Let's get back to the above excerpt. The Form component is obviously stateful, where 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. If you want to add any additional data submission, you can tweak this Form component. If you have a problem in submitting country text, then there must be something wrong inside this Form.

Furthermore, if we want to refactor this component according to the Presentational-Container pattern, then we separate the HTML components in the return statement and pass the states and callbacks for state updates like this:

export const FormBox = ({
    title,
    description,
    answer,
    status,
    error,
    handleSubmit,
    handlerTextareaChange,
}: props) => {
    return (
        <>
          <h2>{title}</h2>
          <p>
              {description}
          </p>
          <form onSubmit={handleSubmit}>
            <textarea
              value={answer}
              onChange={handleTextareaChange}
              disabled={status === 'submitting'}
            />
            <br />
            <button disabled={
              answer.length === 0 ||
              status === 'submitting'
            }>
              Submit
            </button>
            {error !== null &&
              <p className="Error">
                {error.message}
              </p>
            }
          </form>
        </>
    );
};
Enter fullscreen mode Exit fullscreen mode

However, this logical separation itself may not be enough. Since the frontend elements are hierarchical, there is no limit to putting another stateful element inside a stateless element. If this is the case, then is that stateless element purely stateless? Even if it doesn't manage any state at all, you may have to look into this element because the state you want to check out is managed by this element as a parent.

export function FormLayout() {
    return (
        <div>
            {/* some other components*/}
            <Form />
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

In the above code example, we have Form inside FormLayout. Now, although FormLayout has nothing to do with any form submission logic, you are still very likely to visit this component while searching in your IDE or browser developer tool as long as the FormLayout component is conceptually tied to the form submission. Yes, we need a more comprehensive mental model for further organization of our frontend code.

Revisiting Atomic Design

Brad Frost's Atomic Design suggests another great insight for organizing our React project. Although he introduces five levels of component designs analogous to chemistry, my takeaway is that you can think of an entire frontend page in two aspects.

  1. Layout: Layout only concerns how to place several visual components on a screen, such as how big this image should be and where to put it, or how we arrange those cards inside a flexbox, etc. This part is purely related to CSS but you need to make sure each individual component doesn't affect the other elements visually such as by unexpected resizing or content overflow.
  2. Feature page: This is about what this single component is trying to convey with its content to the users product-wise. Please note that I am using the word "single" here, for we want to handle a component with the single responsibility principle. A single product feature can consist of several sub-features. For example, a form submission page can contain several various inputs for text and file upload. Each single feature is responsible for handling UI and data states.

Now we can see that the naming FormLayout is somewhat misleading, in that it can contain not only the form submission page but other features such as the navigation bar or a Google Ads banner. If this is the case, then we might as well use another name such as "QuizPageLayout" instead.

So far so good, now we have our mental model for separating concerns. There is a hierarchical structure of features for the entire project, and each individual feature should be assigned its own space as a page by the layout logic at its tree-level. Each feature fetches and updates its own feature data. I would like to refer to this mental model as Layout - Page. Have you noticed any familiar names? You're right. This model works naturally with Next.js.

Layout - Page Model

Let's discuss the Layout-Page model in more details in conjunction with the Container-Presentational pattern we discussed previously.

  • First, Layout corresponds to organisms, templates, and pages in Atomic Design. It is responsible only for how to arrange several components on the entire screen. It decides the position, display, and size of each component. It may contain some visual components such as dividers, but those are pretty rare. Layout never deals with how to render each individual component (Page), even including its margin or padding properties.

  • Next, each Page represents a single feature in the product, with the Single Responsibility Principle in mind.

    • It is responsible for fetching and updating the data relevant to the feature in the backend, and managing the UI states on the client side, if necessary. That is, there could also be stateless and purely functional pages that only visualize the data that is passed to them.
    • A page consists of two elements: its own layout and sub-pages(!). Hence, in every single page, there is another layout and sub-pages. If a page only consists of basic HTML elements and no other React components, then we may say this page is a leaf page.

Hence the structure here is recursive. You have a tree of pages, and each page is logically separated into layouts and sub-pages. For example, we can organize the Form element and possibly related components as follows:

QuizPage
├── @AdsBanner
│   ├── @Page
│   └── Layout
├── @QuizSubmitPage
│   ├── @Page # <Form> will be in this page
│   └── Layout
└── Layout
Enter fullscreen mode Exit fullscreen mode

Note that this tree structure is very similar to the structure of Next.js App Router. However, Next.js itself doesn't really enforce any design principles for developers, so you won't find the idea argued here in the Next.js documentation. It is totally up to you, the developer, to decide how to organize your project. However, the mechanism of App Router that Next.js provides fits perfectly with the idea of the Layout-Page model, and in fact, a part of inspiration of this model is from the App Router itself.

If we translate the structure above into a Next.js file routing system, then it would be like this:

quiz
├── @adsbanner
│   ├── page.tsx
│   └── layout.tsx
├── submit
│   ├── page.tsx # <Form> will be in this page
│   └── layout.tsx
├── layout.tsx
└── page.tsx
Enter fullscreen mode Exit fullscreen mode

Here, note that we use a parallel route for the ads banner component, since we don't want the user to access the banner only through an exposed route. Also, it is not under submit/ but under quiz/, which means the banner will show up in the other sub-routes of quiz/, not limited to its submission page /quiz/submit. In general, it is essential to utilize the parallel routes in the Next.js App Router system for the Layout-Page model, as there is no guarantee that only one sub-feature exists inside a product feature.

To recap, the entire recursive tree structure of a project according to the Layout-Page model is like this:

Project
├── Layout
├── Page0
│   ├── Layout
│   ├── Page00
│   │   ├── Layout
│   │   ├── Page000
│   │   ...
│   ├── Page01
│   ├── Page02
│ ...
├── Page1
├── Page2
├── Page3
...
Enter fullscreen mode Exit fullscreen mode

Before the end the post

I would like to mention that this might not be my original idea, and someone somewhere could have already thought of this mental model and publicized under a name that I haven't heard of yet. However, it was not easy for me to come up with this idea after a long time of searching for any useful idea for organizing the frontend messes, that wasn't much successful. It would be great to see any comments on this. Thank you.

Top comments (0)