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.
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
Alternatively you can also add it to existing projects by installing the module:
> npm install @marko/tags-api-preview
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.
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.
Tag Variables
Tag Variables are a new way to get values out of tags.
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*.
*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.
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:
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.
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.
<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:
Inside its template we might describe its attrs like this:
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:
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.
<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.
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.
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.
<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.
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.
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.
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.
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).
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.
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.
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
Top comments (12)
This is awesome! Can't wait until the optimization comes out
I have a question about cleanup of `
Does it mean that cleanup will be run for all dependency changes since that is when the effect is run. I only expect the effect to be run when it is released
It does mean that. But it also makes sense. The effect makes something on each run (ie creates the side effect) and then would need to be released. It's symmetrical pair.
In order to get out of that if you create something in the effect and would want to check that it is already created on then next run you'd need to hoist that out. Hoisting that into
<let>
would make it stateful and cause updates itself which might not be intended.Picture a subscription for example. You'd presumably need to know if you were already subscribed if it ever ran again. Now if the data doesn't change then you never need to re-run the effect. If it does in a way that would change what you are subscribing to, release and recreate makes sense. If it doesn't well you need to now keep track of that externally.
So if you need something that persists with a reference over multiple updates the
<lifecycle>
tag is exactly what you are looking for as it has a built in mechanism to preserve its references.Thanks for the explanation @ryansolid !
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
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.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.
Thanks. That cleared things up for me.
so many questions:
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.
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.
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.
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)
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.
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.