Búsqueda de sitios web

javascript - ¿Cómo puedo usar múltiples refs para un array de elementos con hooks?


Hasta donde entendí, puedo usar referencias para un solo elemento así:

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>

¿Cómo puedo implementar esto para un arreglo de elementos? Obviamente no así: (¡lo sabía incluso sin intentarlo:) )

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>

He visto esto y, por lo tanto, esto. Pero, todavía estoy confundido sobre cómo implementar esa sugerencia para este caso simple.

Respuesta #1

Como no se pueden usar hooks dentro de bucles, aquí hay una solución para que funcione cuando el array cambia con el tiempo.

Supongo que el arreglo proviene de las props:

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>
    ));
}

Respuesta #2

Un ref es inicialmente solo un objeto { current: null }. useRef mantiene la referencia a este objeto entre renderizados del componente. El valor de current está destinado principalmente para refs de componentes, pero puede contener cualquier cosa.

Debería haber un arreglo de referencias en algún momento. En caso de que la longitud del arreglo pueda variar entre renders, el arreglo debería ajustarse en consecuencia:

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>
);

Este fragmento de código se puede optimizar desempaquetando useEffect y reemplazando useState con useRef, pero se debe tener en cuenta que realizar efectos secundarios en la función de renderizado generalmente se considera una mala práctica:

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>
);

Respuesta #3

Actualizar

El nuevo documento de React muestra una forma recomendada usando map.

Consulta la versión Beta aquí (dic., 2022)


Hay dos maneras

  1. usa un ref con múltiples elementos current
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. usar un Array de ref

    Como dijo el post anterior, no se recomienda ya que la guía oficial (y la verificación interna de lint) no permitirá que pase.

    No llames a los Hooks dentro de bucles, condiciones o funciones anidadas. En su lugar, siempre usa los Hooks en el nivel superior de tu función de React. Al seguir esta regla, te aseguras de que los Hooks se llamen en el mismo orden cada vez que un componente se renderiza.

    Sin embargo, dado que no es nuestro caso actual, la demostración a continuación todavía funciona, solo que no se recomienda.

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>

Respuesta #4

Todas las demás opciones anteriores dependen de Arrays, pero esto hace que las cosas sean extremadamente frágiles, ya que los elementos podrían reordenarse y entonces no llevamos un registro de a qué elemento pertenece cada referencia.

React utiliza la propiedad key para hacer un seguimiento de los elementos. Por lo tanto, si almacenas tus referencias por claves, no habrá ningún problema:

const useRefs = () => {
  const refsByKey = useRef<Record<string,HTMLElement | null>>({})

  const setRef = (element: HTMLElement | null, key: string) => {
    refsByKey.current[key] = element;
  }

  return {refsByKey: refsByKey.current, setRef};
}
const Comp = ({ items }) => {
  const {refsByKey, setRef} = useRefs()

  const refs = Object.values(refsByKey).filter(Boolean) // your array of refs here

  return (
    <div>
      {items.map(item => (
        <div key={item.id} ref={element => setRef(element, item.id)}/>
      )}
    </div>
  )
}

Tenga en cuenta que React, al desmontar un elemento, llamará a la función proporcionada con null, lo que establecerá la entrada key correspondiente a null en el objeto, por lo que todo estará actualizado.

Respuesta #5

La forma más simple y efectiva es no usar useRef en absoluto. Simplemente use un ref de callback que cree un nuevo array de refs en cada renderizado.

function useArrayRef() {
  const refs = []
  return [refs, el => el && refs.push(el)]
}

Demostración

<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.log(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>

Actualizar :

Aquí hay una versión en TypeScript del hook que crea referencias de callback memorizadas y admite pasar una clave personalizada a cada referencia de elemento.

import { MutableRefObject, useRef } from 'react'

type UseElementsRefResult<T extends HTMLElement = HTMLElement> = [
  /**
   * Map of DOM node refs identified by an auto-created key or custom one.
   */
  MutableRefObject<{ readonly [K: string | number]: T }>,

  /**
   * Creates a memoized callback ref for a DOM node.
   * If you expect the rendering order of nodes to change, use a custom key.
   *
   * @param customKey Key used to set/unset the node ref.
   */
  (customKey?: string | number) => (el?: T) => void,
]

/**
 * Hook that creates callback refs to manage DOM node refs of multiple elements.
 * Ensures refs remain stable even with re-renders. Use custom keys if the rendering order may change.
 */
export const useElementsRef = <T extends HTMLElement = HTMLElement>(): UseElementsRefResult<T> => {
  const elementRefs = useRef({
    current: {} as { [K: string | number]: T },
    callbacks: {} as Record<string | number, (el?: T) => void>,
  })

  // Resets index on every render; stable refs as long as the render order remains unchanged
  let currentIndex = 0

  return [
    elementRefs.current,
    (customKey) => {
      const { current: elements, callbacks } = elementRefs.current
      const autoKey = currentIndex++
      const key = customKey ?? autoKey

      // Memoize callback for stable ref assignment
      if (!callbacks[key]) {
        callbacks[key] = (el) => {
          // React is setting the ref for this node
          if (el != null) {
            elements[key] = el
            return
          }

          // React is clearing the node ref
          delete elements[key]
        }
      }

      return callbacks[key]
    },
  ]
}

Demostración

const { render } = ReactDOM
const { useEffect, useRef, useState } = React

const useElementsRef = () => {
  const elementRefs = useRef({
    current: {},
    callbacks: {},
  })

  let currentIndex = 0

  return [
    elementRefs.current,
    (customKey) => {
      const { current: elements, callbacks } = elementRefs.current
      const autoKey = currentIndex++
      const key = customKey ?? autoKey

      // Memoize callback for stable ref assignment
      if (!callbacks[key]) {
        callbacks[key] = (el) => {
          // React is setting the ref for this node
          if (el != null) {
            elements[key] = el
            return
          }

          // React is clearing the node ref
          delete elements[key]
        }
      }

      return callbacks[key]
    },
  ]
}

const App = () => {
  const [elementsRef, createElementRef] = useElementsRef()
  const [third, setThird] = useState(false)

  useEffect(() => {
    console.log(elementsRef.current)
  }, [third])

  return (
    <div>
      <div ref={createElementRef()}>
        <button ref={createElementRef()} onClick={() => setThird(!third)}>toggle third div</button>
      </div>
      <div ref={createElementRef('custom-key')}>another div</div>
      { third && <div ref={createElementRef('third-div')}>third div</div>}
    </div>
  );
}

render(<App />, document.getElementById("root"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Respuesta #6

¡Ten en cuenta que no deberías usar useRef en un bucle por una razón simple: el orden de los hooks utilizados importa!

La documentación dice

No llames a los Hooks dentro de bucles, condiciones o funciones anidadas. En su lugar, siempre usa los Hooks en el nivel superior de tu función de React. Siguiendo esta regla, aseguras que los Hooks se llamen en el mismo orden cada vez que un componente se renderiza. Eso es lo que permite a React preservar correctamente el estado de los Hooks entre múltiples llamadas a useState y useEffect. (Si tienes curiosidad, explicaremos esto en profundidad más abajo.)

Pero considera que obviamente se aplica a los arrays dinámicos... pero si estás usando arrays estáticos (siempre renderizas la misma cantidad de componentes) no te preocupes demasiado por eso, ten en cuenta lo que estás haciendo y aprovecha eso 😉

Artículos relacionados