Насколько я понял, я могу использовать refs для одного элемента следующим образом:
const { useRef, useState, useEffect } = React;
const App = () => {
const elRef = useRef();
const [elWidth, setElWidth] = useState();
useEffect(() => {
setElWidth(elRef.current.offsetWidth);
}, []);
return (
<div>
<div ref = {elRef} style = {{ width: "100px" }}>
Width is: {elWidth}
</div>
</div>
);
};
ReactDOM.render(
<App />,
document.getElementById("root")
);<script src = "https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script src = "https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<div id = "root"></div>Как я могу реализовать это для массива элементов? Явно не так: (я это знал, даже не пробовал :)
const { useRef, useState, useEffect } = React;
const App = () => {
const elRef = useRef();
const [elWidth, setElWidth] = useState();
useEffect(() => {
setElWidth(elRef.current.offsetWidth);
}, []);
return (
<div>
{[1, 2, 3].map(el => (
<div ref = {elRef} style = {{ width: `${el * 100}px` }}>
Width is: {elWidth}
</div>
))}
</div>
);
};
ReactDOM.render(
<App />,
document.getElementById("root")
);<script src = "https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script src = "https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<div id = "root"></div>Я видел это и, следовательно, это. Но я все еще не понимаю, как реализовать это предложение для этого простого случая.
Здесь нет невежества, так как я все еще изучаю хуки и рефы. Так что любой совет для меня хороший совет. Это то, что я хочу сделать, динамически создавать разные ссылки для разных элементов. Мой второй пример - это просто пример "Не используйте это" :)
Откуда взялось [1,2,3]? Это статично? Ответ зависит от этого.
В конце концов, они будут поступать с удаленной конечной точки. Но пока, если выучу статическую буду рад. Если бы вы могли объяснить удаленную ситуацию, это было бы здорово. Спасибо.



