DEV Community

loading...
Cover image for Introducing the Marko Tags API Preview

Introducing the Marko Tags API Preview

Ryan Carniato
Frontend performance enthusiast and Fine-Grained Reactivity super fan. Author of the SolidJS UI library and MarkoJS Core Team Member.
・10 min read

The Marko Tags API is a new set of Core Tags coming to Marko. They let you use state in your templates without using classes.

Alt Text

Try this example online


No Breaking Changes

Before we continue, note that the Tags API are:

  • Completely opt-in. You can try the Tags API in a few templates without rewriting any existing code. But you don’t have to learn or use them right now if you don’t want to.
  • 100% backwards-compatible. The Tags API doesn’t contain any breaking changes.
  • Preview available now. Tags API is now available in preview for Marko 5.14.0+ by installing @marko/tags-api-preview.

Class components will continue to be supported.
However, templates that consume the Tags API cannot have classes.


Motivation

The Tags API marks a shift from Marko as a templating language with a bolted-on component API to a fully-fledged language that can describe state and updates.

Powerful composition

The last couple of years have seen build-around primitive take over the front-end ecosystem from React Hooks to Vue's Composition API. They've drastically improved developer experience by letting state be grouped by behavior rather than lifecycle. This makes it easy to compose behavior and extract it into separate reusable modules.

The Tags API brings this capability to Marko. You can build your own <let> that syncs its value with localStorage or your own <for> that is paginated. The possibilities are endless.

Flexible development

Having a language for state and updates means it can transcend the component model as we know it today. Other component libraries have introduced primitives, but still tie them to the concept of a component instance.

  • React's Hook Rules

  • Vue's and Svelte's top-level <script> tags.

With the new Tags API, lifecycle and state management can be handled anywhere within your templates even when nested under <if> and <for>.

Compiler optimizations

Marko is already one of the best options for server-rendered applications, in part due to its automatic partial hydration: only components that have state or client-side logic are even sent to the browser.

But why should we even send down entire components? What if we only send down the exact expressions that can are needed in the browser? We call this fine-grained hydration and it's made possible by the Tags API which makes it much easier to trace which values are dynamic, where they are used, and where they change. This means Marko can know exactly what code needs to run where whether on the server, in the client, or on both.

The preview version we are releasing today doesn't leverage these optimizations, but don't worry, the work on this is already well underway.


Installation

To get started using the Tags API Preview you can spin up a new project using:

> npm init marko --template tags-api
Enter fullscreen mode Exit fullscreen mode

Alternatively you can also add it to existing projects by installing the module:

> npm install @marko/tags-api-preview
Enter fullscreen mode Exit fullscreen mode

New Syntax and Concepts

There are a couple of new language-level features you need to learn to get started with the Tags API.

Default Attribute

We wanted to generalize Tag Arguments ( ), used in some internal Marko tags, with a syntax any tag can use. So we are introducing the Default Attribute.

Alt Text

This assignment happens with no explicit attribute and instead is passed to the child component as "default". It is just a shorthand but it removes a lot of verbosities when the tag conceptually has a main value that is passed to it. All existing tags that accept an argument will use this syntax instead.

Attribute Method Shorthands

To keep with Marko's terse syntax we are adding a short form for declaring function attributes that shortcuts having to write the assignment. This is very useful for things like event handlers. But also we can apply it to the default attribute to reduce the syntax for things like our <effect> tag.

Alt Text

Tag Variables

Tag Variables are a new way to get values out of tags.

Alt Text

We use a preceding slash to denote a variable name that will be created in the current scope. Left-hand side of assignment syntax is also legal such as destructuring.

Given that Marko already has Tag Parameters | | as used in the <for> tag you might wonder why the new syntax. This is all about scope. Tag parameters are designed for nested scope purposes. For things like iteration where there can be multiple copies of the same variable.

With Tag Variables the value is exposed to the whole template*.

Alt Text

*A Tag Variable will error if read before it is initialized.

Binding Events/Operators

The Tags API gives us very powerful and explicit control over state in our templates. However, it introduces a new consideration when we are passing values between tags. We are introducing a binding mechanism to handle those scenarios.

Any tag can define a matching attribute and ___Change handler that serves as a callback whenever the tag would suggest a change to its parent. The parent can intercept that change and handle it accordingly.

However, in the common case where this is a direct mapping we introduce a binding operator := that automatically writes the new value to the variable passed to the corresponding attribute.

Alt Text

We will cover more specific usage later in this article.

Stateful Dependencies

Marko's Tags API embraces the conceptual model of fine-grained reactivity. This means that when talking about stateful variables and expressions we refer to them as having dependencies.

A dependency is any stateful variable that is used to calculating an expression. Where some libraries require you to state dependencies explicitly, Marko's compiler automatically detects these variables to ensure that all templates stay up to date with the latest values and only perform work as needed.


Tags API at a Glance

<let>

<let> is the tag that allows us to define state in our templates:

Alt Text

In this example, we assign the value 0 to count. Then we increment it on each button click. This change is reflected in the <p> text.

