Как я могу использовать несколько ссылок для массива элементов с хуками?

Насколько я понял, я могу использовать 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>

Я видел это и, следовательно, это. Но я все еще не понимаю, как реализовать это предложение для этого простого случая.

Простите, если это невежество, но если вы вызываете useRef() только один раз, почему вы ожидаете, что элементы будут иметь разные ссылки? Насколько я знаю, React использует ссылку как идентификатор повторяющихся элементов, поэтому он не знает разницы между ними, когда вы используете одну и ту же ссылку.

MTCoster 11.02.2019 16:21

Здесь нет невежества, так как я все еще изучаю хуки и рефы. Так что любой совет для меня хороший совет. Это то, что я хочу сделать, динамически создавать разные ссылки для разных элементов. Мой второй пример - это просто пример "Не используйте это" :)

devserkan 11.02.2019 16:26

Откуда взялось [1,2,3]? Это статично? Ответ зависит от этого.

Estus Flask 11.02.2019 16:29

В конце концов, они будут поступать с удаленной конечной точки. Но пока, если выучу статическую буду рад. Если бы вы могли объяснить удаленную ситуацию, это было бы здорово. Спасибо.

devserkan 11.02.2019 16:31
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
141
4
146 526
10
Перейти к ответу Данный вопрос помечен как решенный

Ответы 10

Ответ принят как подходящий

