Как управлять React refs в цикле для нескольких копий компонента

    При разработке на React часто возникает задача отрендерить несколько копий одного компонента и получить ссылки (refs) на каждый из них. В этой статье мы разберём типичную ошибку, когда console.log возвращает null, и предложим правильное решение с использованием forwardRef и useCallback.

    Почему ref возвращает null при рендере в цикле

    В исходном коде используется useRef([]) и функция setRef, которая присваивает элемент по индексу. Проблема в том, что ref callback вызывается дважды: при монтировании и при размонтировании. Во время размонтирования в setRef передаётся null, что затирает сохранённую ссылку. Кроме того, при изменении numComp массив compRefs.current заполняется null, а старые ссылки теряются.

    Правильное решение: используем useCallback и useEffect

    Чтобы избежать затирания refs, нужно обновлять массив ссылок только при монтировании и корректно обрабатывать очистку. Вот рабочий пример:

    const [numComp, setNumComp] = useState(1);
    const compRefs = useRef([]);
    
    useEffect(() => {
      // Инициализируем массив нужной длины
      compRefs.current = compRefs.current.slice(0, numComp);
      while (compRefs.current.length < numComp) {
        compRefs.current.push(null);
      }
    }, [numComp]);
    
    const setRef = useCallback((el, index) => {
      compRefs.current[index] = el;
    }, []);
    
    const handleClick = useCallback((index) => {
      if (compRefs.current[index]) {
        console.log(compRefs.current[index]);
      }
    }, []);
    
    return (
      <>
        {[...Array(numComp)].map((_, index) => (
          <CompExample
            key={index}
            ref={(el) => setRef(el, index)}
            onClick={() => handleClick(index)}
          />
        ))}
      </>
    );

    Ключевые моменты для корректной работы

    • key - обязательно добавьте уникальный ключ для каждого элемента в цикле, чтобы React правильно отслеживал изменения.
    • forwardRef - убедитесь, что CompExample обёрнут в React.forwardRef и передаёт ref на корневой DOM-элемент.
    • useCallback - оберните setRef и handleClick в useCallback, чтобы избежать лишних ререндеров.
    • Очистка ref - не используйте обнуление массива через fill(null), так как это сбрасывает уже сохранённые ссылки.

    Альтернативный подход: хранение refs в state

    Если вам нужно динамически добавлять или удалять компоненты, рассмотрите хранение refs в useState с объектом:

    const [refs, setRefs] = useState({});
    
    const setRef = useCallback((el, index) => {
      setRefs(prev => ({...prev, [index]: el}));
    }, []);
    
    const handleClick = useCallback((index) => {
      console.log(refs[index]);
    }, [refs]);

    Этот способ гарантирует, что ссылки не затираются, но может вызывать ререндеры при каждом изменении refs.

    Проверка и отладка

    Чтобы убедиться, что refs работают корректно, выведите массив compRefs.current в консоль после рендера. Если все элементы - это DOM-узлы или экземпляры компонентов, значит, решение верное. В противном случае проверьте, что forwardRef настроен правильно и что ref не передаётся на функциональный компонент без обёртки.

    Заключение

    Проблема с null в консоли возникает из-за неправильного управления массивом refs и игнорирования фаз жизненного цикла. Используйте useCallback для функций, useEffect для синхронизации длины массива и обязательно добавляйте key в цикле. Теперь вы можете легко управлять refs для любого количества копий компонента.

    Часто задаваемые вопросы