What you need to know about frontend design patterns

Imagine you find yourself facing your code at a point where you clearly have a problem to solve. You start to think about different ways to go about it, though nothing seems quite right… Then you decide to search the internet for possible solutions and you realize… wait a minute! This seems to be a common problem, doesn’t it? Other people seem to have faced it before me, what a coincidence! And then you see there is a solution to this problem. I mean, not all problems you saw people solving were identical, but they were similar enough so you can see the resemblance and how to adapt the solution to your own situation.

In most cases, if you just copied the code exactly as it was shown in previous solutions it clearly wouldn’t work… but you can see a clear pattern in how it goes and is possible to adapt it to your reality. After you search and exercise on it some more, you realize that this solution seems to be used all around with its specificities according to each and every situation you encounter. Well, congratulations, you found a Design Pattern.

General concepts

Okay, so Design Patterns are basically a solution to a problem. They are, however, a general solution, and not something you can just copy to your code. The situation we discussed in the beginning shows that the problem you face in your code is probably something someone else has already encountered before you. So, a solution is possibly already there in a general concept that you might adapt to your own reality. There will be differences according to what you are developing, but in a high-level, they will be similar.

Numerous design patterns exist and they were created by developers as solutions to recurring problems encountered in software development. But why use them?, you might ask. Cultivating patterns inside your team means maintaining the integrity of the code. If the team reproduces general concepts, it means there will be a pattern being followed and the code will be better organized and easier to understand and maintain.

Besides the catalogs of design patterns, we also have community patterns, which are commonly followed by developers and might be adaptations of other previously existing patterns. We are going to talk about one of them soon enough.

Design patterns in frontend development

Now that we covered the general idea of design patterns, let’s move our focus to frontend development and take a closer look at how we use them here, shall we? Here we will be focusing on React Design Patterns only, but there is much more where these come from.

There are many patterns you can follow, and, as we said, your team might have an idea of which ones should be used in the project and how to use them as a team. That’s fine. Also, there is much information you can easily check online about all the well-known patterns used. So here we will focus on two examples of design patterns used in the frontend. Are you ready? Let’s go, then!

Custom hooks

Okay, having all that in mind, let’s say you need a loading state to track whether some data is being fetched from the API and this state is used across your application in multiple components. Suddenly you see yourself writing the same code over and over again checking if the information is ready to be rendered in multiple components. There must be a better way to do it, right?

And here we are: we have a problem and we must find a solution, just as we said before! Fortunately, others have faced this same problem, and the community has come up with a good solution that has been widely adopted: a design pattern, if you will.

And now we come to face a common pattern for frontend: Custom hooks. They are custom-made reusable functions in which you use React hooks and can isolate some logic, following DRY principles. This way you encapsulate some logic, making it easier to read, reuse, maintain, and test. Here we will talk about React, but keep in mind that this is not exclusive of it!

As we know by now, there are built-in hooks in React. They store data, execute side effects when data changes, and so on. However, they might not be enough in a given situation. Luckily you are allowed to build your own by combining one or more of those hooks and come to an answer to your specific problem.

By using custom hooks you can extract common logic and reuse it throughout your application. as well as isolate some complex logic and separate responsibilities. Our loading state case, for example, could be easily solved by separating it into a custom hook. No more code duplication. Also, before the custom hook, any changes you needed to make in this piece of code meant tracking your whole application to see where it was used… and now your code is easily maintainable since you know exactly where to look and only have to change it in one place!

This is just an example, of course. Basically, by using custom hooks, you are managing the state in a separate layer from the UI, and in it, you can have business logic, service interactions, and use cases, all individually organized and ready to use across your application as you see fit.

Let’s explore our loading state idea for a minute. Better yet, let’s expand it. We want to fetch some API data and track the loading state, as well as possible fetching errors.

To do that, let’s say we are developing a small business that sells products. Here is our Products component at the moment:

import { useState, useEffect } from "react";
import {fetchProducts} from "../services/products";

export const Products = () => {
    const [products, setProducts] = useState(null);
   const [loading, setLoading] = useState(true);
   const [error, setError] = useState(null);

   useEffect(() => {
       async function fetchData() {
           try {
             const response = await fetchProducts();
             const result = await response.json();
             setProducts(result);
           } catch (err) {
             setError(err);
           } finally {
             setLoading(false);
           }
       }
       fetchData();
   }, []);

  if(loading){
      return <Spinner />
  }

  if(error){
      return <ErrorMessage error={error} />
  }

  return (
    <div>
      {products.map((product) => (
        <Product key={product.id} product={product} />
      ))}
    </div>
  );
};

Okay, so we have some refactoring to do. This code clearly has two different responsibilities here: one is fetching the information and tracking loading/errors, while the other is the rendering of the component itself, the products of our application.

That being said, let’s move the fetching-related code to a custom hook:

import { useState, useEffect } from "react";
import {fetchProducts} from "../services/products";

