Tag Variable Hosting (round 3)
So… While trying to implement tag variable hoisting, we ran into some conceptual issues. What happens in these cases?
-
<my-input-range>
-
<dims>
-
<carousel>
with paddles
Overview
Hoisting becomes a problem when we can create cycles. Marko has the additional challenge that due to the nature of our reactive system (compiled & push-based), we can have cases (<${dynamic}>
being the big one) where a hoisted value shouldn't need to be circular but we compile such that input to a tag is grouped together and it is circular.
So we have 4 classes of usage of variables
- No hoist
- Non-circular hoist
The value is used outside the scope it is introduced in, or before it in the source order, but its usage does not intersect with the definition in any way.
- Component-circular hoist
These could be dependency circular, but they could also not be. Depends on the implementation of <my-tag>
.
- Dependency-circular hoist
In the case of a circular hoist (definitely 4, probably 3 unless we make some significant changes to the reactive system - switch to pull-based), we have to have some rules in place to prevent the cycle.
There is also the question of where we apply these rule.
- Only to circular (3/4)
- or to all hoists (2/3/4)?
Option 0: No Circular Hoisting
A hoisted variable (or values derived from it) cannot be passed to a tag that wraps the variable.
Works:
Not Allowed:
Pros:
Cons:
Option 1: Functions only
Restrictions
- If you return a value and render content, the returned value must be a function
- A tag variable that's hoisted
- Must be a function
- Can't be called/invoked during render phase
If a .marko
template renders any DOM nodes, it is only allowed to return functions. That means that <my-input-range>
with a return is not possible, and instead the way to accomplish the same goal is with something like
Pros:
- We can allow double renders at some point in the future
Cons:
- We need element refs to be functions
- The
<dims>
example can't ever be reactive
Option 2: Objects via Proxies
Restrictions
- If you return a value and render content, the returned value must be an object
- A tag variable that's hoisted
- Must be an object
- Can't be destructured or have properties accessed during render phase
- Can't be called/invoked during render phase
- Properties of native element refs can't be read during the render phase
Pros
- We don't need element refs to be functions?
- We can allow double renders at some point in the future
Cons
- Proxies can't be passed to DOM apis, which is a problem for native element refs
- Devs will probably see proxies all over the place where they don't expect them
- Proxies are slow
Option 3: Double Render
The hoisted value is initially undefined (but would be defined before the effect phase?). It then re-executes with the actual value.
Pros:
- No usage limitations
- Michael's excited about it so he might do it
Cons:
- How do we double render on the server? Ideally won't send additional code to the browser if
- Requires null checks on all hoisted values
- Extra work being done
- Requires a rethink of the server runtime (probably)
- Do we ever have to triple render? Quadruple?
- If a double render happens to redefine the property it will cause an infinite loop. This may be impossible to avoid in places where expressions are grouped together like the dynamic tag.
Addon 1: Explicit Hoisting
Many solutions have differing behavior of hoisted values. Having explicitly hoisted values may make it more clear where this unique behavior is present.
Exposgt a global $hoist
function that must wrap any hoisted value.
Addon 2: Explicit Iteration
We've discussed making hoisted, non-primitive values iterable so if a variable is rendered multiple times you can access all values.
However this doesn't support primitive values and may benefit from explicit syntax.
Expose a global $hoistAll
function that finds all tag variables with a given name and returns a list