![Безумие обратных вызовов в javascript [JS]](https://i.imgur.com/WsjO6zJb.png)


Ссылка изначально является просто объектом { current: null }. useRef хранит ссылку на этот объект между рендерами компонентов. Значение current в первую очередь предназначено для ссылок на компоненты, но может содержать что угодно.
В какой-то момент должен быть массив ссылок. В случае, если длина массива может варьироваться между рендерами, массив должен масштабироваться соответствующим образом:
const arrLength = arr.length;
const [elRefs, setElRefs] = React.useState([]);
React.useEffect(() => {
// add or remove refs
setElRefs((elRefs) =>
Array(arrLength)
.fill()
.map((_, i) => elRefs[i] || createRef()),
);
}, [arrLength]);
return (
<div>
{arr.map((el, i) => (
<div ref = {elRefs[i]} style = {...}>
...
</div>
))}
</div>
);
Этот фрагмент кода можно оптимизировать, развернув useEffect и заменив useState на useRef, но следует отметить, что использование побочных эффектов в функции рендеринга обычно считается плохой практикой:
const arrLength = arr.length;
const elRefs = React.useRef([]);
if (elRefs.current.length !== arrLength) {
// add or remove refs
elRefs.current = Array(arrLength)
.fill()
.map((_, i) => elRefs.current[i] || createRef());
}
return (
<div>
{arr.map((el, i) => (
<div ref = {elRefs.current[i]} style = {...}>
...
</div>
))}
</div>
);
Спасибо за ваш ответ @estus. Это ясно показывает, как я могу создавать ссылки. Можете ли вы предоставить способ, как я могу использовать эти ссылки с «состоянием», если это возможно, пожалуйста? Так как в этом состоянии я не могу использовать ни один из рефов, если я не ошибаюсь. Они не создаются перед первым рендером, и мне как-то нужно использовать useEffect и состояние, я думаю. Скажем, я хочу получить ширину этих элементов, используя refs, как я сделал в моем первом примере.
Я не уверен, правильно ли я вас понял. Но состояние, вероятно, также должно быть массивом, например, setElWidth(elRef.current.map(innerElRef => innerElRef.current.offsetWidth)].
О, спасибо. Во-первых, я думал сохранить состояние как объект и установить связь между элементами, но поскольку длина элементов и ссылок будет одинаковой, я думаю, что могу использовать эту логику.
это работает только в том случае, если массив всегда имеет одинаковую длину, если длина варьируется, ваше решение не будет работать.
где бы вы выполнили эту инструкцию elRef.current[i] = elRef.current[i] || createRef()?
@OlivierBoissé В приведенном выше коде это произойдет внутри .map((el, i) => ....
Умное решение. Отличный пример значения подписи createRef.
Вам не хватает скобки. Первая строка должна быть: const elRef = useRef([...Array(3)].map(() => createRef()));
@EstusFlask .map((el, i) => вызывается только при первой инициализации компонента. Если длина массива (в данном случае 3) изменилась на основе реквизита, новые элементы не будут инициализированы.
@Greg Об этом спрашивал OP, это было отмечено в сообщении Если длина массива фиксирована. FWIW, я обновил код, чтобы сделать его более гибким.
@EstusFlask useEffect будет вызываться после метода рендеринга, поэтому эти ссылки будут созданы после их использования.
@ Грег Это правильно. Это не проблема, потому что элементы будут перерисованы после обновления elRefs с правильными ссылками. Можно оптимизировать комп, исключив обновления состояния и useEffect (я думаю, это то, что вы предлагаете в своем ответе), но я не могу рекомендовать это в чрезмерно обобщенном решении, подобном этому.
@EstusFlask не понимает, в чем преимущество вашего решения за счет дополнительного рендеринга
@Greg Преимущество заключается в том, чтобы не иметь побочных эффектов в функции рендеринга, что считается плохой практикой, которая приемлема, но ее не следует рекомендовать в качестве практического правила. Если бы я ради предварительной оптимизации сделал наоборот, то это тоже был бы повод покритиковать ответ. Я не могу придумать случай, в котором побочный эффект на месте был бы действительно плохим выбором, но это не значит, что его не существует. Я просто оставлю все варианты.
@Hugo Ответ содержит работоспособный код. Это может не сработать для вас, если вы не используете его так, как он не работает. Проблема, вероятно, специфична для вашего случая, рассмотрите возможность публикации вопроса, который отражает ее.
@EstusFlask, не могли бы вы объяснить мне, как мне передать ref в hook? Я хотел бы адаптировать ваше решение к пользовательскому hook, но я совершенно не понимаю, как передать ref = {elRef[i]} в качестве аргумента, соединяющего его с hook. Спасибо =)
@Alioshr Если я правильно понял ваш случай, вам не нужно передавать его на крючок. useRef нужно вызывать внутри хука. Хук, аналогичный описанному случаю, будет содержать код от последнего сниппета как есть, вплоть до return <div>, let myHook = arrLength => { ...; return elRefs }. И будет использоваться в компоненте как let elRefs=myHook(arr.length).
Обратите внимание, что вы не должны использовать useRef в цикле по простой причине: порядок используемых хуков имеет значение!
В документации говорится
Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function. By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls. (If you’re curious, we’ll explain this in depth below.)
Но учтите, что это, очевидно, относится к динамическим массивам... но если вы используете статические массивы (вы ВСЕГДА визуализируете одно и то же количество компонентов), не беспокойтесь об этом слишком сильно, знайте, что вы делаете, и используйте это. ?
Как вы нельзя использовать хуки внутри циклов, вот решение, чтобы заставить его работать, когда массив меняется с течением времени.
Я полагаю, что массив исходит из реквизита:
const App = props => {
const itemsRef = useRef([]);
// you can access the elements with itemsRef.current[n]
useEffect(() => {
itemsRef.current = itemsRef.current.slice(0, props.items.length);
}, [props.items]);
return props.items.map((item, i) => (
<div
key = {i}
ref = {el => itemsRef.current[i] = el}
style = {{ width: `${(i + 1) * 100}px` }}>
...
</div>
));
}
refs для массива элементов, размер которых заранее неизвестен.
это решение должно работать, когда размер заранее неизвестен, useEffect выполняется после рендеринга, он изменяет размер массива, чтобы он соответствовал фактическому количеству элементов.
Отлично! Дополнительное примечание: в TypeScript подпись itemsRef выглядит так: const itemsRef = useRef<Array<HTMLDivElement | null>>([]).
Как насчет компонентов на основе классов? Поскольку хуки не работают внутри композиций на основе классов...
вы можете получить тот же результат в компоненте класса, создав переменную экземпляра в конструкторе с помощью this.itemsRef = []. Затем вам нужно переместить код useEffect внутрь метода жизненного цикла componentDidUpdate. Наконец, в методе render вы должны использовать <div key = {i} ref = {el => this.itemsRef.current[i] = el} ` для хранения ссылок
Это не работает для меня.
Как это работает, если ожидаемый массив может быть больше?
От callback refs до React.createRef() и обратно, чтобы хорошо играть с хуками. ?
Может быть хорошей идеей использовать props.items.length в массиве useEffect?
@LuckyStrike да, конечно, поскольку размер массива будет таким же, нет необходимости вызывать метод slice, но польза будет очень-очень маленькой
это не работает для меня, я получаю undefined как ref prof на элементе
Это было хорошим решением для меня, спасибо! Я передал ссылку компоненту с помощью forwardRef, а затем использовал ref = {el => itemsRef.current[i] = el} внутри карты его рендеринга.
Как правильно написать подпись TypeScript для const itemsRef = useRef([]); , если я сопоставляю свой массив элементов с массивом const DocumentEntry = React.forwardRef<{ setEditable(editable: boolean): void; }, EntryProps>(DefaultDocumentEntry);? Мне нужно получить метод setEditable для каждого экземпляра компонента в массиве экземпляров... Я пробовал const entriesInstances = React.useRef<React.ReactNode[]>([]); и const entriesInstances = React.useRef<typeof DocumentEntry[]>([]);, но оба типа дают мне ошибки.
На самом деле вы можете использовать хуки внутри циклов. Иногда это полезно. Вам просто нужно убедиться, что цикл выполняется одинаковое количество раз при каждом рендеринге.
Вы можете использовать массив (или объект), чтобы отслеживать все ссылки и использовать метод для добавления ссылок в массив.
ПРИМЕЧАНИЕ. Если вы добавляете и удаляете ссылки, вам придется очищать массив каждый цикл рендеринга.
import React, { useRef } from "react";
const MyComponent = () => {
// intialize as en empty array
const refs = useRefs([]); // or an {}
// Make it empty at every render cycle as we will get the full list of it at the end of the render cycle
refs.current = []; // or an {}
// since it is an array we need to method to add the refs
const addToRefs = el => {
if (el && !refs.current.includes(el)) {
refs.current.push(el);
}
};
return (
<div className = "App">
{[1,2,3,4].map(val => (
<div key = {val} ref = {addToRefs}>
{val}
</div>
))}
</div>
);
}
рабочий пример https://codesandbox.io/s/serene-hermann-kqpsu
Почему, если вы уже проверяете, находится ли el в массиве, вы должны очищать его при каждом цикле рендеринга?
Поскольку каждый цикл рендеринга он будет добавлять его в массив, нам нужна только одна копия файла el.
Да, но разве вы не проверяете с помощью !refs.current.includes(el)?
Мы не можем использовать состояние, потому что нам нужно, чтобы ссылка была доступна до вызова метода рендеринга. Мы не можем вызывать useRef произвольное количество раз, но можем вызвать его один раз:
Предполагая, что arr - это реквизит с множеством вещей:
const refs = useRef([]);
// free any refs that we're not using anymore
refs.current = refs.current.slice(0, arr.length);
// initialize any new refs
for (let step = refs.current.length; step < arr.length; step++) {
refs.current[step] = createRef();
}
Ссылки должны быть обновлены в побочном эффекте, таком как useEffect(). ...avoid setting refs during rendering — this can lead to surprising behavior. Instead, typically you want to modify refs in event handlers and effects.http://reactjs.org/docs/…
Есть два способа
const inputRef = useRef([]);
inputRef.current[idx].focus();
<input
ref = {el => inputRef.current[idx] = el}
/>
const {useRef} = React;
const App = () => {
const list = [...Array(8).keys()];
const inputRef = useRef([]);
const handler = idx => e => {
const next = inputRef.current[idx + 1];
if (next) {
next.focus()
}
};
return (
<div className = "App">
<div className = "input_boxes">
{list.map(x => (
<div>
<input
key = {x}
ref = {el => inputRef.current[x] = el}
onChange = {handler(x)}
type = "number"
className = "otp_box"
/>
</div>
))}
</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));<div id = "root"></div>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js"></script>используйте массив ссылка
Как сказано в приведенном выше сообщении, это не рекомендуется, поскольку официальное руководство (и внутренняя проверка на ворсинки) не позволит ему пройти.
Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function. By following this rule, you ensure that Hooks are called in the same order each time a component renders.
Однако, поскольку это не наш текущий случай, приведенная ниже демонстрация все еще работает, но не рекомендуется.
const inputRef = list.map(x => useRef(null));
inputRef[idx].current.focus();
<input
ref = {inputRef[idx]}
/>
const {useRef} = React;
const App = () => {
const list = [...Array(8).keys()];
const inputRef = list.map(x => useRef(null));
const handler = idx => () => {
const next = inputRef[idx + 1];
if (next) {
next.current.focus();
}
};
return (
<div className = "App">
<div className = "input_boxes">
{list.map(x => (
<div>
<input
key = {x}
ref = {inputRef[x]}
onChange = {handler(x)}
type = "number"
className = "otp_box"
/>
</div>
))}
</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));<div id = "root"></div>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js"></script>второй вариант - это то, что сработало для меня, когда я пытался использовать showCallout() на Marker с реактивными картами.
просто, но полезно
Вариант №2 не правильный. Вы можете использовать хуки только на верхнем уровне: pl.reactjs.org/docs/… #2 работает для вас, пока длина списка постоянна, но когда вы добавите новый элемент в список, он выдаст ошибку.
@Adrian Как я уже сказал в ответе, писать таким образом нельзя, это также не рекомендуется, вы можете не использовать его и щелкнуть по минусу, но это не позволяет приведенной выше демонстрации не работать (вы можете попробовать его также, нажав show code snippet, затем Run). Причина, по которой я все еще оставляю № 2, состоит в том, чтобы прояснить, почему существует проблема.
первый метод работает как шарм.
Вариант № 1 будет без необходимости повторно отображать все дочерние элементы, возвращая новую функцию ref при каждом рендеринге.
Предполагая, что ваш массив содержит не примитивы, вы можете использовать WeakMap в качестве значения Ref.
function MyComp(props) {
const itemsRef = React.useRef(new WeakMap())
// access an item's ref using itemsRef.get(someItem)
render (
<ul>
{props.items.map(item => (
<li ref = {el => itemsRef.current.set(item, el)}>
{item.label}
</li>
)}
</ul>
)
}
На самом деле, в моем реальном случае мой массив содержит непримитивы, но мне пришлось перебрать массив. Я думаю, что это невозможно с WeakMap, но это действительно хороший вариант, если не требуется итерация. Спасибо. PS: Ах, для этого есть предложение, и теперь это этап 3. Полезно знать :)
Если я правильно понимаю, useEffect следует использовать только для побочных эффектов, по этой причине вместо этого я решил использовать useMemo.
const App = props => {
const itemsRef = useMemo(() => Array(props.items.length).fill().map(() => createRef()), [props.items]);
return props.items.map((item, i) => (
<div
key = {i}
ref = {itemsRef[i]}
style = {{ width: `${(i + 1) * 100}px` }}>
...
</div>
));
};
Затем, если вы хотите манипулировать элементами/использовать побочные эффекты, вы можете сделать что-то вроде:
useEffect(() => {
itemsRef.map(e => e.current).forEach((e, i) => { ... });
}, [itemsRef.length])
Самый простой и эффективный способ — вообще не использовать useRef. Просто используйте ссылка обратного вызова, который создает новый массив ссылок при каждом рендеринге.
function useArrayRef() {
const refs = []
return [refs, el => el && refs.push(el)]
}
<div id = "root"></div>
<script type = "text/babel" defer>
const { useEffect, useState } = React
function useArrayRef() {
const refs = []
return [refs, el => el && refs.push(el)]
}
const App = () => {
const [elements, ref] = useArrayRef()
const [third, setThird] = useState(false)
useEffect(() => {
console.info(elements)
}, [third])
return (
<div>
<div ref = {ref}>
<button ref = {ref} onClick = {() => setThird(!third)}>toggle third div</button>
</div>
<div ref = {ref}>another div</div>
{ third && <div ref = {ref}>third div</div>}
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
</script>
<script src = "https://unpkg.com/@babel/standalone@7/babel.min.js"></script>
<script src = "https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script src = "https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>React будет повторно отображать элемент при изменении его ссылки (ссылочное равенство/проверка «тройного равенства»).
Большинство ответов здесь не учитывают это. Еще хуже: когда родитель визуализирует и повторно инициализирует объекты ref, все дочерние элементы будут повторно визуализированы, даже если это запомненные компоненты (React.PureComponent или React.memo)!
Приведенное ниже решение не имеет ненужных повторных рендеров, работает с динамическими списками и даже не создает реальных побочных эффектов. Доступ к неопределенной ссылке невозможен. Ссылка инициализируется при первом чтении. После этого он остается референциально устойчивым.
const useGetRef = () => {
const refs = React.useRef({})
return React.useCallback(
(idx) => (refs.current[idx] ??= React.createRef()),
[refs]
)
}
const Foo = ({ items }) => {
const getRef = useGetRef()
return items.map((item, i) => (
<div ref = {getRef(i)} key = {item.id}>
{/* alternatively, to access refs by id: `getRef(item.id)` */}
{item.title}
</div>
))
}
Предостережение: Когда items со временем сжимается, неиспользуемые объекты ref не удаляются. Когда React размонтирует элемент, он правильно установит ref[i].current = null, но «пустые» ссылки останутся.
Простите, если это невежество, но если вы вызываете
useRef()только один раз, почему вы ожидаете, что элементы будут иметь разные ссылки? Насколько я знаю, React использует ссылку как идентификатор повторяющихся элементов, поэтому он не знает разницы между ними, когда вы используете одну и ту же ссылку.