Wie registriere ich ein Ereignis mit useEffect-Hooks?

77

Ich verfolge einen Udemy-Kurs zum Registrieren von Ereignissen mit Hooks. Der Kursleiter gab den folgenden Code an:

  const [userText, setUserText] = useState('');

  const handleUserKeyPress = event => {
    const { key, keyCode } = event;

    if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
      setUserText(`${userText}${key}`);
    }
  };

  useEffect(() => {
    window.addEventListener('keydown', handleUserKeyPress);

    return () => {
      window.removeEventListener('keydown', handleUserKeyPress);
    };
  });

  return (
    <div>
      <h1>Feel free to type!</h1>
      <blockquote>{userText}</blockquote>
    </div>
  );

Jetzt funktioniert es großartig, aber ich bin nicht davon überzeugt, dass dies der richtige Weg ist. Der Grund dafür ist, dass, wenn ich das richtig verstehe, Ereignisse bei jedem erneuten Rendern jedes Mal registriert und abgemeldet werden, und ich denke einfach nicht, dass dies der richtige Weg ist, dies zu tun.

Also habe ich die useEffectHaken unten leicht modifiziert

useEffect(() => {
  window.addEventListener('keydown', handleUserKeyPress);

  return () => {
    window.removeEventListener('keydown', handleUserKeyPress);
  };
}, []);

Indem Sie ein leeres Array als zweites Argument verwenden und die Komponente den Effekt nur einmal ausführen lassen, um zu imitieren componentDidMount. Und wenn ich das Ergebnis ausprobiere, ist es seltsam, dass bei jedem Schlüssel, den ich eingebe, statt angehängt, stattdessen überschrieben wird.

Ich hatte setUserText ( ${userText}${key}) erwartet ; Wenn ein neuer Schlüssel an den aktuellen Status angehängt und als neuer Status festgelegt wird, wird der alte Status vergessen und mit dem neuen Status neu geschrieben.

War es wirklich die richtige Art und Weise, dass wir Ereignisse bei jedem erneuten Rendern registrieren und abmelden sollten?

Isaac
quelle

Antworten:

90

Der beste Weg, um solche Szenarien zu lösen, besteht darin, zu sehen, was Sie im Ereignishandler tun. Wenn Sie den Status einfach mit dem vorherigen Status festlegen, verwenden Sie am besten das Rückrufmuster und registrieren Sie die Ereignis-Listener nur beim ersten Mounten. Wenn Sie nicht die Optioncallback pattern ( https://reactjs.org/docs/hooks-reference.html#usecallback ) verwenden, wird die Listener-Referenz zusammen mit ihrem lexikalischen Bereich vom Ereignis-Listener verwendet, es wird jedoch eine neue Funktion mit aktualisiertem Abschluss erstellt neues Rendern und daher im Handler können Sie den aktualisierten Status nicht erreichen

const [userText, setUserText] = useState('');

  const handleUserKeyPress = useCallback(event => {
    const { key, keyCode } = event;

    if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
      setUserText(prevUserText => `${prevUserText}${key}`);
    }
  }, []);

  useEffect(() => {
    window.addEventListener('keydown', handleUserKeyPress);

    return () => {
      window.removeEventListener('keydown', handleUserKeyPress);
    };
  }, [handleUserKeyPress]);

  return (
    <div>
      <h1>Feel free to type!</h1>
      <blockquote>{userText}</blockquote>
    </div>
  );
Shubham Khatri
quelle
Ihre Lösung ist für mich im Vergleich zu anderen am sinnvollsten, vielen Dank!
Isaac
Ich bin
7
Ich sehe, dass es wichtig ist, prevUserText auf den vorherigen Zustand von zu verweisen userText. Was ist, wenn ich auf mehrere Status zugreifen muss? Wie kann ich auf alle vorherigen Status zugreifen?
Joey Yi Zhao
1
Wie können wir den vorherigen Status erhalten, wenn wir useReduceranstelle von verwenden useState?
Eesa
1
@tractatusviii Ich denke, dieser Beitrag wird Ihnen helfen: stackoverflow.com/questions/55840294/…
Shubham Khatri
17

