Joe C
Joe C

Reputation: 1855

How to pass more than 1 ref to a child component in SolidJS?

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

Answers (2)

snnsnn
snnsnn

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

Caleb Taylor
Caleb Taylor

Reputation: 3230

When Refs set in Components

There's 3 ways to reference a DOM element inside a component.

  1. Pass regular variable
function Component() {
  let buttonEl;

  onMount(() => {
    console.log(buttonEl) // logs button element
  }) 

  return (
    <button ref={buttonEl}>Click</button>
  );
}
  1. Pass signal setter
function Component() {
  const [buttonEl, setButtonEl] = createSignal(null);

  onMount(() => {
    console.log(buttonEl()) // logs button element
  }) 

  return <button ref={setButtonEl}>Click</button>;
}
  1. 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.

  1. 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>;
}
  1. 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>;
}
  1. 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
    }));
    
    // ...

  })();
}

Upvotes: 14

Related Questions