function useProducts(url) {
    const [products, setProducts] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    useEffect(() => {
        async function fetchData() {
            try {
                const response = await fetchProducts(url);
                const result = await response.json();
                setProducts(result);
            } catch (err) {
                setError(err);
            } finally {
                setLoading(false);
            }
        }
        fetchData();
    }, [url]);

    return { data, loading, error };
}

Now let’s go back to our Products component and call our hook:

import Product from './Product';
import Spinner from './Spinner';
import ErrorMessage from './ErrorMessage';
import useProducts from '../hooks/useProducts';

export const Products = () => {
    const { products, loading, error } = useProducts();

    if(loading){
      return <Spinner />
  }

  if(error){
      return <ErrorMessage error={error} />
  }

  return (
    <div>
      {products.map((product) => (
        <Product key={product.id} product={product} />
      ))}
    </div>
  );
};

As you can see, we call our hooks at the component’s higher level, no loops our conditionals. That’s another important point on how to use them!

That’s better, isn’t it? This way we have all fetching-related logic separated from the components. It can be reused if necessary, but even if it isn’t, we have a separation between the responsibilities of it and the component itself. Our component Products doesn’t need to know how the fetching is done, just that is it done and how to respond to it accordingly. This means that our hook will not share the state itself, only the stateful logic behind it.

What did you say? You’d like to see another example? Okay, let’s do this! Now let’s imagine our little application will show some prices for all these products, but, however small this new company might be, we still sell to different countries (how fancy). So we must use the currency accordingly.

First let’s think about where in our application we will need to format the currencies: wherever we find product prices. So, at least in the component that shows all displayed products and the component showing the cart’s added products. Well, maybe you also have some modal component that displays products on sale from time to time to your users… Can you see where I’m going? Suddenly we are facing the same problem we faced with the loading state before: many components repeating the same logic over and over…

And here is where the custom hook comes to save us all. The idea is pretty simple: isolate logic. So let’s go and check our currency custom hook:

import { useMemo } from "react";

// Locale-to-currency mapping
const localeToCurrency = {
    "en-US": "USD",
    "en-GB": "GBP",
    "fr-FR": "EUR",
    "de-DE": "EUR",
    "ja-JP": "JPY",
    "zh-CN": "CNY",
    "es-ES": "EUR",
    "it-IT": "EUR",
    "in-ID": "IDR",
    // And so on...
};

function useCurrency(amount, locale = "en-US", currency) {
    const resolvedCurrency = currency || localeToCurrency[locale] || "USD";

    const formattedCurrency = useMemo(() => {
        if (amount == null) return "";
        return new Intl.NumberFormat(locale, { style: "currency", currency: resolvedCurrency }).format(amount);
    }, [amount, locale, resolvedCurrency]);

    return formattedCurrency;
}

And now let’s use it in our Product component:

import useCurrency from "./useCurrency";

function Product({ name, description, price, currency }) {
    // Determine the user's locale based on browser language
    const userLocale = navigator.language || "en-US";

    // Format the price using the custom hook
    const formattedPrice = useCurrency(price, userLocale, currency);

    return (
        <div style={styles.productCard}>
            <h2 style={styles.productName}>{name}</h2>
            <p style={styles.productDescription}>{description}</p>
            <p style={styles.productPrice}>Price: {formattedPrice}</p>
            <button style={styles.buyButton}>Buy Now</button>
        </div>
    );
}

** This custom hook could also be divided into two, can you see where? Our const userLocale could be extracted from here and have its own custom hook. Try to do it yourself!

Basically, what we have here is a simple, repetitive code that would need to be added to each and every component in which you might need to format the products’ prices. Many times we repeat code for the routine of it and don’t even realize we could actually extract it and turn it into a custom hook. Well, that’s an example of repetitive code that doesn’t need to be repetitive. However, that’s not how we usually handle currency, just an educational example.

According to all the information we shared here, we can see why custom hooks might be an important addition to your code. By using them, you can improve code reusability, separate responsibilities, keep your code cleaner and easier to understand, and, with all the aforementioned in mind, keep it easier to maintain.

Conclusion

With that, we’ve explored design patterns in frontend development, specifically within the context of React applications by using the Custom Hook pattern as a practical example. By applying this pattern, we enhanced the organization, reusability, and clarity of the code, making it more efficient and easier to maintain. It’s important to always adapt this and other patterns to your reality and your project’s needs; however, the overall usage and foundation will hopefully be clearer now so you can tackle complex issues.

Keep in mind to cultivate a shared knowledge and application of design patterns with your team, so you can reach greater consistency, better collaboration, and ultimately, higher-quality software that stands the test of time.

References

Design Patterns
Reusing Logic With Custom Hooks – React
Rules of Hooks – React

Previously: Accessibility & Responsiveness in action – Why they matter

This post is part of our ‘The Miners’ Guide to Code Crafting’ series, designed to help aspiring developers learn and grow. Stay tuned for more!

We want to work with you. Check out our Services page!