Перейти к содержимому

14. Refs и работа с DOM

Яша, в React ты привык к useRef — создаёшь объект { current: null }, вешаешь на JSX, и React заполняет .current после монтирования. В Solid.js всё проще и честнее: ref — это просто присваивание переменной. Никаких обёрток, никаких .current. Давай разберёмся! 🎯


В Solid ref работает как обычная переменная — компилятор JSX превращает ref={el} в присваивание el = element при монтировании.

import { onMount } from 'solid-js';
function App() {
let inputEl: HTMLInputElement; // просто переменная, не ref-объект!
onMount(() => {
inputEl.focus(); // уже доступен
console.log(inputEl.tagName); // "INPUT"
});
return <input ref={inputEl!} placeholder="Автофокус" />;
}

⚠️ Важно: inputEl будет undefined до монтирования. TypeScript об этом предупредит — поэтому ref={inputEl!} с !. Используй onMount или createEffect для работы с ним.


Это мощная фича Solid — можно передать setter сигнала как ref. Элемент будет записан прямо в сигнал:

import { createSignal, createEffect } from 'solid-js';
function MeasuredBox() {
const [el, setEl] = createSignal<HTMLDivElement | null>(null);
const [size, setSize] = createSignal({ width: 0, height: 0 });
// Реактивно реагируем на появление элемента
createEffect(() => {
const element = el();
if (element) {
const rect = element.getBoundingClientRect();
setSize({ width: rect.width, height: rect.height });
}
});
return (
<div>
{/* setter сигнала — прямо в ref! */}
<div ref={setEl} style={{ padding: '20px', background: '#1e293b' }}>
Измеряемый блок
</div>
<p>Размер: {size().width}×{size().height}px</p>
</div>
);
}

Это работает потому что Solid под капотом вызывает ref(element) — а setEl как раз функция-сеттер!


Помимо переменной и сигнала, можно использовать функцию-колбэк — она вызовется сразу при монтировании:

function App() {
function setupCanvas(canvas: HTMLCanvasElement) {
const ctx = canvas.getContext('2d')!;
ctx.fillStyle = '#2c67d5';
ctx.fillRect(10, 10, 100, 100);
ctx.strokeStyle = '#34d399';
ctx.strokeRect(20, 20, 80, 80);
}
return (
<canvas
ref={setupCanvas}
width={200}
height={150}
/>
);
}

💡 Колбэк получает DOM-элемент как первый аргумент. Отлично подходит для инициализации canvas, интеграции с D3.js, Three.js и другими библиотеками.


Чтобы получить ref из дочернего компонента, используй props.ref + ref={props.ref}:

import { JSX } from 'solid-js';
interface FancyInputProps {
ref?: HTMLInputElement | ((el: HTMLInputElement) => void);
placeholder?: string;
}
function FancyInput(props: FancyInputProps) {
return (
<input
ref={props.ref} // пробрасываем ref вниз
class="fancy-input"
placeholder={props.placeholder}
/>
);
}
function Parent() {
let inputRef: HTMLInputElement;
return (
<div>
<FancyInput
ref={inputRef!}
placeholder="Фокус из родителя"
/>
<button onClick={() => inputRef.focus()}>
Фокус!
</button>
</div>
);
}

🎭 Интеграция со сторонними библиотеками

Заголовок раздела «🎭 Интеграция со сторонними библиотеками»

Один из главных кейсов для ref — подключение DOM-библиотек. Вот пример с Chart.js:

import { onMount, onCleanup } from 'solid-js';
function ChartComponent() {
let canvasRef: HTMLCanvasElement;
let chartInstance: any;
onMount(() => {
// Импортируем Chart.js только на клиенте
import('chart.js/auto').then(({ Chart }) => {
chartInstance = new Chart(canvasRef, {
type: 'line',
data: {
labels: ['Янв', 'Фев', 'Мар', 'Апр', 'Май'],
datasets: [{
label: 'Продажи',
data: [12, 19, 3, 5, 2],
borderColor: '#2c67d5',
}]
}
});
});
});
onCleanup(() => {
chartInstance?.destroy(); // очищаем при размонтировании
});
return <canvas ref={canvasRef!} />;
}

// ❌ ОШИБКА: обращение к ref вне onMount
function Bad() {
let el: HTMLDivElement;
console.log(el!.offsetHeight); // ошибка! el ещё undefined
return <div ref={el!}>Текст</div>;
}
// ✅ ПРАВИЛЬНО: внутри onMount
function Good() {
let el: HTMLDivElement;
onMount(() => {
console.log(el.offsetHeight); // теперь доступен
});
return <div ref={el!}>Текст</div>;
}
// ❌ ОШИБКА: React-стиль с useRef (не нужен в Solid!)
function AlsoWrong() {
const ref = { current: null }; // лишняя обёртка!
return <div ref={ref}>...</div>; // ref.current не будет заполнен!
}

function App() {
let el: HTMLElement;
const [visible, setVisible] = createSignal(true);
// onMount — один раз при монтировании
onMount(() => {
el.style.opacity = '1';
});
// createEffect — реактивно, при изменении зависимостей
createEffect(() => {
if (visible()) {
el.classList.add('active');
} else {
el.classList.remove('active');
}
});
return (
<div>
<div ref={el!} class="box">Содержимое</div>
<button onClick={() => setVisible(v => !v)}>
Переключить
</button>
</div>
);
}

onMount — это просто createEffect с отложенным запуском (после рендера). Используй onMount для инициализации, createEffect — для реактивных изменений DOM.