Skip to main content

· 9 min read
Pedro Marquez Soto

After a walk down memory lane in web development, let us jump into the question: Why did I wrote LeanJSX?

TLDR; LeanJSX is an alternative for all those web-based projects when "you don't need a framework" but you still want to build server-driven web applications in a modular, component-based approach that React has made popular for the past decade, without having to use React.

It's like writing React applications, but using vanilla JavaScript and plain HTML under the hood.

If not a framework, then what?

In the previous blog post I mentioned I agree with the idea that not all web applications need a full-fledged JavaScript framework:

"There is a high change that your application don't need a framework"

One day after catching myself saying these words, I also asked myself:

"Then, what do I need?"

It's an oversimplification of the actual question in my head. A better wording would be: "If most applications don't need a JavaScript framework, what options do they have?".

The only answer I've heard for questions like that is just "vanilla JavaScript and HTML".

So I tried it, and I really disliked it. I mean, I could see the didactic benefit of manually writing a web application using only HTML and JavaScript, but I could also see how hard to maintain that was.

Going vanilla

Let's look at a simple example of vanilla JavaScript. You have a simple HTML button:

<button>Click me</button>

Now, a button that does nothing when you click it is useless. Let's keep throwing vanilla at it.

First, we need to get a reference to this element in JavaScript and add an event listener:

const button = document.querySelector('button');
button.addEventListener('click', (ev) => {
console.log('Click!')
})

Not too bad, but also not good:

  • There can be more than one button, so we need to add a ID or any other unique attribute to the button.
  • This has to be careful wrapped in another event listener for DOMContentLoaded or load. Otherwise, the button may not be there at all!.
<button id="btn1">Click me</button>
document.addEventListener('DOMContentLoaded', () => {
const button = document.querySelector('#btn1');
button.addEventListener('click', (ev) => {
console.log('Click!')
})
})

Again, not to bad, but I wouldn't call this "awesome". From a development ergonomics point of view:

  • You have to keep two separate context in mind at the same time.
  • This is the minimal code needed for one single event handler. No way around the "get element reference, then add event listener" two-step process.

Now, compare that with JSX:

<button onclick={() => {console.log('Click')}}>Click me</button>

Obviously is not that simple: This needs to be transpiled into actual JavaScript, loaded and rendered. And if we need to support SEO, it has to be SSR'ed. But leave those things aside for a second, and focus on the JSX itself:

  • No need to switch context (and files) between HTML and JavaScript. It's all in the same place.
  • Not manually having to get a reference to the DOM element.
  • No need to guarantee that the DOM is loaded. Whoever implemented the underlying JSX rendering took care of that.

I won't argue that this is the best solution to this ergonomics problem, but it is a solution, and one that thousands of developers are already familiar with, for that matter.

More vanilla challenges: Sharing data between JavaScript and HTML

I think one of the things that I disliked the most of pure vanilla JavaScript is around development ergonomics: There is just so much manual wiring needed to just make a button "clickable", wiring that is hard or just time consuming to abstract a way in a maintenable way.

The previous example could have been addressed by just bringing the old, reliable tools: Go JQuery on that button!

But, what if we want to write a button to greet our users?

<button id="btn1">Say hi to John</button>
document.addEventListener('DOMContentLoaded', () => {
const button = document.querySelector('#btn1');
button.addEventListener('click', (ev) => {
console.log('Hi John!')
})
})

It's all good if "John" can be hardcoded. But, what if that comes from a database?

Now the whole thing gets more complicated:

<button id="btn1">Loading...</button>
document.addEventListener('DOMContentLoaded', () => {
const button = document.querySelector('#btn1');

fetchUser().then(user => {
button.textContent = `Say hi to ${user.firstName}`
button.addEventListener('click', (ev) => {
console.log(`Hi ${user.firstName}`)
})
})
})

Users will only be able to click on this button after the whole page loads and the user information is fetched. Before that, you only have a useless button. JQuery itself cannot save us from this.

Of course, this is only one approach. You could also:

  • Return the data on page load as an embedded stringified JSON and retrieve it using JavaScript, then updating the button contents and the event listener.
    • We save ourselves a round-trip to the server, at the cost of having to manually store state somewhere in the page, being careful of avoiding naming collitions because this is now global state.
  • Render both HTML and JavaScript code on the server.
    • That could save us from having to show a "Loading..." message and have to update the button contents with JavaScript, but we get a new set of challenges:
      • The contents of the JavaScript file need to be dynamically rendered on the server. Writing tests for this would be a nightmare.

