Wie kann ich mehrere Refs für ein Array von Elementen mit Hooks verwenden?

77

Soweit ich verstanden habe, kann ich refs für ein einzelnes Element wie dieses verwenden:

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>

Wie kann ich dies für ein Array von Elementen implementieren? Offensichtlich nicht so: (Ich wusste es, obwohl ich es nicht ausprobiert habe :)

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>

Ich habe das und damit das gesehen . Aber ich bin immer noch verwirrt darüber, wie ich diesen Vorschlag für diesen einfachen Fall umsetzen soll.

Devserkan
quelle
Verzeihen Sie mir, wenn dies unwissend ist, aber wenn Sie nur useRef()einmal anrufen , warum erwarten Sie dann, dass die Elemente unterschiedliche Refs haben? AFAIK, React verwendet die Referenz als Kennung für iterierte Elemente, sodass der Unterschied zwischen ihnen nicht bekannt ist, wenn Sie dieselbe
Referenz verwenden
2
Keine Unwissenheit hier, da ich immer noch Hooks und Refs lerne. Jeder Rat ist also ein guter Rat für mich. Dies ist, was ich tun möchte, dynamisch verschiedene Refs für verschiedene Elemente erstellen. Mein zweites Beispiel ist nur "Verwenden Sie dieses nicht" Beispiel :)
Devserkan
Woher kam [1,2,3]? Ist es statisch? Die Antwort hängt davon ab.
Estus Flask
Schließlich werden sie von einem entfernten Endpunkt kommen. Aber jetzt, wenn ich die statische lerne, werde ich froh sein. Wenn Sie für die entfernte Situation erklären können, wäre das fantastisch. Vielen Dank.
Devserkan

Antworten:

64

Ein Ref ist zunächst nur ein { current: null }Objekt. useRefbehält den Verweis auf dieses Objekt zwischen Komponentenrenderen bei. currentvalue ist hauptsächlich für Komponentenreferenzen gedacht, kann aber alles enthalten.

Irgendwann sollte es eine Reihe von Refs geben. Falls die Länge des Arrays zwischen den Renderings variieren kann, sollte ein Array entsprechend skaliert werden:

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

Dieses Stück Code kann durch Abwickeln optimiert werden useEffectund ersetzt useStatemit , useRefaber es sollte diese Funktion in machen dabei Nebenwirkungen beobachtet wird , wird in der Regel eine schlechte Praxis in Betracht gezogen:

  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 Flask
