#javascript #reactjs #react-ref
#javascript #reactjs #react-ref
Вопрос:
Обычно с помощью props мы можем написать
componentDidUpdate(oldProps) {
if (oldProps.foo !== this.props.foo) {
console.log('foo prop changed')
}
}
чтобы обнаружить изменения prop.
Но если мы используем React.createRef()
, как мы можем определить, когда ссылка изменилась на новый компонент или элемент DOM? В документах React на самом деле ничего не упоминается.
Например.,
class Foo extends React.Component {
someRef = React.createRef()
componentDidUpdate(oldProps) {
const refChanged = /* What do we put here? */
if (refChanged) {
console.log('new ref value:', this.someRef.current)
}
}
render() {
// ...
}
}
Предполагается ли, что мы сами должны реализовать какую-то вещь со старым значением?
Например.,
class Foo extends React.Component {
someRef = React.createRef()
oldRef = {}
componentDidMount() {
this.oldRef.current = this.someRef.current
}
componentDidUpdate(oldProps) {
const refChanged = this.oldRef.current !== this.someRef.current
if (refChanged) {
console.log('new ref value:', this.someRef.current)
this.oldRef.current = this.someRef.current
}
}
render() {
// ...
}
}
Это то, что мы должны делать? Я бы подумал, что React включил бы для этого какую-нибудь простую функцию.
Комментарии:
1. В некоторых случаях вам может сойти с рук просто
useLayoutEffect
убедиться, что ref не равен null.2. @grabantot Я вижу,
useLayoutEffect
после того, как React обновил DOM, и поэтому все ссылки должны были быть изменены в этот момент. Хороший совет. Я думаю, что это достойно того, чтобы быть его собственным ответом!
Ответ №1:
Документы React рекомендуют использовать ссылки обратного вызова для обнаружения ref
изменений значения.
Перехваты
export function Comp() {
const onRefChange = useCallback(node => {
if (node === null) {
// DOM node referenced by ref has been unmounted
} else {
// DOM node referenced by ref has changed and exists
}
}, []); // adjust deps
return <h1 ref={onRefChange}>Hey</h1>;
}
useCallback
используется для предотвращения двойного вызова обратного вызова ref с null
и элементом.
Вы можете инициировать повторный рендеринг при изменении, сохранив текущий узел DOM с useState
:
const [domNode, setDomNode] = useState(null);
const onRefChange = useCallback(node => {
setDomNode(node); // trigger re-render on changes
// ...
}, []);
Компонент класса
export class FooClass extends React.Component {
state = { ref: null, ... };
onRefChange = node => {
// same as Hooks example, re-render on changes
this.setState({ ref: node });
};
render() {
return <h1 ref={this.onRefChange}>Hey</h1>;
}
}
Примечание: useRef
не уведомляет об ref
изменениях. Также не повезло с React.createRef()
ссылками / object.
Вот тестовый пример, который удаляет и повторно добавляет узел при запуске onRefChange
обратного вызова :
const Foo = () => {
const [ref, setRef] = useState(null);
const [removed, remove] = useState(false);
useEffect(() => {
setTimeout(() => remove(true), 3000); // drop after 3 sec
setTimeout(() => remove(false), 5000); // ... and mount it again
}, []);
const onRefChange = useCallback(node => {
console.log("ref changed to:", node);
setRef(node); // or change other state to re-render
}, []);
return !removed amp;amp; <h3 ref={onRefChange}>Hello, world</h3>;
}
ReactDOM.render(<Foo />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.1/umd/react.production.min.js" integrity="sha256-vMEjoeSlzpWvres5mDlxmSKxx6jAmDNY4zCt712YCI0=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.1/umd/react-dom.production.min.js" integrity="sha256-QQt6MpTdAD0DiPLhqhzVyPs1flIdstR4/R7x4GqCvZ4=" crossorigin="anonymous"></script>
<script> var {useState, useEffect, useCallback} = React</script>
<div id="root"></div>
Комментарии:
1. Спасибо. Функция ref в React не идеальна. Ссылки намного проще в Vue, например.
2. @Erwol да, вы можете это сделать. Если вам нужно выполнить повторный рендеринг при изменении узла, используйте
useState
/setState
. Если изменение узла не должно вызывать повторный рендеринг, используйте ref или просто переменную экземпляра (в случае классов). Если вы используете ссылки, вы обычно предпочитаете написать что-то вродеthis.containerRef.current = currentNode
.3. как насчет переадресации ссылок? я думаю, мы, возможно, все еще сможем использовать
React.createRef()
ссылки, если примем ссылку извне компонента (Comp(props, ref)
и т.д.). предполагая, что ссылка обновляется при каждом рендеринге; может ли что-то подобное сработать?4. Отличный четкий ответ!
5. Это был первый раз, когда я увидел useCallback в примере, который действительно имел смысл для меня. Спасибо!
Ответ №2:
componentDidUpdate
вызывается при изменении состояния компонента или реквизитов, поэтому оно не обязательно будет вызываться при ref
изменении, поскольку его можно видоизменять по своему усмотрению.
Если вы хотите проверить, изменилась ли ссылка с предыдущего рендеринга, вы можете сохранить другую ссылку, которую вы сверяете с реальной.
Пример
class App extends React.Component {
prevRef = null;
ref = React.createRef();
state = {
isVisible: true
};
componentDidMount() {
this.prevRef = this.ref.current;
setTimeout(() => {
this.setState({ isVisible: false });
}, 1000);
}
componentDidUpdate() {
if (this.prevRef !== this.ref.current) {
console.log("ref changed!");
}
this.prevRef = this.ref.current;
}
render() {
return this.state.isVisible ? <div ref={this.ref}>Foo</div> : null;
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="root"></div>
Комментарии:
1. Итак, в примере на основе классов, когда я должен выполнить эту проверку?
2. Подождите, разве это не
componentDidUpdate
вызывается после каждогоrender
? Итак, неcomponentDidUpdate
ли это подходящее место для выполнения проверки (даже еслиcomponentDidUpdate
это было вызвано косвенно изменениями prop или состояния)?3. @trusktr Да, вы правы, это
componentDidUpdate
вызывается косвенно после prop или изменения состояния, ноref
это изменяемое значение, которое может быть изменено чем угодно, и React не имеет возможности узнать, что ссылка изменилась в этом смысле. В примере класса вы бы использовали комбинациюcomponentDidMount
иcomponentDidUpdate
. Я обновил ответ.4. «ссылка — это изменяемое значение, которое может быть изменено чем угодно», верно, но аналогично это может сделать что угодно в
this.state
, однако мы, очевидно, избегаем этого, потому что это не способ изменить состояние. Аналогично, я думаю, было бы (надеюсь) очевидно, что мы не должны произвольно изменять реквизиты или ссылки. Итак, похоже, что если мы позволим изменять только Reactref.current
(только путем передачи ref в разметку JSX), то наша идея о необходимости отслеживать старое значение кажется единственным способом сделать это. Было бы неплохо, если бы в React было больше возможностей для этого.5. Со старыми ссылками (ссылки на основе функций) было легко просто
setState
ввести новую ссылку внутри функций, что вызвало бы реактивность без необходимости отслеживать старые значения вручную. На первый взгляд, это могло бы быть более интуитивно понятным (например, более очевидным, как обрабатывать реактивность). (Однако меня бесит, что каждый вызов функции ВСЕГДА начинался с нулевой ссылки, что было абсолютно ошеломляющим. Они аргументировали это тем, что это было для принудительной очистки, но я думаю, что это вызвало больше проблем, чем защита от неправильного кода конечного пользователя).