You can add as many <let> tags as you want to your template and they can even be nested.

Alt Text

Nested tags have their own lifecycles. If showMessage changes between false and true in this case the count would be reset. If you wished to preserve the count it could be lifted above the <if> tag in the tree.

<const>

The <const> tag allows you to assign reactive expressions to a variable. Unlike a<let> variable you cannot assign to it and its value is kept in sync with its dependencies.

Alt Text

<attrs>

Marko has always had a way to interact with input passed into its templates. But now we wish to be more explicit using the <attrs> tag.

Picture a simple name-tag tag:
Alt Text

Inside its template we might describe its attrs like this:
Alt Text

We have all the syntax of destructuring available to us like setting default values, aliasing, and rest parameters.

<effect>

The <effect> tag adds the ability to perform side effects. It serves the same purpose as onMount, onUpdate, and onDestroy in Marko classes, but is unified into a single API.

For example, this template sets the document title after Marko updates the DOM:

Alt Text

The effect re-runs whenever any of its dependencies change. So every button click updates the document title.

The <effect> tag also lets us define a cleanup method by returning a function. This method run whenever the effect is re-run, or when it is finally released.

Alt Text

<lifecycle>

Sometimes it is easier to represent an external effect as lifecycles. For that reason, we are including the <lifecycle> tag.

The onMount callback is called once on the first mount and onDestroy when it is finally released. The onUpdate callback is not called on that initial mount, but whenever any of its dependencies of the onUpdate callback are updated.

The real power unlocked here is that you can use this to store references and manage your side effects as needed.

Alt Text

While the <lifecycle> tag looks a bit like a class component, it isn't intended to be used as a replacement. You can have multiple in a template and, like other tags, serves as a way to independently manage your application state.

<return>

One of the best parts of the Tags API is we can use it to create our own custom tags. The <return> tag is used to return values from your tags.

Alt Text

This is a simple example where we have just encapsulated an expression. However, we can return anything from our templates so we can use <return> to build many different types of composed Tag behaviors.

<set> and <get>

These two form the pair for Marko's Context API, that lets us share data from parent templates without having to pass them through attributes directly.

The way this works in Marko is that the provider or <set> is keyed to the template it is in. And the <get> traces up the tree until it finds the nearest parent matching the requested tag name.

Alt Text

<id>

It is often very useful to have a unique identifier in your templates. It is even more useful to have the guarantee it will be the same when rendered on both client and server. The <id> tag is a simple way to achieve that.

Alt Text


Using the Tags API

The Tags API represents more than just a syntax change and some new features. It opens up new ways to develop with Marko.

It's all Tags

We are embracing tags with Marko. Where you would have used a $ (scriptlet) in the past you can use <let>, <const>, or <effect>. We are now treating the inline style tag similar to the style block.

Most things other than import can now be done with just tags.

Alt Text

Keyless

With new explicit syntax we have removed most use cases for the key attribute. We now can access our DOM references directly as variables.

The one place where the need remains is in loop iteration. For that reason, in Tags API the <for> tag has a by attribute.

Alt Text

This allows us to set a key from the data passed in without marking a key on the child tags.

Co-location

The real power the Tags API opens up is composability and refactorability. Using template scope we can now have nested State without necessarily breaking out different components.

Alt Text

This state only lives for as long as that loop iteration is rendered. If we wanted to extract this into a separate template we could just cut and paste it.

Controllable Tags

When dealing with forms and tag wrappers there are a few different options on how to manage your state. Either the child controls the state(uncontrolled) or the parent does(controlled).

Alt Text

It is often difficult to define both behaviors without ending up with inconsistency. In the uncontrolled, form the parent can only set the initial value and any further updates to the props don't reflect. In controlled form, if the change handler is omitted the parent gets out of sync.

Marko's binding enables authoring the tag in a way where the parent can decide which mode it prefers simply by opting in.

Alt Text

Binding to the <let> allows the use of local state when the parent isn't bound or to connect directly to the parent's state when it is available. With a simple modification of our uncontrolled example now the parent can simply opt-in by choosing to bind or not.

Binding <return> and <set>

We can also use binding with <return> and <set> to expose the ability to assign new values. Consider creating a new <let>-like tag that stores in local storage.

Alt Text

This leverages our new binding operator by binding the <return>. This counter works like our previous examples, incrementing on button click. But whenever you reload the page the count will be loaded from localStorage and continue from where it left off.


Available Today

The Marko Tags API Preview is available today and works simply by including it in your projects. Files that use the new syntax will be opted in automatically.

Keep in mind this is just a preview and may change before the final version gets brought into Marko 5 and Marko 6. We believe the best way to refine the new patterns this brings is to put them in developers' hands. Your hands, to see what this means for how you author templates and think about how you approach your applications.

We are really excited about what this means for Marko. We are looking for your feedback. We are sure there will be a few kinks to work through and wrinkles to iron out. But your contribution could shape the future of Marko.


Cover illustration by @tigt

Discussion (9)

Collapse
doeixd profile image
doeixd

