Reputation: 1855
Parent Component:
function ParentComponent() {
return (
<section>
<ChildComponent ref={sectionRef} ref1={headerRef} />
</section>
);
}
Child Component:
function ChildComponent(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>
);
}
My goal is to be able to target different DOM elements in the child component from the parent component so that I can animate them based on a scroll event from the parent.
I have tried to pass the refs as different data structures:
<ChildComponent ref={{sectionRef, headerRef}} />
and:
<ChildComponent ref={[sectionRef, headerRef]} />
and:
<ChildComponent section={sectionRef} header={headerRef} />
But continuously get errors that the 2nd ref is undefined. I can only get it to work if I pass in a single ref per child component. Any ideas?
Links to reference material I looked at: https://www.solidjs.com/tutorial/bindings_forward_refs https://www.solidjs.com/docs/latest/api#ref
Upvotes: 9
Views: 3869
Reputation: 13698
Since you want to interact with children on parent's scroll, which has nothing to do with the state, you can skip all the ceremony and access children directly by taking advantage of solid being compiled to native JavaScript.
import { render } from 'solid-js/web';
const App = () => {
const handleScroll = (event: any) => {
const header = event.currentTarget.querySelector('header');
const section = event.currentTarget.querySelector('section');
console.log(header, section);
};
return (
<div
style={`position: fixed; overflow: scroll; width: 100vw; height: 100vh`}
onScroll={handleScroll}
>
<header style={`height: 500px`}>Header</header>
<section style={`height: 500px`}>Section</section>
</div>
)
};
render(App, document.body);
Live Demo: https://playground.solidjs.com/anonymous/19a789fe-d710-40ba-8b3e-a256ab23c1e2
See, you don't need all that React's twisted mindset.
If your child component somehow depends on parent's state, i.e its y
position, it is best to pass the parent's state as prop and add event inside the child's body:
import { render } from 'solid-js/web';
import { Accessor, Component, createEffect, createSignal } from 'solid-js';
const Child: Component<{ y: Accessor<number> }> = (props) => {
createEffect(() => {
console.log(props.y());
});
return <div>Child Component</div>
}
const App = () => {
const [y, setY] = createSignal<number>(0);
setInterval(() => {
setY(v => v + 1);
}, 1000);
return (
<div>
<Child y={y} />
</div>
)
};
render(App, document.body);
https://playground.solidjs.com/anonymous/55405c86-b811-4ef7-8cfc-788f74ea0681
If state is object but you need only one property, you can derive the state:
// interface State { x: number, y: number }
const y = () => state().y;
// Then pass it as a prop
<Child y={y} />
Refs are for accessing element inside the component's body for things like adding and removing event listeners. Don't pass them around if you can help it and certainly don't overuse them. Because state+effects brings predictability, refs bring chaos.
Upvotes: 1
Reputation: 3230
There's 3 ways to reference a DOM element inside a component.
function Component() {
let buttonEl;
onMount(() => {
console.log(buttonEl) // logs button element
})
return (
<button ref={buttonEl}>Click</button>
);
}
function Component() {
const [buttonEl, setButtonEl] = createSignal(null);
onMount(() => {
console.log(buttonEl()) // logs button element
})
return <button ref={setButtonEl}>Click</button>;
}
function Component() {
let buttonEl;
const refCallback = (el) => {
buttonEl = el;
};
onMount(() => {
console.log(buttonEl); // logs button element
});
return <button ref={refCallback}>Click</button>;
}
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.
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>;
}
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>;
}
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
.
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>
);
}
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
}));
// ...
})();
}
Upvotes: 14