How does a ref
attr in a Stencil component get called,
and in what circumstances will it be updated with a new value?
This is, it turns out, a bit hard to fully track down!
Let’s go from the inside-out.
setAccessor
A ref
callback will be called with a reference to the
DOM node a Stencil component is attached to here in the
setAccessor
function, which is responsible for setting
various attributes on a DOM element for a new Vdom node:
src/runtime/vdom/set-accessor.ts:L59-L64
else if (BUILD.vdomRef && memberName === 'ref') {
} // minifier will clean this up
if (newValue) {
newValue(elm);
}else if ( }
that function is, in turn, called in two places within
updateElement
, a function taking old and new Vdom nodes and
essentially porting the attributes from the old node to the new:
src/runtime/vdom/update-element.ts:L24-L36
if (BUILD.updatable) {
// remove attributes no longer present on the vnode by setting them to undefined
for (memberName in oldVnodeAttrs) {
if (!(memberName in newVnodeAttrs)) {
setAccessor(elm, memberName, oldVnodeAttrs[memberName], undefined, isSvgMode, newVnode.$flags$);
}
}
}
// add new & update changed attributes
for (memberName in newVnodeAttrs) {
setAccessor(elm, memberName, oldVnodeAttrs[memberName], newVnodeAttrs[memberName], isSvgMode, newVnode.$flags$);
}
So of the two setAccessor
calls here:
What is updateElement
doing? It’s basically called any
time a new and old Vdom node need to be reconciled. Additionally, on the
first render when there is no old Vdom node it will be called with
null
, so it handles that case too.
Where is that called? It’s called by two VDom functions,
createElm
and patch
.
createElm
The createElm
call is pretty straightforward - that
function is called when we need to create a DOM node corresponding to a
new Vdom node which didn’t previously have a DOM element associated with
it (because its…new).
So the updateElement
call in this case passes
null
for the old Vdom node:
src/runtime/vdom/vdom-render.ts:L103-L106
// add css classes, attrs, props, listeners, etc.
if (BUILD.vdomAttribute) {
updateElement(null, newVNode, isSvgMode);
}
In updateElement
we then have (as seen above) a few
lines at the bottom of the function:
src/runtime/vdom/update-element.ts:L33-L36
// add new & update changed attributes
for (memberName in newVnodeAttrs) {
setAccessor(elm, memberName, oldVnodeAttrs[memberName], newVnodeAttrs[memberName], isSvgMode, newVnode.$flags$);
}
If the new Vdom node has a ref
attr the
setAccessor
call here with that ref
callback
will end up calling it, since in setAccessor
there’s just a
check before that happens that the new value for the attr is
defined.
Great, so that explains how for a new node the ref
callback will be called! 🎉
patch
and
renderVdom
and friendsThe patch
function is just a bit more involved to unwind. This function, as
you might suspect, reconciles an old Vdom tree with a new Vdom
tree, making any edits to the DOM that are necessary to produce the
right output. It receives old and new Vdom nodes as arguments.
patch
is in turn called by renderVdom
, then
entry point for Stencil’s Vdom:
src/runtime/vdom/vdom-render.ts:L805-L809
export const renderVdom = (hostRef: d.HostRef, renderFnResults: d.VNode | d.VNode[]) => {
const hostElm = hostRef.$hostElement$;
const cmpMeta = hostRef.$cmpMeta$;
const oldVNode: d.VNode = hostRef.$vnode$ || newVNode(null, null);
const rootVnode = isHost(renderFnResults) ? renderFnResults : h(null, null, renderFnResults as any);
In that function we grab a previous Vdom tree off of the
hostRef
if present, and create a new empty node if not. The
rootVnode
is also passed in as an argument from the calling
context.
This is a normal Vdom pattern - you have a factory function,
h
, that takes a runtime representation of DOM attributes
and so on and returns a tree of DOM nodes, and then a render function
takes that tree and a DOM node and makes it work.
This entry point into the Vdom world is then called in the code that handles updating components.
update-component.ts
This module handles, well, updating components! Great!
Basically there are some functions like scheduleUpdate
and dispatchHooks
which do things like emitting lifecycle
events, calling lifecycle functions, and so on. It also, crucially,
kicks off the first call to updateComponent
:
src/runtime/update-component.ts:L67-L67
return then(promise, () => updateComponent(hostRef, instance, isInitialLoad));
The updateComponent
function then in turn calls
callRender
which then, in turn, conditionally calls
renderVdom
:
src/runtime/update-component.ts:L167-L176
if (BUILD.vdomRender || BUILD.reflect) {
// looks like we've got child nodes to render into this host element
// or we need to update the css class/attrs on the host element
// DOM WRITE!
if (BUILD.hydrateServerSide) {
return Promise.resolve(instance).then((value) => renderVdom(hostRef, value));
else {
} renderVdom(hostRef, instance);
}else { }
The long and short of this is that any time that a component updates
the updateComponent
function should end up being called
which should result in a DOM re-render and can then normally
result in the ref
callback being called again as well - see
below for more on that.
So above we reviewed the code paths that can lead to the bits of
setAccessor
which actually call the ref
callback being called:
src/runtime/vdom/set-accessor.ts:L59-L63
else if (BUILD.vdomRef && memberName === 'ref') {
} // minifier will clean this up
if (newValue) {
newValue(elm);
}
Here’s the if
conditional:
src/runtime/vdom/set-accessor.ts:L24-L24
if (oldValue !== newValue) {
The complication here is that if the value passed to the
ref
attr doesn’t change between re-renders then it will
only ever be called once.
This normally won’t be an issue because people use an inlined arrow function, like so:
<div ref={el => (this.divEl = el)}>gonna update</div>
However, if you have a component like this the ref
callback will only get called once:
@Component({
tag: 'my-component',
styleUrl: 'my-component.css',
shadow: true,
})
export class MyComponent {
render() {
return <div ref={stableReference}>
Hello, World! I'm not gonna call my ref more than once.
</div>;
}
}
function stableReference(el) {
console.log(el);
}
This was a little surprising to me!