None of this solutions are impossible. In fact, we've been doing this for decades in traditional server-driven web frameworks.

But let's just look again to JSX:

async function GreeBtn() {
const user = await fetchUser();
return <button onclick={() => {
console.log(`Hi! ${user.firstName}`)
}}>
Say hi to {user.firstName}
</button>
}

Again, leaving implementation work aside, the development experience is very straightforward:

  • All code related to this piece of content is collocated.
  • All rendering-related behavior is encapsulated in a single place, instead of being spread in multiple files
    • Granted, most of the time the code for the event handler wouldn't be fully inlined, but in JSX you can just define a function right above the JSX definition.

But we cannot ignore the implementation work anymore, nor the concerns many developers have regarding React Server Components (to which this last JSX example is extremely similar):

  • React client components cannot be async.
  • React server components are still working on defining better boundaries between which components run in the server versus which run on the client, to avoid confusions or leaking sensitive information from the server into the client.
  • At the end, this is pure JavaScript, which needs to be SSR'd, loaded from a bundle on the client and re-hydrated, all before the button can be useful.

It would be awesome if we could just write this piece of JSX, have it rendered once directly as HTML and get all JavaScript just for the event handling wiring for free. Here is where a make my case for LeanJSX.

LeanJSX in the middle land

In LeanJSX, the last example of JSX we just saw is a valid component. Also the following example is valid:

async function* GreeBtn() {
// render something while we wait:
yield (<>Loading</>)

const user = await fetchUser();
return <button onclick={() => {
console.log(`Hi! ${user.firstName}`)
}}>
Say hi to {user.firstName}
</button>
}

LeanJSX will take this compoenent and translate it into pure HTML and a bit of vanilla JavaScript, which then -with the power of chunked transfer encoding will be streamed and rendered in the browser piece-by-piece:

<div data-placeholder="element-0">Loading</div>

<template id="element-0">
<<button data-action="element-1">Say hi to John</button>
<script>
(function(){
document.querySelector('[data-action="element-1"]').addEventListener('click', () => alert(`Hi! John`))
}).call({})
</script>
</template>
<script>
sxl.fillPlaceHolder("element-0");
</script>

Looks a bit convulated, but in practice this is what happens:

  • A placeholder element containing Loading will be rendered in the browser.
  • Once the user information is fully fetched, a <template> element is sent to the browser, along with inlined JavaScript code which will:
    • Replace the loading placeholder with the actual button contents.
    • Create the event handler for the onclick event, passing the correct handler to it.

I know, you may have taken a step back in horror after seeing those inlined <script> tags. But there is a good reasong behind that: The button will be rendered and interactive before the page or any other JavaScript bundle finishes loading. No need to wait for a DOMContentLoaded event.

Wait, but sxl.fillPlaceHolder needs to come from somewhere, right?

That is correct. LeanJSX does include one single JavaScript file at the top of the document. You can see the pre-minified contents of that file here.

It's a minimal-sized file which, adding GZIP and cache in top of it, has an almost neglectible impact on the page. the size for this script also remaing constant regardless of the number of components in your application.

As long as you're careful of not passing tons of code to your event handlers, that blocking, inline JavaScript code will be pure goodness.

Finally: The use case for LeanJSX

LeanJSX is not a framework. It's basically a rendering engine for the server that uses JSX and defines a set of conventions on how to build web UIs; these conventions are based in how we currently build React applications.

I built LeanJSX for all those cases when "you don't need a framework" but you still want to build server-driven web applications in a modular, component-based approach.

It's like writing React applications, but using vanilla JavaScript and plain HTML under the hood.

For a better insight on the use cases and limitations of LeanJSX, take a look at our docs.

There you can also find an in-dept explanation of how LeanJSX works under the hood, and how it handles some of the challenges of building modern web applications.

· 5 min read
Pedro Marquez Soto

Now that the first Alpha relase for LeanJSX is out, I wanted to share with y'all some background about the project.

For this, I have written two blog posts:

  • A quick look back into the state of web development
  • Where LeanJSX fits in that history

TLDR; Server-rendered web applications were not great in the past, which is why we ended up having JavaScript frameworks like Angular and React. This is may not the case anymore (at least not for all projects), which is why it's a great time for something like LeanJSX.

The case for (and against) web frameworks

More than once I've heard the words that many senior web developers like to repeat at the height of JavaScript fatigue:

