Как передать более 1 ссылки дочернему компоненту в SolidJS?

Родительский компонент:

      function ParentComponent() {
 return (
    <section>
      <ChildComponent ref={sectionRef} ref1={headerRef} />
    </section>
  );
} 

Дочерний компонент:

      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>
  );
}

Моя цель состоит в том, чтобы иметь возможность ориентироваться на разные элементы DOM в дочернем компоненте из родительского компонента, чтобы я мог анимировать их на основе события прокрутки от родителя.

Я пытался передать ссылки как разные структуры данных:

      <ChildComponent ref={{sectionRef, headerRef}} />

а также:

      <ChildComponent ref={[sectionRef, headerRef]} />

а также:

      <ChildComponent section={sectionRef} header={headerRef} />

Но постоянно получаю ошибки, что 2-й ref не определен. Я могу заставить его работать, только если я передам один ref для каждого дочернего компонента. Любые идеи?

Ссылки на справочные материалы, которые я просмотрел: https://www.solidjs.com/tutorial/bindings_forward_refs https://www.solidjs.com/docs/latest/api#ref

2 ответа

Когда Refs установлены в компонентах

Есть 3 способа сослаться на элемент DOM внутри компонента.

  1. Передать обычную переменную
      function Component() {
  let buttonEl;

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

  return (
    <button ref={buttonEl}>Click</button>
  );
}
  1. Установщик сигналов пропуска
      function Component() {
  const [buttonEl, setButtonEl] = createSignal(null);

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

  return <button ref={setButtonEl}>Click</button>;
}
  1. Пропустить обратный вызов
      function Component() {
 let buttonEl;

 const refCallback = (el) => {
   buttonEl = el;
 };

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

 return <button ref={refCallback}>Click</button>;
}

Когда Refs установлены в дочерних компонентах

Однако при ссылке на элемент DOM, установленный в дочерних компонентах, ситуация иная. Для демонстрации мы не будем использовать prop для дочернего компонента, а вместо этого будем использовать prop.

  1. Передать обычную переменную в . Переменная не обновляется и 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. Передайте установщик сигналов в , работает.
      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. Передайте обратный вызов, который объявлен в той же области, что и , в PASSREF, работает.
      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>;
}

Чтобы исправить решение № 1, в котором вы используете обычную переменную let buttonEl;, вы используете правильное свойство компонента, чтобы установить элемент в переменную.

      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>;
}

Так почему же это работает? Хорошо, потому что в скомпилированном выводе аргумент Child prop, где используется ref, фактически заменяется встроенным обратным вызовом, таким образом, он живет в той же области, где объявлен и может быть обновлен.

      // 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>;
}

Разве это не выглядит знакомо? Это структурировано почти точно по решениям № 3, где вы передаете функцию обратного вызова для обновления buttonEl.

Решение

Честно говоря, это зависит от вашего варианта использования, либо используйте установщики сигналов, от createSignal, для передачи ссылок или используйте функции обратного вызова, объявленные в родительском элементе, для установки простых переменных.

В этом примере решения и являются неназначенными переменными. передается в реквизит, где за кулисами он завернут в обратный вызов. Функция обратного вызова refCallbackпередается ref1опора, где она установлена headerRefк переданному значению элемента.

      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>
  );
}

Как

Чтобы еще раз повторить. Способ, которым Solid заставляет это работать, заключается в том, что в скомпилированном выводе, если свойство компонента имеет имя, оно заменяется методом объекта (в этом контексте имеет ту же стратегию, что и функция обратного вызова), который находится в том же месте, где " ref" (например, sectionRef), таким образом, ему может быть назначена переменная "ref".

Если вам интересно, вот фактический скомпилированный вывод решения, где вы можете увидеть, как refна самом деле выглядит.

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

  })();
}

Поскольку вы хотите взаимодействовать с дочерними элементами в родительской прокрутке, что не имеет ничего общего с состоянием, вы можете пропустить всю церемонию и получить доступ к дочерним элементам напрямую, воспользовавшись тем, что Solid компилируется в собственный 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);

Живая демонстрация: https://playground.solidjs.com/anonymous/19a789fe-d710-40ba-8b3e-a256ab23c1e2

Видите ли, вам не нужно все это извращенное мышление React.

Если ваш дочерний компонент каким-то образом зависит от состояния родителя, т.е.yposition, лучше всего передать родительское состояние как свойство и добавить событие внутри дочернего тела:

      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

Если состояние является объектом, но вам нужно только одно свойство, вы можете получить состояние:

      // interface State { x: number, y: number }
const y = () => state().y;

// Then pass it as a prop
<Child y={y} />

Ссылки предназначены для доступа к элементу внутри тела компонента для таких вещей, как добавление и удаление прослушивателей событий. Не раздавайте их, если можете, и уж точно не злоупотребляйте ими. Поскольку state+effects обеспечивает предсказуемость, ссылки приносят хаос.

Другие вопросы по тегам