This looks incredible, it's unlike anything else, and will take me a bit to get the hang of it. I would love to see some examples with async data. Is there something like Solid's create Resource? Thanks for the write up, Marko 6 looks to be coming along nicely

Collapse
ryansolid profile image
Ryan Carniato Author • Edited

Yeah while Marko and Solid have a shared understanding of the mechanical aspects of frameworks, including the role of reactive language and disappearing components, there are a lot differences.

In Marko, async is handled with an <await> tag that accepts a promise and displays a placeholder. It runs on the server and streams the result when it resolves into the browser.

For Marko 6 we want to do better to allow client side fetching as well. For that we intend to introduce an tag which acts as a boundary (think Suspense) and to be consistent with everything being simple values, instead of resources we will use the compiler to detect await keyword and use that to register promises inside the boundaries. In so you will be using promises directly in your code. But more often than not it will just feed into a <const> that will trigger the tag above and render the placeholder and resolve when it completes.

<attrs/{ userId }/>
<const/user = await fetchUser(userId) />
Enter fullscreen mode Exit fullscreen mode

This will be more difficult to port back into Marko 5 given the potential for wasted re-renders of the VDOM. In Marko 6's granular system (more similar to Solid) only what needs to re-reruns. We have more details to work through as async is one of the last things we are tackling but I hope that we atleast get the simple(non-concurrent) client version working during the preview time period.

Collapse
doeixd profile image
doeixd

Thanks. That cleared things up for me.

Collapse
joshuaamaju profile image
Joshua

so many questions:

  • can effects be placed in loops.
  • will the value of collapsed be reset in the example with the collapsed variable every time the loop executes.

I used to say, "it's safe using a niche framework like Marko, it's basically html with some simple extra stuff", but I guess that's no more. And finally, passing functions to components, been waiting for that.

Collapse
ryansolid profile image
Ryan Carniato Author
  1. Yes. They can be nested. You more or less can develop a child component inline with its own state. We want you to not worry as much about components but think about what makes sense for you templating out your site/application.
  2. No. I find it more helpful to think of this in the declarative model like the DOM itself rather than the imperative VDOM model. But if you think of it in terms of For loop re-running nested state does not get reset. If you think of it in the declarative sense it only runs once when each loop iteration is created and then it just keeps in sync automatically.

It is still just HTML as it ever was. Our perspective hasn't change. This article is heavy on new syntax to show it off, but in general you are writing 80% HTML, dabble in a few components, and now instead of classes you have tags. You start from the same place of just writing HTML and then adding a couple tags here and there where you need some extra behavior. We haven't taken anything away, and through co-location we hope that the change is even less jarring than before. If you have a template you wish to manage as a single piece you don't have to split it apart for performance. This should simplify things. It also reduces the need for things like component.js files etc...

Every template language adds something to make it more than just HTML. Whether you use a tag or a class, or once you had custom event handlers in any framework they have gone beyond HTML. But it's still HTML at the foundation and that is what you will be doing most of the time anyway.

Collapse
joshuaamaju profile image
Joshua

Thanks, this would take some time getting used to. But it solves some problems I've had with the mental model of Marko, I guess that's just a product of too much React.

Collapse
trusktr profile image
Joe Pea • Edited

We call this fine-grained hydration and it's made possible by the Tags API which makes it much easier to trace which values are dynamic, where they are used, and where they change. This means Marko can know exactly what code needs to run where whether on the server, in the client, or on both.

I recalled you said Marko was more for MPA's, but by the description of "fine-grained hydration" it sounds like it would work fine for SPAs too? Can someone write an SPA with Marko?

Suppose, for example, one wishes to use custom elements for 3D WebGL rendering, and thinks to use Marko for the template logic and component hierarchy of the application, while the 3D custom elements end up as the leafs in the application markup that do the actual WebGL rendering.

They intend to make a realtime RTS or FP shooter. Will this work out with Marko? How would it differ from choosing Solid.js?

Assume the user only cares about the client side experience, and whether SSR is in place or not doesn't matter much.

(I also see Marko isn't published at custom-elements-everywhere.com yet)

Collapse
redonkulus profile image
Seth Bertalotto

Definitely getting coldfusion vibes from many years ago. I think the tags are nice for some of the lifestyle and sideeffects issues. I'm not sure how much use they will be for server only use cases. I still like the $ {} expressiveness.

Collapse
ryansolid profile image
Ryan Carniato Author

That's the nice thing here since you just add them as needed. So if things are mostly static you are just writing normal idiomatic HTML. It almost encourages you to just add little bits here and there only as needed. But the real gains for for us is the syntax is composable unlike using a keyword and still explicit so when we get to Marko 6 this unlocks incredible things. We can basically just strip out all the static stuff in between and generate the smallest optimized bundles.

The biggest thing this does for the server case over our $ scriptlets is it forces everything to be a declaration or be denoted as a side effect. Side effects run only in the browser so it keeps things streamlined. You can always import outside JavaScript or define functions still. <const> is basically the new $ and in Marko 5 Tags preview basically compiles to it. When we reveal the new Async patterns coming I think the missing pieces will come together.