DEV Community

Cover image for Best Way To Write Frontend Components
Sotiris Kourouklis
Sotiris Kourouklis

Posted on • Originally published at sotergreco.com

Best Way To Write Frontend Components

There are a lot of different ways for developers to write frontend. We have React, Vue, and now HTMX, but in general, frontend, despite the framework, is one thing at the core: JavaScript, HTML, and CSS.

Modern developers face a major problem when it comes to reusability. This issue is not within their current project but arises when they create another project and can't easily reuse parts of the code.

In this article, we are going to discuss the method I use to reuse my code across multiple projects with minimal bundle size and almost zero boilerplate and zero NPM installs.

What Makes Code Reusable

First before we even start, we need to discuss what makes code reusable. The features and the specification that are necessary to make everything reusable.

Modules

The first thing is external modules. The fewer modules a component depends on, the more reusable it becomes. For example, in Angular or Next.js apps, we often have many npm installs, which makes reusability difficult.

Props

By giving a component a lot of props, we make it configurable. Dynamic properties need to exist for us to achieve our goals. Especially when it comes to text, size, or color, all of these need to be able to change through props.

Plug n Play

This is really important, maybe the most important of the three. Depending on SSR or other ways of rendering usually makes plug n play vanish. By using native web features like web components or by using JsDelivr, which we will talk about later, we ensure that our components can plug n play in any project just by importing them.

Tech Stack

Now we are going talk to about all the fun staff. The tech stack of my choice when it comes to frontend development is Web Components with Lit and Tailwind.

The whole purpose of frontend is for the rendering to be on the frontend, or it is no longer frontend it becomes backend development. I understand that SSR in a lot of cases is useful, but for most project SSR is just useless.

Let's see an example:

The first thing is to have a simple html file, no need for frameworks or libraries.



<!doctype html>
<html class="scroll-smooth" lang="en">
<head>
  <meta charset="UTF-8"/>
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <title>Project</title>
</head>
<body>
<main id="outlet"></main>
<script type="module" src="/src/entry-client.js"></script>
</body>
</html>


Enter fullscreen mode Exit fullscreen mode

Next, we need an entry-client.js file, which we will use for rendering our page. Also as you can see I am using vaadin router to handle multiple pages



import {Router} from 'https://cdn.jsdelivr.net/npm/@vaadin/router@1.7.5/+esm';
import './pages/index.js'

const router = new Router(document.getElementById('outlet'));
let routes = [
    {
        path: '/',
        component: 'home-page',
    },
    {
        path: '/sign-up',
        component: 'sign-up-page',
    },
    {
        path: '/sign-in',
        component: 'sign-in-page',
    },
    {
        path: '(.*)',
        component: 'not-found-page',
    },
];

router.setRoutes(routes);


Enter fullscreen mode Exit fullscreen mode

The components are simple Lit Web Components where we import lit from JsDelivr and our styles.



import {css, html, LitElement, unsafeCSS} from 'https://cdn.jsdelivr.net/npm/lit@3.1.3/+esm';
import styles from '../index.css?inline'

class HomePage extends LitElement {
    static styles = css`${unsafeCSS(styles)}`;

    render() {
        return html`
          <div>Homepage</div>
        `;
    }
}

customElements.define('home-page', HomePage);


Enter fullscreen mode Exit fullscreen mode

For those who will say these way of writing frontend has a lot of disadvantages you can read another article I wrote explaining why this is simply not true.