Ссылка изначально является просто объектом { 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, как я сделал в моем первом примере.

devserkan 11.02.2019 19:12

Я не уверен, правильно ли я вас понял. Но состояние, вероятно, также должно быть массивом, например, setElWidth(elRef.current.map(innerElRef => innerElRef.current.offsetWidth)].

Estus Flask 11.02.2019 19:23

О, спасибо. Во-первых, я думал сохранить состояние как объект и установить связь между элементами, но поскольку длина элементов и ссылок будет одинаковой, я думаю, что могу использовать эту логику.

devserkan 11.02.2019 22:21

это работает только в том случае, если массив всегда имеет одинаковую длину, если длина варьируется, ваше решение не будет работать.

Olivier Boissé 09.05.2019 17:31

где бы вы выполнили эту инструкцию elRef.current[i] = elRef.current[i] || createRef()?

Olivier Boissé 09.05.2019 17:44

@OlivierBoissé В приведенном выше коде это произойдет внутри .map((el, i) => ....

Estus Flask 09.05.2019 17:49

Умное решение. Отличный пример значения подписи createRef.

codeVerine 04.07.2019 13:13

Вам не хватает скобки. Первая строка должна быть: const elRef = useRef([...Array(3)].map(() => createRef()));

robsco 10.07.2019 16:02

@EstusFlask .map((el, i) => вызывается только при первой инициализации компонента. Если длина массива (в данном случае 3) изменилась на основе реквизита, новые элементы не будут инициализированы.

Greg 19.02.2020 19:45

@Greg Об этом спрашивал OP, это было отмечено в сообщении Если длина массива фиксирована. FWIW, я обновил код, чтобы сделать его более гибким.

Estus Flask 19.02.2020 22:07

@EstusFlask useEffect будет вызываться после метода рендеринга, поэтому эти ссылки будут созданы после их использования.

Greg 19.02.2020 22:46

@ Грег Это правильно. Это не проблема, потому что элементы будут перерисованы после обновления elRefs с правильными ссылками. Можно оптимизировать комп, исключив обновления состояния и useEffect (я думаю, это то, что вы предлагаете в своем ответе), но я не могу рекомендовать это в чрезмерно обобщенном решении, подобном этому.

Estus Flask 19.02.2020 22:56

@EstusFlask не понимает, в чем преимущество вашего решения за счет дополнительного рендеринга

Greg 19.02.2020 23:17

@Greg Преимущество заключается в том, чтобы не иметь побочных эффектов в функции рендеринга, что считается плохой практикой, которая приемлема, но ее не следует рекомендовать в качестве практического правила. Если бы я ради предварительной оптимизации сделал наоборот, то это тоже был бы повод покритиковать ответ. Я не могу придумать случай, в котором побочный эффект на месте был бы действительно плохим выбором, но это не значит, что его не существует. Я просто оставлю все варианты.

Estus Flask 20.02.2020 11:29

@Hugo Ответ содержит работоспособный код. Это может не сработать для вас, если вы не используете его так, как он не работает. Проблема, вероятно, специфична для вашего случая, рассмотрите возможность публикации вопроса, который отражает ее.

Estus Flask 22.09.2020 11:49

@EstusFlask, не могли бы вы объяснить мне, как мне передать ref в hook? Я хотел бы адаптировать ваше решение к пользовательскому hook, но я совершенно не понимаю, как передать ref = {elRef[i]} в качестве аргумента, соединяющего его с hook. Спасибо =)

Aliosh r 29.09.2020 03:21

@Alioshr Если я правильно понял ваш случай, вам не нужно передавать его на крючок. useRef нужно вызывать внутри хука. Хук, аналогичный описанному случаю, будет содержать код от последнего сниппета как есть, вплоть до return <div>, let myHook = arrLength => { ...; return elRefs }. И будет использоваться в компоненте как let elRefs=myHook(arr.length).

Estus Flask 01.10.2020 21:49

Обратите внимание, что вы не должны использовать 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 для массива элементов, размер которых заранее неизвестен.

MeanMan 26.07.2019 13:57

это решение должно работать, когда размер заранее неизвестен, useEffect выполняется после рендеринга, он изменяет размер массива, чтобы он соответствовал фактическому количеству элементов.

Olivier Boissé 26.07.2019 15:35

Отлично! Дополнительное примечание: в TypeScript подпись itemsRef выглядит так: const itemsRef = useRef<Array<HTMLDivElement | null>>([]).

Mike Niebling 30.08.2019 23:57

Как насчет компонентов на основе классов? Поскольку хуки не работают внутри композиций на основе классов...

Yuri Ramos 13.11.2019 15:51

вы можете получить тот же результат в компоненте класса, создав переменную экземпляра в конструкторе с помощью this.itemsRef = []. Затем вам нужно переместить код useEffect внутрь метода жизненного цикла componentDidUpdate. Наконец, в методе render вы должны использовать <div key = {i} ref = {el => this.itemsRef.current[i] = el} ` для хранения ссылок

Olivier Boissé 13.11.2019 17:17

Это не работает для меня.

Vinay Prajapati 03.12.2019 11:08

Как это работает, если ожидаемый массив может быть больше?

madav 23.12.2019 12:18

От callback refs до React.createRef() и обратно, чтобы хорошо играть с хуками. ?

Drazen Bjelovuk 25.02.2020 23:15

Может быть хорошей идеей использовать props.items.length в массиве useEffect?

Jamneck 13.06.2020 14:09

@LuckyStrike да, конечно, поскольку размер массива будет таким же, нет необходимости вызывать метод slice, но польза будет очень-очень маленькой

Olivier Boissé 13.06.2020 18:24

это не работает для меня, я получаю undefined как ref prof на элементе

Hugo 22.09.2020 11:40

Это было хорошим решением для меня, спасибо! Я передал ссылку компоненту с помощью forwardRef, а затем использовал ref = {el => itemsRef.current[i] = el} внутри карты его рендеринга.

Kitson 13.10.2020 13:24

Как правильно написать подпись 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[]>([]);, но оба типа дают мне ошибки.

Marecky 19.11.2020 14:48

На самом деле вы можете использовать хуки внутри циклов. Иногда это полезно. Вам просто нужно убедиться, что цикл выполняется одинаковое количество раз при каждом рендеринге.

ICW 27.09.2021 16:53

Вы можете использовать массив (или объект), чтобы отслеживать все ссылки и использовать метод для добавления ссылок в массив.

ПРИМЕЧАНИЕ. Если вы добавляете и удаляете ссылки, вам придется очищать массив каждый цикл рендеринга.

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 в массиве, вы должны очищать его при каждом цикле рендеринга?

GWorking 26.03.2020 11:03

Поскольку каждый цикл рендеринга он будет добавлять его в массив, нам нужна только одна копия файла el.

Neo 03.04.2020 00:09

Да, но разве вы не проверяете с помощью !refs.current.includes(el)?

GWorking 03.04.2020 22:28

Мы не можем использовать состояние, потому что нам нужно, чтобы ссылка была доступна до вызова метода рендеринга. Мы не можем вызывать 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/…

Daniele Orlando 11.03.2020 17:41

Есть два способа

  1. используйте один ссылка с несколькими элементами Текущий
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>
  1. используйте массив ссылка

    Как сказано в приведенном выше сообщении, это не рекомендуется, поскольку официальное руководство (и внутренняя проверка на ворсинки) не позволит ему пройти.

    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 с реактивными картами.

7ahid 06.07.2020 03:25

просто, но полезно

Faris Rayhan 02.10.2020 15:05

Вариант №2 не правильный. Вы можете использовать хуки только на верхнем уровне: pl.reactjs.org/docs/… #2 работает для вас, пока длина списка постоянна, но когда вы добавите новый элемент в список, он выдаст ошибку.

Adrian 30.03.2021 08:39

@Adrian Как я уже сказал в ответе, писать таким образом нельзя, это также не рекомендуется, вы можете не использовать его и щелкнуть по минусу, но это не позволяет приведенной выше демонстрации не работать (вы можете попробовать его также, нажав show code snippet, затем Run). Причина, по которой я все еще оставляю № 2, состоит в том, чтобы прояснить, почему существует проблема.

keikai 30.03.2021 09:11

первый метод работает как шарм.

zonay 10.11.2021 15:35

Вариант № 1 будет без необходимости повторно отображать все дочерние элементы, возвращая новую функцию ref при каждом рендеринге.

Daniel 03.01.2022 23:57

Предполагая, что ваш массив содержит не примитивы, вы можете использовать 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. Полезно знать :)

devserkan 10.06.2020 13:52

Если я правильно понимаю, 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, но «пустые» ссылки останутся.

Другие вопросы по теме