quelle
1
Vielen Dank für Ihre Antwort @estus. Dies zeigt deutlich, wie ich Refs erstellen kann. Können Sie eine Möglichkeit angeben, wie ich diese Refs wenn möglich mit "state" verwenden kann? Da ich in diesem Zustand keine Refs verwenden kann, wenn ich mich nicht irre. Sie werden nicht vor dem ersten Rendern erstellt und irgendwie muss ich sie verwenden useEffectund angeben, denke ich. Angenommen, ich möchte die Breite dieser Elemente mithilfe von Refs ermitteln, wie ich es in meinem ersten Beispiel getan habe.
Devserkan
Ich bin mir nicht sicher, ob ich dich richtig verstanden habe. Aber ein Staat muss wahrscheinlich auch ein Array sein, so etwas wiesetElWidth(elRef.current.map(innerElRef => innerElRef.current.offsetWidth)]
Estus Flask
3
Dies funktioniert nur, wenn das Array immer dieselbe Länge hat. Wenn die Länge variiert, funktioniert Ihre Lösung nicht.
Olivier Boissé
1
@ OlivierBoissé Im obigen Code würde dies im Inneren passieren .map((el, i) => ....
Estus Flask
1
@ Greg Der Vorteil ist, dass die Renderfunktion keine Nebenwirkungen hat. Dies wird als schlechte Vorgehensweise angesehen, die akzeptabel ist, aber als Faustregel nicht empfohlen werden sollte. Wenn ich es aus Gründen der vorläufigen Optimierung umgekehrt machen würde, wäre dies auch ein Grund, die Antwort zu kritisieren. Ich kann mir keinen Fall vorstellen, der eine Nebenwirkung an Ort und Stelle zu einer wirklich schlechten Wahl machen würde, aber das bedeutet nicht, dass es sie nicht gibt. Ich lasse einfach alle Optionen.
Estus Flask
95

Da Sie keine Hooks in Schleifen verwenden können , finden Sie hier eine Lösung, damit sie funktioniert, wenn sich das Array im Laufe der Zeit ändert.

Ich nehme an, das Array stammt von den Requisiten:

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>
    ));
}
Olivier Boissé
quelle
Verweise auf ein Array von Elementen, deren Größe vorher nicht bekannt ist.
MeanMan
12
Ausgezeichnet! Ein zusätzlicher Hinweis, in TypeScript itemsRefscheint die Signatur zu sein: const itemsRef = useRef<Array<HTMLDivElement | null>>([])
Mike Niebling
1
Sie können das gleiche Ergebnis in einer Klassenkomponente erzielen, indem Sie im Konstruktor mit eine Instanzvariable erstellen this.itemsRef = []. Anschließend müssen Sie den useEffect-Code innerhalb der componentDidUpdate-Lebenszyklusmethode verschieben. Schließlich sollten Sie in der renderMethode <div key={i} ref={el => this.itemsRef.current [i] = el} `verwenden, um die Refs zu speichern
Olivier Boissé
2
Das ist kein Wokring für mich.
Vinay Prajapati
1
Wie funktioniert das, wenn das erwartete Array möglicherweise größer ist?
Madav
16

Es gibt zwei Möglichkeiten

  1. Verwenden Sie eine Referenz mit mehreren aktuellen Elementen
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. Verwenden Sie ein Array von ref

    Wie im obigen Beitrag erwähnt, wird es nicht empfohlen, da die offizielle Richtlinie (und die Prüfung der inneren Flusen) nicht zulassen, dass es bestanden wird.

    Rufen Sie Hooks nicht in Schleifen, Bedingungen oder verschachtelten Funktionen auf. Verwenden Sie stattdessen immer Hooks auf der obersten Ebene Ihrer React-Funktion. Indem Sie diese Regel befolgen, stellen Sie sicher, dass Hooks bei jedem Rendern einer Komponente in derselben Reihenfolge aufgerufen werden.

    Da dies jedoch nicht unser aktueller Fall ist, funktioniert die folgende Demo weiterhin, wird jedoch nicht empfohlen.

const inputRef = list.map(x => useRef(null));

inputRef[idx].current.focus();

<input
  ref={inputRef[idx]}
/>

Keikai
quelle
Option zwei ist , was für mich gearbeitet versuchen , verwenden showCallout()auf reagieren-native-Karten Markers
7ahid
einfach aber hilfreich
Faris Rayhan
4

Beachten Sie, dass Sie useRef aus einem einfachen Grund nicht in einer Schleife verwenden sollten: Die Reihenfolge der verwendeten Hooks spielt eine Rolle!

Die Dokumentation sagt

Rufen Sie Hooks nicht in Schleifen, Bedingungen oder verschachtelten Funktionen auf. Verwenden Sie stattdessen immer Hooks auf der obersten Ebene Ihrer React-Funktion. Indem Sie diese Regel befolgen, stellen Sie sicher, dass Hooks bei jedem Rendern einer Komponente in derselben Reihenfolge aufgerufen werden. Auf diese Weise kann React den Status von Hooks zwischen mehreren useState- und useEffect-Aufrufen korrekt beibehalten. (Wenn Sie neugierig sind, werden wir dies im Folgenden ausführlich erläutern.)

Bedenken Sie jedoch, dass dies offensichtlich für dynamische Arrays gilt. Wenn Sie jedoch statische Arrays verwenden (Sie rendern IMMER die gleiche Anzahl von Komponenten), machen Sie sich darüber keine allzu großen Sorgen. Achten Sie darauf, was Sie tun, und nutzen Sie diese 😉

NoriSte
quelle
2

Sie können ein Array (oder ein Objekt) verwenden, um alle Refs zu verfolgen, und eine Methode verwenden, um dem Array Refs hinzuzufügen.

HINWEIS: Wenn Sie Refs hinzufügen und entfernen, müssen Sie das Array bei jedem Renderzyklus leeren.

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

}

Arbeitsbeispiel https://codesandbox.io/s/serene-hermann-kqpsu

Neo
quelle
Warum sollten Sie es bei jedem Renderzyklus leeren, wenn Sie bereits prüfen, ob sich el im Array befindet?
GWorking
Da es bei jedem Renderzyklus dem Array hinzugefügt wird, möchten wir nur eine Kopie des el.
Neo
1
Ja, aber überprüfen Sie nicht mit !refs.current.includes(el)?
GWorking
2

Angenommen, Ihr Array enthält keine Grundelemente, können Sie a WeakMapals Wert für verwenden 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>
    )
}
Dan F.
quelle
In meinem realen Fall enthält mein Array eigentlich Nicht-Grundelemente, aber ich musste das Array durchlaufen. Ich denke, dass dies mit WeakMap nicht möglich ist, aber es ist in der Tat eine gute Option, wenn keine Iteration erforderlich ist. Vielen Dank. PS: Ah, es gibt einen Vorschlag dafür und es ist jetzt Stufe 3. Gut zu wissen :)
devserkan
0

Wir können state nicht verwenden, da der ref verfügbar sein muss, bevor die Rendermethode aufgerufen wird. Wir können useRef nicht beliebig oft aufrufen, aber wir können es einmal aufrufen:

Angenommen, es arrist eine Requisite mit einer Reihe von Dingen:

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();
}
Greg
quelle
Referenzen sollten in einem Nebeneffekt wie aktualisiert werden useEffect(). ...avoid setting refs during rendering — this can lead to surprising behavior. Instead, typically you want to modify refs in event handlers and effects. reactjs.org/docs/…
Daniele Orlando
0

Wenn ich das richtig verstehe, useEffectsollte es nur für Nebenwirkungen verwendet werden, aus diesem Grund habe ich mich stattdessen für die Verwendung entschieden 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>
    ));
};

Wenn Sie dann die Elemente manipulieren / Nebenwirkungen verwenden möchten, können Sie Folgendes tun:

useEffect(() => {
    itemsRef.map(e => e.current).forEach((e, i) => { ... });
}, [itemsRef.length])
Gustavo Maximo
quelle