Problem

[...] Bei jedem erneuten Rendern werden Ereignisse jedes Mal registriert und abgemeldet, und ich denke einfach nicht, dass dies der richtige Weg ist.

Du hast recht. Es ist nicht sinnvoll, die Ereignisbehandlung useEffectbei jedem Rendern neu zu starten .

[...] leeres Array als zweites Argument, sodass die Komponente den Effekt nur einmal ausführen kann, [...] es ist seltsam, dass bei jedem Schlüssel, den ich eingebe, statt angehängt, stattdessen überschrieben wird.

Dies ist ein Problem mit veralteten Schließwerten .

Grund: Verwendete Funktionen im Inneren useEffectsollten Teil der Abhängigkeiten sein . Sie setzen nichts als dependency ( []), rufen aber trotzdem auf handleUserKeyPress, der selbst userTextstate liest .

Lösung

Abhängig von Ihrem Anwendungsfall gibt es einige Alternativen.

1. Statusaktualisierungsfunktion

setUserText(prev => `${prev}${key}`);

✔ am wenigsten invasiver Ansatz
✖ nur Zugriff auf den eigenen vorherigen Zustand, nicht auf andere Zustände


2. useReducer- "Cheat-Modus"

Wir können useReducerzum aktuellen Status / Requisiten wechseln und Zugriff darauf haben - mit ähnlicher API wie useState.

Variante 2a: Logik innerhalb der Reduzierfunktion
const [userText, handleUserKeyPress] = useReducer((state, event) => {
    const { key, keyCode } = event;
    // isUpperCase is always the most recent state (no stale closure value)
    return `${state}${isUpperCase ? key.toUpperCase() : key}`;  
}, "");

Variante 2b: Logik außerhalb der Reduzierfunktion - ähnlich der useStateUpdater-Funktion
const [userText, setUserText] = useReducer((state, action) =>
      typeof action === "function" ? action(state, isUpperCase) : action, "");
// ...
setUserText((prevState, isUpper) => `${prevState}${isUpper ? key.toUpperCase() : key}`);

✔ keine Notwendigkeit , um Abhängigkeiten zu verwalten
✔ Zugriff mehrere Zustände und Requisiten
✔ gleiche API wie useState
✔ erweiterbar auf komplexere Fälle / Reduktions
✖ etwas weniger Leistung durch Inline - Reduzierstück ( irgendwie vernachlässigbaren )
✖ leicht erhöhte Komplexität Minderer


3. useCallback / event handler insideuseEffect

Wenn Sie nach handleUserKeyPressinnen gehen useEffect, zeigt Ihnen die ESLint-Regel für umfassende Deps, welche genauen kanonischen Abhängigkeiten fehlen ( userText):

useCallback ist eine äquivalente Alternative mit etwas mehr Indirektion von Abhängigkeiten:

Hinweis: Diese Variante kann zwar auf verschiedene Arten angewendet werden, ist jedoch nicht für den Fragenfall geeignet und der Vollständigkeit halber aufgeführt. Grund: Der Ereignis-Listener wird bei jedem Tastendruck neu gestartet.

✔ pragmatische Allzwecklösung
✔ minimal invasiv
✖ manuelles Abhängigkeitsmanagement
useCallback macht die Funktionsdefinition ausführlicher / übersichtlicher


4. useRef/ Rückruf in veränderlicher Referenz speichern

const cbRef = useRef(handleUserKeyPress);
useEffect(() => { cbRef.current = handleUserKeyPress; }); // update after each render
useEffect(() => {
    const cb = e => cbRef.current(e); // then use most recent cb value
    window.addEventListener("keydown", cb);
    return () => { window.removeEventListener("keydown", cb) };
}, []);

✔ für Rückrufe / Ereignishandler, die nicht zum erneuten Rendern des Datenflusses verwendet werden.
✔ Keine Notwendigkeit, Abhängigkeiten zu verwalten
✖ Nur als letzte Option von React Docs
empfohlen. ✖ Wichtigerer Ansatz

Weitere Informationen finden Sie unter diesen Links: 1 2 3

