Разбираемся с ref’ами, которые позволяют добираться до конкретных DOM-нод и производить с ними различные манипуляции.
GitHub-репозиторий с исходниками: http://bit.ly/gh_course_react_beginner
Давайте рассмотрим так называемые ref’ы. На самом деле это просто сокращение от английского reference — ссылка.В сущности они просто позволяют получить доступ к тем нодам внутри компонента, которые были созданы в результате выполнения метода render().
Это бывает нужно по разным причинам:- при работе с API для выделения текста или с фокусом текстовых полей,- произведения каких-то анимаций, перемещений и изменений внешнего вида путем прямого взаимодействии с нодой (хотя рекомендуется маскимально этого не делать), замеров размеров DOM-нод,- при работе с внешними библиотеками — скажем с jQuery (хотя уж его точно не стоит использовать в современных React-приложениях).
Поначалу новички, особенно те, кто перешел с jQuery на React могут злоупотреблять использованием рефов, однако со временем все становится на свои места.
Итак, исторически ref’ы в React прошли эволюционный путь, состоящий из трех этапов.
Ref как строка
Сначала ref’ы задавались строкой. Сразу замечу, что этот синтаксис устаревший, так что никогда его не используйте в современных приложениях. А выглядело это так:
class App extends React.Component {
state = { value: '' }
handleSubmit = e => {
e.preventDefault()
this.setState(_ => ({ value: this.refs.textInput.value }))
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<input type='text' ref='textInput' />
<button>Submit</button>
{this.state.value && <h3>Вы отправили: {this.state.value}</h3>}
</form>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'))
Здесь у целевой ноды, к которой мы хотим обратиться, стоит специальный атрибут ref и его значение — строка. А эта строка фигурирует и вот здесь — в методе handleSubmit, срабатывающем когда мы отправляем форму. То есть this.refs это специальное поле в нашем инстансе компонента, которое хранит все подобные рефы, которые определены внутри этого компонента. Оттуда к ним доступ и осуществляется.
Ref как callback
Следующим этапом стала передача функции в атрибут ref.
class FocusedTextInput extends React.Component {
constructor(props) {
super(props)
this.setTextInputRef = element => this.textInput = element
}
focusTextInput = _ => {
if (this.textInput) this.textInput.focus()
}
componentDidMount() {
this.focusTextInput()
}
render() {
return (
<div>
<input type='text' ref={this.setTextInputRef} />
<input type='button' value='Focus' onClick={this.focusTextInput} />
</div>
)
}
}
ReactDOM.render(<FocusedTextInput />, document.getElementById('root'))
Здесь в ref передается уже не строка, а колбэк, который после своего выполнения (после первого рендера), в this.textInput сохраняет ссылку на нативную браузерную DOM-ноду, к которой в componentDidMount мы и обращаемся вызывая метод focus. Это, после того как компонент замаунтился, заставляет его поймать фокус.
Вы можете быть уверены, что ДО выполнения методов жизненного цикла componentDidMount и componentDidUpdate, ref’ы уже будут установлены.
Ref как вызов createRef
Ну и наконец самым современным и рекомендуемым способом является вызов в конструкторе метода React.createRef():
class App extends React.Component {
constructor(props) {
super(props)
this.inputRef = React.createRef()
}
componentDidMount() {
console.log(this.inputRef.current.value)
}
render() {
return <input type='text' ref={this.inputRef} value='testVal' />
}
}
ReactDOM.render(<App />, document.getElementById('root'))
Тут вообще все просто. Вызов обозначенной выше функции создает в this.inputRef структуру данных, которая пердназначена для хранения ref’а. А в render в ref передается ссылка на эту структуру в классе компонента. И вот, ref уже готов к использованию. Пишем this.inputRef.current и получаем ссылку на DOM-ноду.
Видно, что в консоли после выполнения componentDidMount появилось содержимое инпута.
Еще момент — мы можем в дочерний компонент передать ref как один из пропсов, который будет называться иначе чем ref, и затем уже в дочернем установить его на конкретный элемент, чтобы получить из родителя доступ к ноде дочернего элемента.
class Parent extends React.Component {
constructor(props) {
super(props)
this.setTextInputRef = element => this.textInput = element
}
focusTextInput = _ => {
if (this.textInput) this.textInput.focus()
}
componentDidMount() {
this.focusTextInput()
}
render() {
return (
<div>
<CustomTextInput setTextInputRef={this.setTextInputRef} />
<input type='button' value='Focus' onClick={this.focusTextInput} />
</div>
)
}
}
class CustomTextInput extends React.Component {
render() {
return <input
style={{ backgroundColor: '#0d4' }}
type='text' ref={this.props.setTextInputRef}
/>
}
}
ReactDOM.render(<Parent />, document.getElementById('root'))
Данный код демонстрирует, как колбэк, устанавливающий в Parent ссылку на DOM-ноду мы можем отправить в дочерний компонент. Это происходит вот здесь, в пропсе setTextInputRef. Этот колбэк мы отправляем в пропс ref компонента CustomTextInput и так как этот колбэк объявлен в родительском компоненте, то и ссылка на DOM-ноду установится там же. Соответственно, после componentDidMount инпут нормально зафокусится. Как и в примере выше.
Фактически, мы сейчас кастомизировали инпут и заставили его работать так, будто бы он находится прямо в компоненте Parent.
А вот еще более интересный пример:
class Parent extends React.Component {
constructor(props) {
super(props)
this.inputRef = React.createRef()
}
focusInput = () => this.inputRef.current.focusInput()
render() {
return (
<div>
<CustomTextInput ref={this.inputRef} />
<input type='button' value='Focus' onClick={this.focusInput} />
</div>
)
}
}
class CustomTextInput extends React.Component {
constructor(props) {
super(props)
this.inputRef = React.createRef()
}
focusInput = () => this.inputRef.current.focus()
render() {
return <input type='text' ref={this.inputRef} />
}
}
ReactDOM.render(<Parent />, document.getElementById('root'))
Тут мы пропс ref пишем прямо у компонента, а не у известного браузеру HTML-элемента. И это работает иначе.
Компонент Parent уже рассмотренным ранее способом готовит в конструкторе поле для ref’а, затем в рендере ссылка создается. И если посмотреть, то она будет указывать на React-компонент, а не на DOM-ноду как в предыдущих случаях. Вот это и есть преимущество, так как получив такую ссылку, мы можем у CustomTextInput вызывать его методы. По нажатию на кнопку фокуса такой метод focusInput у CustomTextInput как раз и вызывается из Parent. А CustomTextInput уже внутри себя имеет ref на конкретный HTML-элемент инпут и может через this.inputRef.current вызвать нативный браузерный метод focus. И вот, наша задача достигнута.
Замечу, что такое не пройдет с компонентами, объявленными как функции, в отличие от тех, что объявлены в качестве класса. Такое просто-напросто не будет работать. Ведь нет инстанса и не у кого вызывать метод.
Но если же компонент у вас все же объявлен как функция, в него можно пробросить другую функцию, устанавливающую ref в классе родителя и работать с этим так же. Это было уже рассмотрено ранее.
class Parent extends React.Component {
constructor(props) {
super(props)
this.setTextInputRef = element => this.textInput = element
}
focusTextInput = _ => {
if (this.textInput) this.textInput.focus()
}
componentDidMount() {
this.focusTextInput()
}
render() {
return (
<div>
<CustomTextInput setTextInputRef={this.setTextInputRef} />
<input type='button' value='Focus' onClick={this.focusTextInput} />
</div>
)
}
}
function CustomTextInput(props) {
return (
<input
style={{ backgroundColor: '#0d4' }}
type='text' ref={props.setTextInputRef}
/>
)
}
ReactDOM.render(<Parent />, document.getElementById('root'))
Вообще же, в компоненте, объявленном как функция, можно использовать ref’ы:
function MyTextInput(props) {
const textInput = React.createRef()
const handleClick = _ => textInput.current.focus()
return (
<div>
<input type='text' ref={textInput} />
<button onClick={handleClick}>Focus</button>
</div>
)
}
ReactDOM.render(<MyTextInput />, document.getElementById('root'))
Вместо использования конструктора мы просто создаем переменные — с инициализацией структуры под ref и с обработчиком клика по кнопке.
Ну и давайте еще вспомним для полноты картины про метод ReactDOM.findDOMNode. Он получает в качестве аргумента React-компонент и позволяет покопаться в DOM-содержимом его инстанса (ну или экземпляра — напоминание для тех кто еще не запомнил, что это одно и то же).
class Parent extends React.Component {
constructor(props) {
super(props)
this.inputRef = React.createRef()
}
focusInput = () => {
// console.log(ReactDOM.findDOMNode(this.inputRef.current))
// ReactDOM.findDOMNode(this.inputRef.current).querySelector('.test-node').innerText = 'Changed'
this.inputRef.current.focusInput()
}
render() {
return (
<div>
<CustomTextInput ref={this.inputRef} />
<input type='button' value='Focus' onClick={this.focusInput} />
</div>
)
}
}
class CustomTextInput extends React.Component {
constructor(props) {
super(props)
this.inputRef = React.createRef()
}
focusInput = () => this.inputRef.current.focus()
render() {
return (
<div>
<input type='text' ref={this.inputRef} />
<div className='test-node' />
</div>
)
}
}
ReactDOM.render(<Parent />, document.getElementById('root'))
Я модифицировал предыдущий пример и добавил в обработчик клика кнопки Focus вывод в консоль результата работы ReactDOM.findDOMNode, которой я передал ссылку на наш компонент CustomTextInput, а точнее на его инстанс — не забываем про current в конце — и это дает нам DOM-ноду, с которой можно делать все, что позволено в браузере делать с DOM-нодой.
Например, давайте поменяем её текст. Вот так — взяли её инстанс, поискали по классу с помощью querySelector и поменяли innerText. Результат вы видите на экране.
Но правда лучше использовать эту штуку в самом крайнем случае, когда ну ничего уже не помогает. В специальном ограниченном режиме Strict Mode реакта, это работать перестанет. Про Strict мы еще поговорим.
Теперь про так называемый Ref Forwarding. О чем идет речь? Рассмотрим такую кнопку:
function CustomBtn(props) {
return (
<button className="CustomBtn">
{props.children}
</button>
)
}
ReactDOM.render(
<CustomBtn>
<strong>SEND</strong>
</CustomBtn>,
document.getElementById('root')
)
Рисуется обычный button и то, что внутри него, соответственно также отрисуется благодаря props.children — нам уже это известно. По-хорошему, один компонент не должен вмешиваться в разметку другого и менять стили или тем более структуру его дерева элементов. Но это больше справедливо для высокоуровневых компонентов вроде комментариев или календаря какого-нибудь. Для простых же компонентов вроде стилизованной кнопки или инпута, это удобно, чтобы изменить фокус, допустим. Или как-то анимировать. Или выделить текст.
И вот тут-то Forwarding Ref приходятся как нельзя кстати. Да, по-русски это можно перевести как «пробрасываемые рефы».
const CustomBtn = React.forwardRef((props, ref) => (
<button ref={ref} className='CustomBtn'>
{props.children}
</button>
))
class App extends React.Component {
constructor(props) {
super(props)
this.ref = React.createRef()
}
componentDidMount() {
this.ref.current.focus()
}
render() {
return (
<CustomBtn ref={this.ref}>
<strong>SEND</strong>
</CustomBtn>
)
}
}
ReactDOM.render(
<App />,
document.getElementById('root')
)
Данный пример наглядно демонстрирует особенности пробрасываемых ref’ов. С помощью метода React.forwardRef мы создаем компонент нашей кнопки и в него пробрасываются не только пропсы, а еще и ref, который для нас эта обертка и создала. Далее этот реф пробрасывается в button.
Затем, когда мы используем в другом компоненте нашу кастомную кнопку, то пробрасываем ей ref, который создаем уже привычным способом. При маунтинге компонента App, наша кнопка прекрасно получает фокус. Все работает.
Эта техника хорошо применима в компонентах высшего порядка (higher-order components) — я их называю просто и удобно — ХОК — от соответствующего акронима.
Сейчас будет, возможно, немного запутанный для вас пример. Необходимо в него вникнуть хорошо, если хотите все до конца понять.
index.js:
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
ReactDOM.render(
<App />,
document.getElementById('root')
)
App.js:
import React from 'react'
import CustomBtn from './CustomBtn'
class App extends React.Component {
constructor(props) {
super(props)
this.ref = React.createRef()
}
componentDidMount() {
console.log(this.ref.current)
setTimeout(() => {
this.ref.current.changeSendingStatus()
}, 2000)
}
render() {
return (
<div className='wrapper'>
<CustomBtn ref={this.ref}>
SENDING...
</CustomBtn>
</div>
)
}
}
export default App
LogProps.js (неработающий вариант):
import React from 'react'
function logProps(WrappedComponent) {
class LogProps extends React.Component {
componentDidMount(prevProps) {
console.log('old props:', prevProps)
console.log('new props:', this.props)
}
render() {
return <WrappedComponent {...this.props} />
}
}
return LogProps
}
export default logProps
LogProps.js (работающий вариант):
import React from 'react'
function logProps(WrappedComponent) {
class LogProps extends React.Component {
componentDidMount(prevProps) {
console.log('old props:', prevProps)
console.log('new props:', this.props)
}
render() {
const {forwardedRef, ...rest} = this.props
return <WrappedComponent ref={forwardedRef} {...rest} />
}
}
return React.forwardRef((props, ref) => {
return <LogProps {...props} forwardedRef={ref} />
})
}
export default logProps
CustomBtn.js:
import React from 'react'
import logProps from './LogProps'
class CustomBtn extends React.Component {
constructor(props) {
super(props)
this.ref = React.createRef()
}
changeSendingStatus = _ => this.ref.current.innerText = 'SENT'
render() {
return (
<button className='custom-btn' ref={this.ref}>
{this.props.children}
</button>
)
}
}
export default logProps(CustomBtn)
Итак..
В index.js просто рисуется компонент App.
App рисует CustomBtn с надписью SENDING внутри, благодаря props.children. Тут же, создается ref и отправляется в пропс ref компонента CustomBtn. Как только App маунтится, он через этот реф у CustomBtn вызывает метод changeSendingStatus.
Дальше самое интересное. HOC-компонент LogProps. Его назначение, выводить пропсы любого компонента, который им обернут. В нашем случае CustomBtn как раз имеет обертку в виде logProps. И мы для начала посмотрим на неработающий вариант и поймем почему он не работает.
Тут у нас объяалена функция logProps с маленькой l. В нее пробрасывается тот, компонент, который нужно обернуть. Внутри объявляется класс LogProps с большой L. Он рендерит WrappedComponent, а это в нашем случае будет CustomBtn, и пробрасывает в него все пропсы, которые были проброшены этажом выше. Как только данный компонент-обертка маунтится, он все пропсы выводит — как предыдущие, так и текущие. И компонент LogProps возвращается из функции. Почему — сейчас поймем.
Оборачиваемый компонент CustomBtn. Вот тут, в экспорте, мы нашу кнопку оборачиваем заимпорченным logProps.
Смотрим еще раз на цепочку. При экспорта выполняется функция logProps, получая аргументом наш CustomBtn. Тут он уже называется WrappedComponent и возвращается из компонента LogProps, а уже компонент LogProps, завязанный на WrappedComponent, возвращается из функции logProps. И попадает в экспорт из CustomBtn. Вот так и получается что отрисованный в App.js CustomBtn уже обернут в logProps.
Но скользкий момент в том, что несмотря на то, что мы передали в CustomBtn ref, он передается в компонент обертку и не пробрасывается дальше, в сам CustomBtn, так как расспредивание this.props не затрагивает специальный реактовский пропс ref и он в этот список просто не включается. При попытке запустить код в этом случае, мы увидим, что ref в App.js будет указывать на LogProps. В этом-то и загвоздка.
И здесь на помощь приходит как раз ref forwarding. Вот измененный код logProps. Вот тут, вместо того, чтобы просто возвратить компонент LogProps, мы возвращаем результат выполения React-метода forwardRef, который получает аргументом колбэк. Туда пробрасываются пропсы и ref, тот самый, который ранее нам был недоступен. Пропсы деструктурируются отдельно, а ref отправляется в отдельный пропс forwardedRef. И тогда внутри LogProps в методе render, мы можем из его уже пропсов извлечь отдельно forwardedRef и остальные пропсы. forwardedRef попадает в ref, а остальные пропсы деструктурируются отдельно.
И вот теперь явно видно, что в App.js ref указывает на CustomBtn. Внутри него создается свой ref, который указывает на button и метод changeSendingStatus, где меняется текст кнопки на SENT. Кстати, плохой пример, потому что по идее текст внутри зависит от this.props.children, а тут прямое вмешательство с перезаписью этого текста. В общем, вы поняли как не надо делать =)
Кстати, смотрите как выглядит React-дерево для этого примера. Вся наша структура дополнительно обернулась еще и в ForwardRef. Мы можем вместо стрелочной безымянной функции пробросить сюда функцию с именем:
import React from 'react'
function logProps(WrappedComponent) {
class LogProps extends React.Component {
componentDidMount(prevProps) {
console.log('old props:', prevProps)
console.log('new props:', this.props)
}
render() {
const {forwardedRef, ...rest} = this.props
return <WrappedComponent ref={forwardedRef} {...rest} />
}
}
return React.forwardRef(function CustomBtn (props, ref) {
return <LogProps {...props} forwardedRef={ref} />
})
}
export default logProps
Тогда в скобочках у ForwardRef это имя появится. Удобно для отладки.
А можно сделать еще круче:
import React from 'react'
function logProps(WrappedComponent) {
class LogProps extends React.Component {
componentDidMount(prevProps) {
console.log('old props:', prevProps)
console.log('new props:', this.props)
}
render() {
const {forwardedRef, ...rest} = this.props
return <WrappedComponent ref={forwardedRef} {...rest} />
}
}
function forwardRef (props, ref) {
return <LogProps {...props} forwardedRef={ref} />
}
const name = WrappedComponent.displayName || WrappedComponent.name
forwardRef.displayName = `logProps(${name})`
return React.forwardRef(forwardRef)
}
export default logProps
Выносим данную функцию отдельно, назвав, например, forwardRef. Затем формируем имя оборачиваемого компонента в переменной name. Для этого берем у него значение displayName или name, если первого нет. дальше приписываем logProps и в скобках интерполируем name. И наконец, вызываем React.forwardRef с «пропатченной» функцией forwardRef.
Что ж.. Надеюсь материал данного урока не поплавил ваш мозг. Если так, то пересмотрите это видео несколько раз с промежутком в несколько дней, попутно запуская самостоятельно, а лучше набирая ручками, все примеры из урока. Всем удачи и встретимся на следующем занятии по React’у. Будет еще интереснее.