When Refs set in Components
There's 3 ways to reference a DOM element inside a component.
- Pass regular variable
function Component() {
let buttonEl;
onMount(() => {
console.log(buttonEl) // logs button element
})
return (
<button ref={buttonEl}>Click</button>
);
}
- Pass signal setter
function Component() {
const [buttonEl, setButtonEl] = createSignal(null);
onMount(() => {
console.log(buttonEl()) // logs button element
})
return <button ref={setButtonEl}>Click</button>;
}
- Pass callback
function Component() {
let buttonEl;
const refCallback = (el) => {
buttonEl = el;
};
onMount(() => {
console.log(buttonEl); // logs button element
});
return <button ref={refCallback}>Click</button>;
}
When Refs set in Child Components
However when referencing a DOM element that is set in child components, the situation is different. For demonstration we won't use ref
prop for Child component but instead PASSREF
prop.
- Pass regular variable to
PASSREF
. The variable doesn't update and is undefined
.
function Component() {
let buttonEl;
onMount(() => {
console.log(buttonEl); // logs `undefined`
});
return <Child PASSREF={buttonEl} />;
}
function Child(props) {
return <button ref={props.PASSREF}>Click</button>;
}
- Pass signal setter to
PASSREF
, works.
function Component() {
const [buttonEl, setButtonEl] = createSignal(null)
onMount(() => {
console.log(buttonEl()); // logs button element
});
return <Child PASSREF={setButtonEl} />;
}
function Child(props) {
return <button ref={props.PASSREF}>Click</button>;
}
- Pass callback, that is declared in same scope as
buttonEl
, to PASSREF
, works.
function Component() {
let buttonEl;
const refCallback = (el) => {
buttonEl = el;
};
onMount(() => {
console.log(buttonEl); // logs button element
});
return <Child PASSREF={refCallback} />;
}
function Child(props) {
return <button ref={props.PASSREF}>Click</button>;
}
To fix #1 solution where you use the regular variable let buttonEl;
, you use the correct component prop ref
in order to set the element to the variable.
function Component() {
let buttonEl;
onMount(() => {
console.log(buttonEl); // logs button element
});
return <Child ref={buttonEl} />;
}
function Child(props) {
return <button ref={props.ref}>Click</button>;
}
So why does this work? Well because in the compiled output the Child prop argument where ref is used is actually replaced by an inline callback, that way it lives in the same scope where buttonEl
is declared and can be updated.
// This is NOT how the Compiled Output actually looks,
// but ref argument is replaced by an inline callback
function Component() {
let buttonEl;
onMount(() => {
console.log(buttonEl); // logs button element
});
return <Child ref={(el) => buttonEl = el} />;
}
function Child(props) {
return <button ref={props.ref}>Click</button>;
}
Doesn't that look familiar? This structured almost exactly to the #3 solutions, where you pass a callback function to update the buttonEl
.
Solution
Honestly it depends on your use case, either use signals setters, from createSignal
, to pass refs, or use callback functions declared in the parent to set your plain variables.
In this solution example, both sectionRef
and headerRef
are unassigned variables. sectionRef
is passed to ref
prop, where behind the scenes, it's wrapped in a callback. A callback function refCallback
is passed to ref1
prop where it sets headerRef
to passed element value.
function ParentComponent() {
let sectionRef;
let headerRef;
const refCallback = (el) => {
headerRef = el
}
onMount(() => {
console.log(sectionRef); // logs section el
console.log(headerRef); // logs header el
});
return (
<section>
<Overview ref={sectionRef} ref1={refCallback} />
</section>
);
}
function Overview(props) {
return (
<section ref={props.ref}>
<article>
<h2 ref={props.ref1}>Lorem Ipsum</h2>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Maxime
mollitia, molestiae quas vel sint commodi repudiandae consequuntur
voluptatum laborum numquam blanditiis harum quisquam eius sed odit
fugiat iusto fuga praesentium optio, eaque rerum!
</p>
</article>
</section>
);
}
How
To reiterate again. The way that Solid makes it work is that in the compiled output, if a component property is named ref
, it is replaced by an object method (in this context has the same strategy like the callback function) that is located in the same place where "ref" variable (such as sectionRef
) is created, that way the "ref" variable can be assigned to it.
If you're curious, here's the actual compiled output of solution, where you can see how ref
actually looks like.
// Compiled Output
function ParentComponent() {
let sectionRef;
let headerRef;
const refCallback = el => {
headerRef = el;
};
// ...
return (() => {
const _el$ = _tmpl$.cloneNode(true);
insert(_el$, createComponent(Overview, {
ref(r$) {
const _ref$ = sectionRef;
typeof _ref$ === "function" ? _ref$(r$) : sectionRef = r$;
},
ref1: refCallback
}));
// ...
})();
}