%[https://x.com/sotergreco/status/1792477974932926469]

ESM and JsDelivr

Migrating to ESM can really benefit you because you will completely move away from npm installs. As we all know, the JavaScript ecosystem has its issues, so the less of it we use, the better.

Also, every module you might think of can be used from JsDelivr, and it will work the same.

You need to keep in mind that by using Vite when you build your application, you will have a very small bundle size. This means that the performance of your application will be excellent.

Let's take a look at an example component to understand the usage of JsDelivr and how Web Components are game changers when it comes to reusability and performance.

This is a sign-in form component I have in one of my project.

Image description



// Importing directly from jsdelivr gives me the advantage
// when I copy and paste the component to another project 
// to work just fine
import {LitElement, html, css, unsafeCSS} from 'https://cdn.jsdelivr.net/npm/lit@3.1.3/+esm'
import {Router} from "https://cdn.jsdelivr.net/npm/@vaadin/router@1.7.5/+esm";
import '../elements/x-button.js'
import '../elements/icon-button.js'
import '../elements/alert-card.js'
import styles from '../index.css?inline'
import endpoints from '../endpoints.js'
import axios from 'axios'
import {globalState} from '../global-state.js'


export class SignInForm extends LitElement {
    static styles = css`${unsafeCSS(styles)}`;
    // Here the properties can be used to
    // overwrite default values when using the component
    // like this <sign-in-form email="test@test.com"></sign-in-form>
    static properties = {
        email: {type: String},
        password: {type: String},
        isError: {type: Boolean},
        errorMessage: {type: String},
        isLoading: {type: Boolean},
        isSuccess: {type: Boolean},
    }

    constructor() {
        super()
        this.email = null
        this.password = null
        this.isError = false
        this.errorMessage = ''
        this.isLoading = false
        this.isSuccess = false
        this.handleChange = this.handleChange.bind(this)
    }

    handleChange(value, field) {
        this[field] = value
    }

    validateInputs() {
        if (this.email == null) {
            this.isError = true
            this.errorMessage = 'Email is required'
            return false
        }
        if (this.password == null) {
            this.isError = true
            this.errorMessage = 'Password is required'
            return false
        }
        return true
    }

    async handleSubmit() {
        this.isLoading = true
        if (this.validateInputs()) {
            try {
                const response = await axios.post(endpoints.login, {
                    email: this.email,
                    password: this.password
                })
                const data = response.data
                console.log(data)
                this.isSuccess = true
                this.isLoading = false
                globalState.setAuth(true)
                globalState.setToken(data.token)
                globalState.setRefreshToken(data.refresh_token)
                Router.go('/dashboard')
            } catch (e) {
                this.isLoading = false
                this.isError = true
                this.errorMessage = 'Invalid credentials'
            }
        } else {
            this.isLoading = false
        }
    }

    firstUpdated(_changedProperties) {
        super.firstUpdated(_changedProperties);
        const url = new URL(window.location.href)
        const token = url.searchParams.get('token')
        const refreshToken = url.searchParams.get('refreshToken')
        if (token && refreshToken) {
            globalState.setAuth(true)
            globalState.setToken(token)
            globalState.setRefreshToken(refreshToken)
            Router.go('/dashboard')
        }
    }

    render() {
        return html`
          <div class="w-full min-w-96 h-full p-6">
            <icon-button url="/" pure="true" text="Go Back" icon="arrow-back-outline"></icon-button>
            <h1 class="text-3xl text-black font-bold mt-4">Sign in</h1>
            <a class="mt-3 block text-xs hover:underline" href="/sign-up">
              Don't you have an account? <strong>Sign-up</strong>
            </a>
            <div class="mt-8">
              <x-button></x-button>
            </div>
            ${this.errorMessage ? html`
              <alert-card title="${this.errorMessage}" type="error" class="mt-4"></alert-card>
            ` : null}
            <div class="mt-8">
              <input-element
                  .handleChange="${this.handleChange}"
                  class="mb-4"
                  icon="mail-outline"
                  name="email"
                  type="email"
                  label="E-mail"
                  placeholder="Enter your e-mail"
                  required
              >
              </input-element>
              <input-element
                  .handleChange="${this.handleChange}"
                  class="mb-4"
                  label="Password"
                  icon="eye-outline"
                  name="password"
                  type="password"
                  placeholder="Enter your password"
                  required
              >
              </input-element>
              <div class="w-full flex justify-end mt-8">
                <icon-button .loading="${this.isLoading}" @click="${this.handleSubmit}" type="button"
                             text="Sign In"
                             icon="arrow-forward-outline"></icon-button>
              </div>
            </div>
          </div>
        `
    }


}

customElements.define('sign-in-form', SignInForm)


Enter fullscreen mode Exit fullscreen mode

Don't be afraid of classes. The code is pretty clean and reusable. We have build in validation, tailwindcss and with a simple import everything works.

We have some moving parts, like the CSS but overall we reduce the boilerplate by 90%. This component can work on any project with minimal changes.

Overall Simplicity

That way, by writing your frontend like this, you not only reduce boilerplate, but deployment also becomes easy, and there is zero learning curve. You can give this to any developer, and they can understand how it works.

Compare this to using Next.js, Gatsby, or even Angular. I think the future of frontend development is heading in this direction because frontend frameworks have become too cluttered and have big learning curves.

Yet, by using literally zero packages, you can create full frontend applications and websites without needing to install anything, and they are also reusable.

Conclusion

In conclusion, writing frontend components with reusability in mind can greatly enhance your development process.

By minimizing dependencies, utilizing props for dynamic configurations, and ensuring components are plug-and-play, you can create flexible and efficient code.

Adopting technologies like Web Components with Lit and leveraging the power of JsDelivr for module management can streamline your workflow and improve performance.

This approach reduces boilerplate, simplifies deployment, and makes your components easily understandable for any developer, paving the way for a more efficient and scalable frontend development experience.

Thanks for reading, and I hope you found this article helpful. If you have any questions, feel free to email me at kourouklis@pm.me, and I will respond.

You can also keep up with my latest updates by checking out my X here: x.com/sotergreco

Top comments (0)