ford04
quelle
3
Wow, das ist eine Belohnung für eine Antwort!
Kremuwa
8

neue Antwort:

useEffect(() => {
  function handlekeydownEvent(event) {
    const { key, keyCode } = event;
    if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
      setUserText(prevUserText => `${prevUserText}${key}`);
    }
  }

  document.addEventListener('keyup', handlekeydownEvent)
  return () => {
    document.removeEventListener('keyup', handlekeydownEvent)
  }
}, [])

Übergeben Sie bei Verwendung setUserTextdie Funktion als Argument anstelle des Objekts. prevUserTextDies ist immer der neueste Status.


alte Antwort:

Versuchen Sie dies, es funktioniert genauso wie Ihr ursprünglicher Code:

useEffect(() => {
  function handlekeydownEvent(event) {
    const { key, keyCode } = event;
    if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
      setUserText(`${userText}${key}`);
    }
  }

  document.addEventListener('keyup', handlekeydownEvent)
  return () => {
    document.removeEventListener('keyup', handlekeydownEvent)
  }
}, [userText])

weil es in Ihrer useEffect()Methode von der userTextVariablen abhängt , aber Sie es nicht in das zweite Argument einfügen, sonst userTextwird das immer ''mit dem Argument an den Anfangswert gebunden[] .

Sie müssen dies nicht tun, sondern möchten Sie nur wissen lassen, warum Ihre zweite Lösung nicht funktioniert.

Spark.Bao
quelle
Durch Hinzufügen [userText]ist genau das gleiche wie ohne zweites Argument, oder? Grund dafür ist, dass ich nur userTextim obigen Beispiel habe und ohne zweites Argument einfach bedeutet, bei jeder Änderung von Requisiten / Status erneut zu rendern. Ich sehe nicht, wie es meine Frage beantwortet. ** P / S: ** Ich bin nicht der Downvoter, trotzdem danke für deine Antwort
Isaac
hey @Isaac, yep, es ist dasselbe wie ohne zweites Argument. Ich möchte Sie nur wissen lassen, warum Ihre zweite Lösung nicht funktioniert, da Ihre zweite Lösung useEffect()von der userTextVariablen abhängt, Sie aber die zweiten Argumente nicht eingegeben haben.
Spark.Bao
Wenn Sie jedoch hinzufügen [userText], bedeutet dies auch, dass Sie das Ereignis bei jedem erneuten Rendern registrieren und abmelden müssen, oder?
Isaac
genau! Deshalb sage ich, dass es bei Ihrer ersten Lösung genauso ist.
Spark.Bao
1
Ich habe verstanden, was Sie meinen, wenn Sie es in diesem Beispiel wirklich nur einmal registrieren möchten, müssen Sie es useRefgenau wie die Antwort von @Maaz Syed Adeeb verwenden.
Spark.Bao
1

Benötigt für Ihren Anwendungsfall useEffectein Abhängigkeitsarray, um Änderungen zu verfolgen, und basierend auf der Abhängigkeit kann bestimmt werden, ob erneut gerendert werden soll oder nicht. Es wird immer empfohlen, ein Abhängigkeitsarray an zu übergebenuseEffect . Bitte beachten Sie den folgenden Code:

Ich habe useCallbackHaken eingeführt .

const { useCallback, useState, useEffect } = React;

  const [userText, setUserText] = useState("");

  const handleUserKeyPress = useCallback(event => {
    const { key, keyCode } = event;

    if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
      setUserText(prevUserText => `${prevUserText}${key}`);
    }
  }, []);

  useEffect(() => {
    window.addEventListener("keydown", handleUserKeyPress);

    return () => {
      window.removeEventListener("keydown", handleUserKeyPress);
    };
  }, [handleUserKeyPress]);

  return (
    <div>
      <blockquote>{userText}</blockquote>
    </div>
  );

Bearbeiten Sie q98jov5kvq

