E Ottaviani Aragão Blog Projetos Sobre Mim

NextJs and Server-Side Web Components — Using Jails

How to Share Functionality Across Different Projects Using Various Frameworks?

The classic and obvious answer: Web Components 🎉

But here’s the catch — this solution, though appealing, is incomplete. The big question that’s often overlooked is: how do you render Web Components on the server?

Why on the server?

Because there’s been a recent shift in how we think about rendering web applications. The goal now is to deliver as little JavaScript as possible to the user — shifting more processing to the server — to boost performance and user experience.

This creates a dilemma when trying to reuse features with Web Components across projects that use different tech stacks. After all, Web Components are built from a mix of APIs that run exclusively on the browser.

But here’s the thing — as I detailed in a previous post:

Web Components are a set of APIs providing useful tools, especially for developers building solutions, frameworks, or libraries.

Sharing features in a consistent, simple, and productive way isn’t really what Web Components are designed for. Instead, they’re a toolset — great for development but not the ultimate solution for shared functionality.

So, to handle server-side rendering (SSR) or static site generation (SSG) with frameworks like Next.js, we need a more robust approach. And in this post, I’ll introduce a solution using Jails.

The Strategy

Next.js is an isomorphic app — that means the same code can run on both the client and server.

Jails is a library designed to run only in the browser. But thanks to separation of concerns and dependency injection, it can adapt well to our specific case of an isomorphic app.

A Jails component looks like this:

export default function myComponent({ main }) {
//All client functionalities

main(() => {
// application entry point
})
}

export const model = {
initialState: {}
}

It’s essentially a JavaScript module exporting functions like main (for client-side logic) and model (state). The library uses these exports to handle client interactions.

We can create a React wrapper component, let’s call it DS, to integrate Jails components. Here’s an example:

'use client'
import { useEffect, forwardRef } from 'react'
import { register, start } from 'jails-js'

type DSProps = {
use: any
tag?: string
children?: React.ReactNode
dependencies?: Record<string, any>
}

export const DS = forwardRef((
{
use: module,
tag = '',
children = null,
dependencies = {},
...props
}: DSProps,
ref
) => {

const Tag = module.tag || tag

const html = module.markup
? module.markup({ children, ...props })
: null

useEffect(() => {
if (!customElements.get(Tag)) {
register(Tag, module, dependencies)
start(document.querySelector(Tag))
}
}, [Tag, module, dependencies])

if (html) {
return <Tag ref={ref} dangerouslySetInnerHTML={{ __html: html }} />
}

return <Tag ref={ref}>{children}</Tag>
}
)

This wrapper expects a Jails module, which is a JS component, and a tag name (like ds-input).

It also accepts children, but note they’re only compatible with plain text—not React elements.

The forwardRef is key—it allows attaching methods or retrieving info directly from the HTML element, listening to events, or modifying attributes, making integration smoother.

You’d use this component like:

import * as dsInput from '@ds/components/input'
// ...
export default function HelloWorld() {
return (
<DS use={dsInput} ... />
)
}

And here’s the kicker: this component renders on the server.

How?
Because if the JS module exports a markup function, the wrapper executes it to get an HTML string, which is injected into the DOM using dangerouslySetInnerHTML.

Next.js’s server component renders this HTML, and Jails handles the logic separately, updating only specific parts through directives, separating HTML generation from behavior. The useEffect on the client hydrates the custom elements after the server renders.

A Real-World Example — Custom Input

Here’s an example of a Jails component for a <ds-input /> element:

import { type Component } from 'jails-js'
import { html, attributes } from 'jails-js/html'

export const tag = 'ds-input'

export default function input({ main, on }: Component) {

main(() => {

on('focus', 'input', toggleClass(true))
on('blur', 'input', toggleClass(false))
})

const toggleClass = (toggle: Boolean ) => (event: Event) => {
state.set({ focus: toggle })
}
}

export const model = {
focus: false,
}

export const markup = ({
label = '',
name = '',
...props
}) => {
return html`
<div class="form-group" html-class="focus? 'input-focus': ''">
<label>${label}</label>
<input class="input-control" name="${name}" ${attributes(props)}>
</div>
`
}

Breaking down each part, we first introduce two new capabilities that make sense specifically for our project — they’re not part of the core Jails API. These are markup and tag, and they’re used by our custom DS integration component.

markup is an agnostic function that takes a properties object and returns an HTML string using those properties.

As an interesting aside, Jails imports helpers like html and attributes—which are also agnostic and can be used in any Node.js application. The attributes helper converts an object into a string in the format key="value", which is super handy for passing attributes just like in React, for example: {...props}.

To use this component, you’d do something like:

import * as dsInput from '@ds/components/input'
// ...
export default function HelloWorld() {
return (
<DS use={dsInput} name="username" id="username" ... />
)
}

This component isn’t restricted to React — it works in any framework, static site generator, or even server-side rendering backends like PHP, Java, or Go.

Going Beyond the Basics

I was pretty surprised when I tested this idea — everything worked smoothly. So I decided to go further, thinking about common cross-framework reuse cases like form components, validation, and side panels.

These scenarios tend to be tricky because frameworks like Next.js and React use a virtual DOM. External validation or updates outside the framework can cause markup mismatches between external libraries and the current DOM version, leading to potential issues.

Even so, I managed to build a pretty interesting example: delegating field validation and side-panel management to Web Components (Jails). I even integrated both Jails and React renderings on the same screen, blending the two seamlessly.

A key feature of this integration is Jails’ ability to create “holes” in the HTML using the html-static directive. These holes prevent Jails from updating those parts of the DOM when the component’s local state changes. This allows React and Jails to coexist without their virtual DOMs interfering, which is pretty cool.

The Application

The app I’m sharing is on StackBlitz so you can see it in full. It’s a server-generated form with validation behaviors handled entirely by Jails Web Components and side panels.

The React component requests a list from an API on the client side and renders it within Jails Web Components. It then passes this list to a Jails ds-select component, which renders the items on the client.

Once all data is filled out and validated, the form can be submitted. The React page component listens for a custom event from the Jails Web Component and communicates with side panels to display the data.

One side panel shows the form’s filled data as rendered by Jails Web Components, while the other displays it via React rendering — pretty neat, right?

Final Thoughts

I chose a common yet challenging use case — form validation — to test my hypothesis that Jails components can be server-rendered. Handling validation in forms is notoriously complex due to the mixture of components from different frameworks.

I still need to explore more use cases to fully validate this combined React and Jails rendering — since, in some scenarios, the behavior could get unpredictable.

However, in scenarios where it’s unnecessary to embed dynamic data via React inside Web Components, I’m quite confident it works really well.

That’s it for now! I’m genuinely excited about how this project is progressing — without significantly increasing code complexity. Often, you can use it as-is across various setups, thanks to its flexible and decoupled architecture.

Until next time!


NextJs and Server-Side Web Components — Using Jails was originally published in Jails-org on Medium, where people are continuing the conversation by highlighting and responding to this story.