Фрагменты-рэферэнсы ў React 19.3: Паведанне DOM для груп брацоў, без кэшувальнага обертака
Дазвольце дазнацься, як рэферэнс у Fragment дае вам FragmentInstance для слухачаў, фокусу, абзэрватароў і выканання замероў між сяброўскімі элементамі, а таксама якія ёсць яго меры.
Большасць кодавых базаў на React маюць элемент <div>, які існуе толькі для таго, каб ў чым-небудзь можна было размістыць ref. Можа, компонент, які вы хацелі запісаць, адрасав калькуляцыю колькісці своіх „братоў“-элементаў, альбо ён быў взяты з бібліятэкі, якая ніколі не перадавала свой ref. Такі дапаможны элемент тады нарабляе проблемы з сіткавым або гнучкым макетам, пашкоджае стайлізаванне за дапамою :nth-child або, у разе элементаў <tr>, стварае некарэктны HTML. У версіі React 19.3 Fragment Refs сталаўся стабільным: калі вы прыкрэпляеце ref да <Fragment>, вы отрымліваеце FragmentInstance, які можа выкарыстоўваць стандартныя операцыі з DOM, такія як аблекчэнне, фокусаванне, спагляданне, запіс калькуляцый і прасуванне для всіх дзецяў фрагмента адразу, не дадаючы нічога да вырасленага дрэва. У этай статыце рассказваеца пра API, практычныя падходы для кожнай з можнасцей, якія дзеці ўрабляюць кожны з методаў, а таксама пра падазры, якія трэба ведаць перш чым выклікваць код у рэальных умовах.
Паколькі гэтае функцыя ў момент напісання ўжо новая, пераканайцеся, што актуальныя даныя <Fragment> не маюць змян у назвах методаў чы робачым адносе.
API FragmentInstance
У звычайной наладзе элемент Fragment імпортуецца явна, адколькі рэферэнс існуюць толькі пасля монтавання:
import { Fragment, useRef, useEffect } from 'react';
Рэферэнс спачатку ў значэнні null, а становіцься FragmentInstance, калі фрагмент монтаваны. Наведзены выклек пазбягвае проблемам з null, дадзець слухача і адключыць яго пад час чысткі:
function Group({ children }) {
const ref = useRef(null); // becomes a FragmentInstance after mount
useEffect(() => {
const instance = ref.current;
if (instance === null) return;
instance.addEventListener('click', onClick);
return () => instance.removeEventListener('click', onClick);
}, []);
return <Fragment ref={ref}>{children}</Fragment>;
}
Патрэбны явны элемент <Fragment>. Скрацення <>…</> не прыме яні ref, яні key. Чым забезпечваецца інстанцыя:
addEventListener(type, fn, opts?)іremoveEventListener: працююць з дзецямі DOM першага рэвэлю, а зарэгістраваныя слухальнікі пераходзяць да дзецей, якія стваруюцца або удаляюцца пазней.dispatchEvent(event): адправляе званніцу на сам фрагмент; калі задаўся параметрbubbles: true, званніца распространяецца да роднага вузла DOM.focus(opts?)іfocusLast(opts?): шукае ўсіх вкладзеных наследнікаў па прынцыпу глубіны, каб знайсці першы або последні элемент, які можа быць уважаны актыўным, і прыймае значэнне{ preventScroll }.blur(): усунея актыўны элемент, які знаходзіцца ўнутрь фрагмента, а іншыя элементы не падвергаюцца нічым.
observeUsing(observer) і unobserveUsing(observer): рэгіструець дзецей першага рангу DOM з IntersectionObserver або ResizeObserver; тэкстовыя вузлы не ўключаюцца.getClientRects(): вяртае масэвы элемент DOMRect[] для дзецей першага рангу DOM.scrollIntoView(alignToTop?): прасуе першага дзеця ў відобразэнне, калі параметр true, або последняга — калі false; прыймае толькі логічную значэнне, а параметр options выклекае памылку.getRootNode() і compareDocumentPosition(node): апісваюць пазычэнне фрагмента за тымі ж правіламі, як і методы Node.Адны слухач для групы дзецей
Звычным сценарыем ёсць інструментальная палітра, якая адрасаваецца за дапамою компаненту, якім вы не кераваеце, і тады вам патрэбен аднаковы слухальнік нажатых клавіш для всіх яе кнопак, напрыклад, для аналізу дадзеных. У дэме імпортуецца:
import { Fragment, useRef, useEffect, useState } from 'react';
Toolbar — гэта компанент трэцьяго сторонньага выдавца: ён адрасаваець простыя кнопкі-браты без якога-лібо обгортка і без атрыбута ref, плюс неабяжна чатвертая кнопка. Tracked обгортае своіх дзецей у Fragment, які мае атрыбут ref, і прыўязвае обрабнавач onAny у рамках эфекту, практычна прыўязваючы яго занова, калі обрабнавач зміняецца. App фіксуе тэкст кожной нажатай кнопкі і прыладнае палочку падтверджэння, якая дазволяе дадаць ўсё новую кнопку:
// Imagine this comes from a library: no ref, no wrapper, just siblings.
function Toolbar({ extra }) {
return (
<>
<button>Bold</button>
<button>Italic</button>
<button>Link</button>
{extra && <button>Emoji</button>}
</>
);
}function Tracked({ children, onAny }) {
const ref = useRef(null);
useEffect(() => {
const instance = ref.current;
if (instance === null) return;
instance.addEventListener('click', onAny);
return () => instance.removeEventListener('click', onAny);
}, [onAny]);
return <Fragment ref={ref}>{children}</Fragment>;
}export default function App() {
const [log, setLog] = useState([]);
const [extra, setExtra] = useState(false);
return (
<>
<div className="bar">
<Tracked onAny={e => setLog(l => [e.target.textContent, ...l].slice(0, 6))}>
<Toolbar extra={extra} />
</Tracked>
</div>
<label><input type="checkbox" checked={extra} onChange={e => setExtra(e.target.checked)} /> add a fourth button</label>
<p className="log">clicked: {log.join(' · ') || '—'}</p>
</>
);
}
Стылі выключна задаюць распалох інструментальной палітры і лініі журналу:
.bar { display: flex; gap: 8px; margin-bottom: 10px; }
.log { margin: 10px 0 0; font-family: ui-monospace, monospace; opacity: .8; }
Увядзіце чэтвёрты кантакт і нажміце яго — ён таксама будзе зазначаны ў логу, як і іншыя. Экземпляр фрагмента сочыць за дзецячымі элементамі, калі React іх запускае і зупіняе, а таксама адкорэгуе своіх слухачоў, што ёсць самэ тыя функціі, якія інакш было бы трэба выкарыстоўваць самастаі. Зауважыце, што onAny у дэманстрацыі ўжо ёсць вбудованым функцыяйным элементам, таму эфект перэпісваецца на кожны раз, калі рэндаруецца App; у рэальным кодзе, калі такія змены маюць значэнне, стабілізавайце яго за дапамою useCallback.
Перанесенне фокуса праз вярстакаваны контэнт
Толькі focus() і focusLast() аналізуюць элементы за межамі першага рэвярсу. Яны праходзяць праз усіх наследнікаў у парадку глыбокага перасканавання, пакуль не знайдуць элемент, які можна надаць фокус, таму элемент уводу, які знаходзіцца ўнутрь элемента label унутрь fieldset, таксама ёсць доступным. Чыраўна «надаць фокус першаму полю ў гэтым раздэле» стае простым нават для маркапу, які не належыце вам.
У дэме імпортуецца толькі Fragment і useRef:
import { Fragment, useRef } from 'react';
Section атрыбутуе заголовак і тры кнопкі, якія прыўязаны да функцый focus(), focusLast() і blur() у экзанплярэ фрагмента, а пасля атрыбутуе яго дзецей унутрошчы Fragment, які быў пазначаны за дапамогай useRef. App перадае два наборы поль, якія маюць введчыкі з пазначкамі:
function Section({ title, children, controls }) {
const ref = useRef(null);
return (
<>
<div className="row">
<strong>{title}</strong>
<button onClick={() => ref.current.focus()}>First field</button>
<button onClick={() => ref.current.focusLast()}>Last field</button>
<button onClick={() => ref.current.blur()}>Blur</button>
</div>
<Fragment ref={ref}>{children}</Fragment>
</>
);
}export default function App() {
return (
<Section title="Shipping">
<fieldset>
<legend>Address</legend>
<label>Street <input name="street" /></label>{' '}
<label>City <input name="city" /></label>
</fieldset>
<fieldset>
<legend>Contact</legend>
<label>Phone <input name="phone" /></label>
</fieldset>
</Section>
);
}
Кольцо фокуса дапамагае побачыць рэзультат:
.row { display: flex; gap: 8px; align-items: center; margin-bottom: 10px; }
fieldset { border-radius: 8px; margin: 0 0 8px; }
input:focus { outline: 3px solid light-dark(#2563eb, #60a5fa); }
„Перша польна“ атрыбутуецца значэнням Street, хоць яна знаходзіцца ўнутрошчы элемента <label> у наборэ поль <fieldset>; „Пасляпэльная польна“ атрыбутуецца значэнням Phone. Глыбокая пераследаванне разлічна ад эвэнтаў і спостэрагачаў, якія зупыняюцца на першам роўні; раздзіл пра цялевыя пазначкі ніжэй пояснюе прычыны.
Следзіць за відазнаемасцю без нарабаткі пахамоў
observeUsing() прымае абзяральнік, які вы самі стварылі, таму вы залішаецеся пад кантролем над root, threshold і rootMargin, а экземпляр рэгіструе кожны дзецячы элемент першага рангу. У наступным прыкладзе найсвежэйшыя спісакі паведамленняў пазначаюцца як перакананыя, як толькі адна з іх стае відным у рамках прасуваннага чату. Элемент-обгортка навакола гэтых спісакоў могла бы нарабіць пахваленняя ў лейアウтэ флекс-кантэйнера, дзе кожны паведамлення вырашаецца самастойна.
У імпортах ёсць useLayoutEffect, таму абзяральнік рэгіструецца даўна перад адрасаваннем пікселей:
import { Fragment, useRef, useLayoutEffect, useState } from 'react';
ReadReceipt стварае IntersectionObserver, які працюе з роўнем скроллу, з порогам 0.6, вызывае onRead, калі які-небудзь элементы перакрываюцца, і змаўчвае за дапамогою unobserveUsing. App адрасавае чатырнацаць паведамленняў, а последнія тры знаходзяцца ў ReadReceipt:
function ReadReceipt({ root, onRead, children }) {
const ref = useRef(null);
useLayoutEffect(() => {
const instance = ref.current;
const io = new IntersectionObserver(
entries => { if (entries.some(e => e.isIntersecting)) onRead(); },
{ root: root.current, threshold: 0.6 }
);
instance.observeUsing(io);
return () => instance.unobserveUsing(io);
}, [root, onRead]);
return <Fragment ref={ref}>{children}</Fragment>;
}const MESSAGES = Array.from({ length: 14 }, (_, i) => `Message ${i + 1}`);export default function App() {
const pane = useRef(null);
const [read, setRead] = useState(false);
return (
<>
<p className="status">{read ? '✅ Priya has read your last messages' : 'scroll the pane to the bottom…'}</p>
<div className="pane" ref={pane}>
{MESSAGES.slice(0, 11).map(m => <div key={m} className="bubble">{m}</div>)}
<ReadReceipt root={pane} onRead={() => setRead(true)}>
{MESSAGES.slice(11).map(m => <div key={m} className="bubble mine">{m}</div>)}
</ReadReceipt>
</div>
</>
);
}
CSS ператварае роўень у скроллаваны стовпчак тыпу flex і выравнюе власныя паведамлення корыстувальца праваю сторонай:
.status { margin: 0 0 8px; font-weight: 600; }
.pane { height: 190px; overflow: auto; display: flex; flex-direction: column; gap: 6px; padding: 8px; border-radius: 10px; background: light-dark(#f1f5f9, #262a33); }
.bubble { align-self: flex-start; padding: 6px 10px; border-radius: 12px; background: light-dark(#fff, #363b47); }
.mine { align-self: flex-end; background: light-dark(#dbeafe, #1e3a5f); }
ResizeObserver працуе аднойчы. Калі вы керуеце большам чысла груп, якія выкарыстоўваюць аднаковыя настройкі назірання, вам не патрэбны спостэрэльнікі для кожной групы: кожны первасортны дзеціні элемент несе атрыбут reactFragments, які мае Set з екземплярамі фрагментаў, да якіх ён належыць, што дазволяе аднаму спостэрэльніку знаходзіць відпаведную групу для кожнага элемента і вызываць правы працоўнік.
Зьвяржоўка групы брацоў і сестэр
getClientRects() вяртае адны прарактанг у кожнам дзецінскім элеменце першага рангу. Іх сумаванне дае рамку, якая абграўляе всю групу, і яю можна выкарыстаць для падсвечвання выбранага элемента або стварэння рамкі навакол калькulaцыйных поль, не паўтараючы структуру лейауту, у якім знаходзяцца эты элементы.
У дэманстрацыі імпортуецца:
import { Fragment, useRef, useState, useLayoutEffect } from 'react';
Outline чытае прарактангі ў эфекце лейауту, калі гэты эфект становіцца актыўным, вычысляе мінімальныя значэння для верхней і левай граней, а таксама максімальныя для ніжней і правай граней, дадае зсувы прасування і невялікі запас, а потым зберагае рэзультат. Пасля чого ён адрасавае абсалютна распакованую рамку пры самай фрагментацыі. App размешчае две групы по трох элементаў у аднай сетцы і дазволяе выбраць, калкulaцыйныя поля трэба абграўляць:
function Outline({ active, children }) {
const ref = useRef(null);
const [box, setBox] = useState(null);
useLayoutEffect(() => {
if (!active) return setBox(null);
const rects = ref.current.getClientRects();
const top = Math.min(...rects.map(r => r.top)), left = Math.min(...rects.map(r => r.left));
const bottom = Math.max(...rects.map(r => r.bottom)), right = Math.max(...rects.map(r => r.right));
setBox({ top: top + scrollY - 6, left: left + scrollX - 6, width: right - left + 12, height: bottom - top + 12 });
}, [active]);
return (
<>
<Fragment ref={ref}>{children}</Fragment>
{box && <div className="outline" style={box} />}
</>
);
}export default function App() {
const [which, setWhich] = useState('none');
return (
<>
<div className="row">
{['none', 'address', 'card'].map(w => (
<label key={w}><input type="radio" checked={which === w} onChange={() => setWhich(w)} /> {w}</label>
))}
</div>
<div className="grid">
<Outline active={which === 'address'}>
<input placeholder="Street" /><input placeholder="City" /><input placeholder="PIN" />
</Outline>
<Outline active={which === 'card'}>
<input placeholder="Card number" /><input placeholder="MM/YY" /><input placeholder="CVV" />
</Outline>
</div>
</>
);
}
Сетка складаецца з трох столбцоў, а контур размешчаны абсалютна з параметрам pointer-events: none, таму ён ніколі не перэхопляе клікі:
.row { display: flex; gap: 14px; margin-bottom: 10px; }
.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.grid input { min-width: 0; }
.outline { position: absolute; pointer-events: none; border: 2px dashed light-dark(#dc2626, #f87171); border-radius: 8px; }
Кожны элемент уведзення ў даным разе яўляецца безпосерэднім дзецячым элементам той самай CSS-сеткі. Якбы кожную групу пакаваць у <div>, тады ўсе трое елементаў уведзення былі б запакаваныя ў адну клетку; за дапамогою фрагмента сетка застаецца незменнай, і кожную групу все ўсё можна ўзяць пад расчыт. Аднай з абмежэнняў дамаўкі ёсць тое, што розмеры кантэйнера вычысляюцца толькі колі змяніўся статус active, таму падзеяўкі вікна або рух кантэйнера з прасуванням заставяюць розмеры старымі. Яго можна выправіць, супаравняўшы з ResizeObserver за дапамогою методу observeUsing.
Калькі дзецячыя элементы прымае кожны метод
Большасць методаў працуюць з тым, што React называе дзецьмі хоста першага рангу, тыято з элементамі DOM, да якіх можна дасягнуць, спускаючыся праз компоненты React пакуль не будзе знайдзены рэальны элемент. Элементы, якія знаходзяцца ўнутрь іх, не являюцца прымарнымі цілямі.
<Fragment ref={ref}>
<div id="A" />
<Wrapper> {/* a component, not a DOM node: looked through */}
<div id="B">
<div id="C" /> {/* inside a DOM node: NOT targeted */}
</div>
</Wrapper>
<div id="D" />
</Fragment>
// addEventListener / observeUsing / getClientRects → A, B, D
// focus / focusLast → search A, B, C, D depth-first
У гэтым дрэве Wrapper ёсць компонент, таму React аналізуе яго, і B лячыцца элементам першага рангу. C знаходзіцца ўнутрь элемента DOM B, таму яны не ўважаюцца прымарнымі цілямі. Таму слухачы, абзэры і праекты прыкладаны да A, B і D, тады калі focus і focusLast шукаюць у A, B, C і D у парадку глыбокага спуску.
Разлік ўмышлены. Адтакуль, калі эвэнты распространяюцца па прынцыпу „баблінг“, клікі ўнутры C ужо доходзяць да аберанта ў B, а касета і спостэрэння B ужо абягаюць C. У фокусе нэт такога аналагу, і элемент, які можа прымець фокус, можа знаходзіцца на будзь-яй глыбіні, таму гэтыя два методы вынужданы шукаць.
Альтэрнатывныя способы
До появы Fragment Refs існавалі тры распашчытныя падходы, кожны з якіх меў сваю цэну:
- Элемент-обгортка,
<div ref={ref}><Thing /></div>: ён дадае новы элемент flex або grid там, дзе раней быў рад братоўскіх элементаў, нарушае селектары:first-childі:nth-child, дадае касету для макетавання, і ёсць абсалютна недзейны між радкамі таблыцы або элементамі спісу.
innerRef: гэта працюе для вашага сабскоду, але немагчыма для пакета, і ўсё рав дае толькі адзін вузол.parent.querySelectorAll('button') унутры эфекту: гэта не працюе, калі дзецявы элементы зміняюцца пасля запуску эфекту, і застаецца стары набор слухачаў.Чаму гэта корыстна
За дапамою Fragment Refs можна дадзіць додатковыя функцыі к маркапу трэціх сторон, заставаючы гэты маркап абсалютна такім, які ён. Гэта спрыяе ствароўцы семіі складовых, якія маюць толькі функцыі: <Tracked>, <InView>, <FocusScope>, <Outline>. Кожная з іх — гэта фрагмент плюс эфект, які працюе з будзь-якімі дзецямі элементаў і нічога не дадае да DOM. Найбольшыя выгоды отрымаюць стваральнікі сістэм дызайну; складовая Menu можа кераваць фокусам між элементамі, якія відрасцелі ў корыстувача, не нав’язваючы ўпорадкування гэтым элементам. У хроніцы змян React гэта опісвае як композабельная платформеная функцыя, што ёсць хорашым ментальным мадэляваннем: фрагмент — гэта месца, дзе знаходзіцца функцыя, а дзеці можу быць чым завгодна.
Проблемы, якія трэба пераканацца перад выкарыстоўванням
- Для ref неабходна довгая форма;
<>яе не прыйме.
null.scrollIntoView прыймае толькі логічную вялічыну. Перадача { behavior: 'smooth' } вызывае адхэнне. Чырагайце scrollIntoView(), каб першы элемент быў выравняны з верхам, або scrollIntoView(false), каб последні элемент быў выравняны з ніжней часткай.observeUsing прызначаны для элементаў; React павярамляе пра гэта пад час розработы.<Activity> слухачы, даданыя пад час сховання, прыўязуюцца толькі тады, калі паддрэво стане візуабельным.#36047): параметры захоплення не былі нормалізаваны так, як тагучаеся DOM, і гэта значыла, што рэўістрацыя з { capture: true } і анрэўістрацыя з простага true заставлялі аслухальнік застацца прыўязаным. Бяжучы на стабільную версію, пераканайцеся, што вы корыстаецеся аслухальнікамі фазы захоплення.focus() залежыць ад таго, каб вікно мела фокус у операцыйной сістэме. React падтверджае, што элемент прыме фокус, чакаючы на свой адбудовы focus, тады як Chrome прахоўвае гэты адбудовы, калі вікно браузера не ў стане актыўнасці. У тэстах без вікна або на фоне было зафіксавана, што focusLast() пераходзіць да элемента Phone і заставляе фокус на Street, таму пераканайцеся, што тэставая стороніца дзейсна мае фокус, перш чым пераканацца, дзе ён знаходзіцца.FragmentInstance не ўтварае элемента, таму такі атрыбуты як classList, style і innerHTML на ям не існуе; для всьго, што ўпэўненася ў іх наявнасці, паказваецца неабходнасць вікорыстоўваць справжній DOM-вузел.Альтэрнатыва для React 19.2 і ранэйшых версіяў
У разе вікорыстоўвання слухача, найболей разумным заменнікам ёсць дэлегаванне запуску адбування падзеяў з боку роднічага элемента. Паколькі функцыя апрацоўкі падзеяў знаходзіцца на роднічым элеменце і пераказвае e.target.closest('button'), дзеціны элементы можу з’являцца і знікаць без неабходнасці паўтарнага прыўязвання.
function Tracked({ children, onAny }) {
const parent = useRef(null);
useEffect(() => {
const el = parent.current;
const handler = (e) => { if (e.target.closest('button')) onAny(e); };
el.addEventListener('click', handler);
return () => el.removeEventListener('click', handler);
}, [onAny]);
return <div ref={parent} style={{ display: 'contents' }}>{children}</div>;
}
Стаянне display: contents усунеяе сабе ўласны контэйнер, таму элементы, якія знаходзяцца ўнутры, адносуюцца да бацькіх элементаў тыпу flex і grid як прымусовыя елементы. Хоць гэты элемент застаецца самастоятным, яго нельга размешчаць унутры <table>; прылады для чытання экрана і падобныя інструменты іноды спрацоўваюць з яму некорэктна, а для вімеры чы ўважлівага адстэйкання да іншых элементаў все раве трэба вікорыстоўваць querySelectorAll. Гэта падходзіць для прыемнікаў сігналаў і керавання фокусам, але у віпадках спостэрэння і вімеры дасканальна патрэбны версія 19.3.
Ключовыя выводы
- Вікорыстоўванне
refдля явнага<Fragment>даеFragmentInstance, які аб’еднвае прыемнікі сігналаў, керавання фокусам, спостэрэнне, вімеру і прасуванне між сябрамі элементаў без дадатковых вузлаў DOM. - Прыемнікі сігналаў і прыстроі спостэрэння застаюцца сінхроннымі, калі элементы дадаюцца чы адключаюцца, таму не трэба ручнага ведання стану.
focus і focusLast шукаюць у всім паддрэве, таму ўжо focus не адбываецца па прынцыпу „баблінгу“.scrollIntoView, які працуе толькі з булевымі значэннямі, адсутнае назіранне толькі за тэкстам, фокус вакна пад час тэстаў і практыка выкарыстання capture-listener у версіі 19.3.Пэўны спіс усіх методаў і асаблівасцяў можна пазнакаміць у афіцыйной даследчай документацыі Fragment; супаўзяныя прыгласы да змены знаходзяцца ў хроніцы змян праекта.