"There is a high change that your application don't need a framework"

I fully agree with that thought. After years working with different JavaScript frameworks, from their humble beginnings with JQuery all the way to the latest React/Server Components trend, I can see how we have gradually pushed ourselves into the realms of over-engineering.

And don't get me wrong, I love working with JavaScript frameworks and all the latest tooling that come with them. I really enjoy working with things like TypeScript and bundlers -things that now come as part of these tools by default-, and being able to build full web applications without having to start an API server.

However, I also recognize that, as powerful as they are, these tools don't come for free. Adopting them has a cost in development effort, maintenance and above all, performance. Many applications have requirements for which these are valid trade-offs, but being honest, most of the time this is not the case.

To SPAs and back

It's been more than 13 years ago that AngularJS started making the rounds in web development. Back then, building complex web applications -things beyond simple blogs and personal websites- was a task that was tightly coupled with the backend and strongly divided in two parts:

  • Write templates in the server-side (PHP, JSPs, etc).
  • Write JavaScript independently for UI widgets and content (JQuery, ExtJS, Mootools, etc).

You could write each independently, but you couldn't test and execute each in isolation. I spent months maintaining a static copy of the web application just so I could work on the JavaScript side faster.

AngularJS didn't do much to change that, but introduced patterns that were relatively new for web development: Dependency injection, mocking, unit testing, etc; things that you needed to implement yourself before if you wanted them in your web code -which seldom happened-. For many people, including me, that started a revolution on how to build web applications.

Fast forward to 2023, and now you have web developers who not only have never had to build a backend-only server, but who have never worked with pure HTML/JS/CSS. I don't mention this as a critic, but only to outline how far we've gotten with web-development tooling alone.

But now, things like React Server Components and SSR -in an effort to fix SEO problems and improve performance- are moving us back to those old days of server-rendered web content.

Having gone almost full-circle, it is a good moment to stop and see were we're standing.

The "reinvention" of server-rendered content

SPAs were created in an effort to create performant web applications. Server-rendered, multi-page applications (MPAs) were looked down as being bad for performance, and most of the time they were; not because they were inherently bad but due many reasons:

  • The increasing need for better looking, more dynamic web applications.
  • Browsers and servers were ill prepared for dynamic applications. Moving logic to JavaScript was basically a hack to work around the lack of support of traditional web development tooling for increasingly complex web applications.
  • The lack of maturity of supporting infrastructure. Databases and other services were not as optimied for web traffic as they are today.

I try to mention this things without over-generalizing, as obviously this wasn't the case for all tools, servers and applications. But we cannot deny that JavaScript-driven applications came to fill a whole MPA shaped.

These things are not true anymore. In fact, the most common performance challenges in web applications come around network latency -a challenge made even more common due the extended usage of mobile devices-, and increasing bundle sizes.

The major reason driving the re-adoption of server-rendered component is exactly network latency:

  • Requests for data fetching done on the server tend to be faster than those performed on the users devices.
  • Server-rendered content in many cases can mean reduced JavaScript bundles.

A big telling that server-rendered content is not a huge bottleneck anymore is that we're starting to use it again to improve performance.

It is now a great moment to revisit our pre-conceptions about server-side web development and MPAs, which is in part what we will do in the next post.

· 2 min read
Pedro Marquez Soto

I'm very excited to announce that the first relase for LeanJSX is out for everyone to try!

My plan for the first blog post for the project was to give some background on the reasons why I chose to build and publish LeanJSX publicly.

But before going into nicer topics, first things first:

Alpha Release Notice 🚀

We're thrilled you're considering using LeanJSX for your next project! Please note that this framework is currently in an Alpha stage. While we've worked hard to provide a robust and feature-rich experience, you may encounter unexpected behavior or missing features.

🛠 What this means for you

The API may undergo changes; updates might require you to adjust your code. While we have some testing in place, edge cases can occur, and performance may not be fully optimized for all use cases.

🌟 Why try it anyway?

Be among the first to adopt new (yet hopefully familiar) server-driven rendering techniques!

Your feedback at this stage is incredibly valuable for the project's maturity and could shape its future direction. Engage with a community that's passionate about creating a lightweight yet powerful alternative to client-side frameworks.

We highly encourage you to experiment with LeanJSX and would love to hear your thoughts, suggestions, and any issues you may encounter. Your contributions at this stage are especially impactful.

For any comments or suggestions, feel free to open an issue in the main Github repo:

https://github.com/lean-web/lean-jsx