javascript - Hur kan jag använda flera refs för en array av element med hooks?


Såvitt jag förstod kan jag använda refs för ett enda element på detta sätt:

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>

Hur kan jag implementera detta för en array av element? Självklart inte så här: (Jag visste det även om jag inte provade det:)

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>

Jag har sett detta och därför detta. Men jag är fortfarande förvirrad över hur man ska genomföra det förslaget för detta enkla fall.

Svar #1

Eftersom du inte kan använda hooks inuti loopar, här är en lösning för att få det att fungera när arrayen förändras över tid.

Jag antar att arrayen kommer från 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>
    ));
}

Svar #2

Ett ref är initialt bara ett { current: null } objekt. useRef behåller referensen till detta objekt mellan komponentrenderingar. current-värdet är främst avsett för komponentrefs men kan hålla vad som helst.

Det bör finnas en matris av referenser vid någon punkt. Om matrisens längd kan variera mellan renderingar, bör matrisen anpassa sig därefter:

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

Detta kodstycke kan optimeras genom att avlägsna useEffect och ersätta useState med useRef, men det bör noteras att det generellt anses vara en dålig praxis att göra sidoeffekter i renderingsfunktionen:

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

Svar #3

Uppdatera

Den nya React-dokumentationen visar ett rekommenderat sätt genom att använda map.

Kolla beta-versionen här (december 2022)


Det finns två sätt

  1. använd en ref med flera current-element
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. använd en Array av ref

    Som inlägget ovan sa, är det inte rekommenderat eftersom de officiella riktlinjerna (och den interna lint-kontrollen) inte kommer att tillåta att det godkänns.

    Använd inte Hooks inuti loopar, villkor eller nästlade funktioner. Använd istället alltid Hooks på översta nivån i din React-funktion. Genom att följa denna regel säkerställer du att Hooks anropas i samma ordning varje gång en komponent renderas.

    Men eftersom det inte är vårt aktuella fall fungerar demon nedan fortfarande, bara inte rekommenderad.

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>

Svar #4

Alla andra alternativ ovan förlitar sig på arrayer, men det gör saker extremt bräckliga, eftersom element kan omordnas och då håller vi inte reda på vilken referens som tillhör vilket element.

React använder key-egenskapen för att hålla reda på objekt. Därför, om du sparar dina referenser med nycklar, kommer det inte att vara något problem :

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

Observera att React, när det avmonterar ett objekt, kommer att anropa den angivna funktionen med null, vilket kommer att sätta motsvarande key-post till null i objektet, så allt kommer att vara uppdaterat.

Svar #5

Det enklaste och mest effektiva sättet är att inte använda useRef alls. Använd bara en callback-ref som skapar en ny array av referenser vid varje render.

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

Demo

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

Uppdatering :

Här är en Typescript-version av hooken som skapar memoiserade callback-referenser och stöder att skicka en anpassad nyckel till varje elementreferens.

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]
    },
  ]
}

Demo

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>

Svar #6

Observera att du inte bör använda useRef i en loop av en enkel anledning: ordningen på använda hooks spelar roll!

Dokumentationen säger

Ring inte Hooks inuti loopar, villkor eller inbäddade funktioner. Använd istället alltid Hooks på topnivå i din React-funktion. Genom att följa denna regel säkerställer du att Hooks anropas i samma ordning varje gång en komponent renderas. Det är vad som tillåter React att korrekt bevara tillståndet för Hooks mellan flera useState- och useEffect-anrop. (Om du är nyfiken kommer vi att förklara detta mer ingående nedan.)

Men tänk på att det uppenbarligen gäller dynamiska arrayer... men om du använder statiska arrayer (du renderar ALLTID samma antal komponenter) behöver du inte oroa dig så mycket för det, var medveten om vad du gör och utnyttja det 😉

Relaterade artiklar