Codejockie
quelle
Ich habe Ihre Lösung ausprobiert, aber es ist genau das gleiche wie [userText]oder ohne zweites Argument. Grundsätzlich setzen wir ein console.logInnen useEffect, wir werden sehen, dass die Protokollierung jedes Re-Rendering auslöst, was auch bedeutet, dass addEventListenderjedes Re-Rendering ausgeführt wird
Isaac
Ich möchte glauben, dass dies ein erwartetes Verhalten ist. Ich habe meine Antwort aktualisiert.
Codejockie
In Ihrer Sandbox haben Sie eine Anweisung console.log('>');in useEffectHooks eingefügt. Wenn Sie Ihren aktualisierten Code verwenden, wird dieser immer noch protokolliert. Dies bedeutet auch, dass die Ereignisse bei jedem erneuten Rendern registriert werden
Isaac
1
aber wegen return () => {window.removeEventListener('keydown', handleUserKeyPress)}, bei jedem erneuten Rendern wird die Komponente registriert und abgemeldet
Isaac
1
Genau das Verhalten, das ich mir gewünscht habe, aber Sie können es beobachten @ Codesandbox.io/s/n5j7qy051j
Isaac
0

Sie benötigen eine Möglichkeit, den vorherigen Status zu verfolgen. useStatehilft Ihnen, nur den aktuellen Status zu verfolgen. In den Dokumenten können Sie mithilfe eines anderen Hooks auf den alten Status zugreifen.

const prevRef = useRef();
useEffect(() => {
  prevRef.current = userText;
});

Ich habe Ihr Beispiel aktualisiert, um dies zu verwenden. Und es klappt.

const { useState, useEffect, useRef } = React;

const App = () => {
  const [userText, setUserText] = useState("");
  const prevRef = useRef();
  useEffect(() => {
    prevRef.current = userText;
  });

  const handleUserKeyPress = event => {
    const { key, keyCode } = event;

    if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
      setUserText(`${prevRef.current}${key}`);
    }
  };

  useEffect(() => {
    window.addEventListener("keydown", handleUserKeyPress);

    return () => {
      window.removeEventListener("keydown", handleUserKeyPress);
    };
  }, []);

  return (
    <div>
      <h1>Feel free to type!</h1>
      <blockquote>{userText}</blockquote>
    </div>
  );
};

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

maazadeeb
quelle
-1

Beim zweiten Ansatz wird das useEffectnur einmal gebunden und daher wird das userTextnie aktualisiert. Ein Ansatz wäre, eine lokale Variable beizubehalten, die userTextbei jedem Tastendruck zusammen mit dem Objekt aktualisiert wird.

  const [userText, setUserText] = useState('');
  let local_text = userText
  const handleUserKeyPress = event => {
    const { key, keyCode } = event;

    if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
      local_text = `${userText}${key}`;
      setUserText(local_text);
    }
  };

  useEffect(() => {
    window.addEventListener('keydown', handleUserKeyPress);

    return () => {
      window.removeEventListener('keydown', handleUserKeyPress);
    };
  }, []);

  return (
    <div>
      <h1>Feel free to type!</h1>
      <blockquote>{userText}</blockquote>
    </div>
  );

Persönlich mag ich die Lösung nicht, fühle anti-reactmich und ich denke, die erste Methode ist gut genug und wurde entwickelt, um auf diese Weise verwendet zu werden.

varun agarwal
quelle
Haben Sie etwas dagegen, Code einzufügen, um zu demonstrieren, wie ich mein Ziel in der zweiten Methode erreichen kann?
Isaac
-1

Sie haben keinen Zugriff auf den geänderten useText-Status. Sie können es dem vorherigen Status zuordnen. Speichern Sie den Zustand in einer Variablen, zB: Zustand wie folgt:

const App = () => {
  const [userText, setUserText] = useState('');

  useEffect(() => {
    let state = ''

    const handleUserKeyPress = event => {
      const { key, keyCode } = event;
      if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
        state += `${key}`
        setUserText(state);
      }   
    };  
    window.addEventListener('keydown', handleUserKeyPress);
    return () => {
      window.removeEventListener('keydown', handleUserKeyPress);
    };  
  }, []);

  return (
    <div>
      <h1>Feel free to type!</h1>
      <blockquote>{userText}</blockquote>
    </div>
  );  
};
di3
quelle