Drei fehlerhafte React-Codebeispiele und wie man sie behebt

Avatar of Hugh Haworth
Hugh Haworth am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Normalerweise gibt es mehr als eine Möglichkeit, etwas in React zu codieren. Und während es möglich ist, dasselbe auf verschiedene Arten zu erstellen, kann es ein oder zwei Ansätze geben, die technisch gesehen „besser“ funktionieren als andere. Ich stoße tatsächlich auf viele Beispiele, bei denen der Code, der zum Erstellen einer React-Komponente verwendet wird, technisch „korrekt“ ist, aber Probleme aufwirft, die völlig vermeidbar sind.

Lassen Sie uns also einige dieser Beispiele betrachten. Ich werde drei Fälle von „fehlerhaftem“ React-Code vorstellen, der für eine bestimmte Situation technisch gesehen die Aufgabe erfüllt, und Wege, wie er verbessert werden kann, um wartungsfreundlicher, widerstandsfähiger und letztendlich funktionaler zu sein.

Dieser Artikel setzt einige Kenntnisse von React Hooks voraus. Es ist keine Einführung in Hooks – eine gute Einführung finden Sie bei Kingsley Silas auf CSS Tricks, oder werfen Sie einen Blick auf die React-Dokumentation, um sich damit vertraut zu machen. Wir werden uns auch nicht mit all den aufregenden neuen Dingen befassen, die in React 18 kommen. Stattdessen werden wir uns einige subtile Probleme ansehen, die Ihre Anwendung nicht vollständig zum Absturz bringen, aber in Ihren Codebestand schleichen und seltsames oder unerwartetes Verhalten verursachen können, wenn Sie nicht vorsichtig sind.

Fehlerhafter Code #1: Zustands- und Prop-Mutation

Es ist ein großes Anti-Pattern, Zustand oder Props in React zu mutieren. Tun Sie das nicht!

Dies ist keine revolutionäre Ratschlag – es ist normalerweise eines der ersten Dinge, die Sie lernen, wenn Sie mit React beginnen. Aber Sie denken vielleicht, Sie können damit davonkommen (weil es in einigen Fällen so aussieht, als ob Sie es *können*).

Ich werde Ihnen zeigen, wie Fehler in Ihren Code schleichen können, wenn Sie Props mutieren. Manchmal möchten Sie eine Komponente erstellen, die eine transformierte Version einiger Daten anzeigt. Lassen Sie uns eine Elternkomponente erstellen, die einen Zähler im Zustand speichert, und einen Button, der ihn inkrementiert. Wir erstellen auch eine Kindkomponente, die den Zähler über Props empfängt und anzeigt, wie der Zähler aussehen würde, wenn 5 hinzugefügt werden.

Hier ist ein Pen, der einen naiven Ansatz demonstriert

Dieses Beispiel funktioniert. Es tut, was wir wollen: Wir klicken auf den Inkrement-Button und er fügt dem Zähler eins hinzu. Dann wird die Kindkomponente neu gerendert, um anzuzeigen, wie der Zähler aussehen würde, wenn 5 hinzugefügt werden. Wir haben die Props in der Kindkomponente hier geändert und es funktioniert gut! Warum hat uns jeder erzählt, dass die Mutation von Props so schlecht ist?

Nun, was ist, wenn wir später den Code refaktorisieren und den Zähler in einem Objekt speichern müssen? Dies könnte passieren, wenn wir weitere Eigenschaften im selben useState Hook speichern müssen, wenn unsere Codebasis größer wird.

Anstatt die im Zustand befindliche Zahl zu inkrementieren, inkrementieren wir die count-Eigenschaft eines im Zustand befindlichen Objekts. In unserer Kindkomponente empfangen wir das Objekt über Props und addieren zur count-Eigenschaft, um anzuzeigen, wie der Zähler aussehen würde, wenn wir 5 hinzufügen würden.

Sehen wir, wie das läuft. Versuchen Sie, den Zustand in diesem Pen ein paar Mal zu inkrementieren

Oh nein! Jetzt, wenn wir den Zähler inkrementieren, scheint er bei jedem Klick 6 hinzuzufügen! Warum passiert das? Das Einzige, was sich zwischen diesen beiden Beispielen geändert hat, ist, dass wir ein Objekt anstelle einer Zahl verwendet haben!

Erfahrenere JavaScript-Programmierer werden wissen, dass der große Unterschied darin besteht, dass primitive Typen wie Zahlen, Booleans und Zeichenketten unveränderlich sind und nach Wert übergeben werden, während Objekte per Referenz übergeben werden.

Das bedeutet, dass

  • Wenn Sie eine *Zahl* in eine Variable legen, einer anderen Variable zuweisen und dann die zweite Variable ändern, wird die erste Variable nicht geändert.
  • Wenn Sie ein *Objekt* in eine Variable legen, einer anderen Variable zuweisen und dann die zweite Variable ändern, *wird* die erste Variable geändert.

Wenn die Kindkomponente eine Eigenschaft des Zustands-Objekts ändert, fügt sie 5 zu demselben Objekt hinzu, das React beim Aktualisieren des Zustands verwendet. Das bedeutet, dass, wenn unsere Inkrement-Funktion nach einem Klick ausgelöst wird, React dasselbe Objekt verwendet, *nachdem* es von unserer Kindkomponente manipuliert wurde, was sich als Hinzufügen von 6 bei jedem Klick zeigt.

Die Lösung

Es gibt mehrere Möglichkeiten, diese Probleme zu vermeiden. Für eine so einfache Situation können Sie jede Mutation vermeiden und die Änderung in einer Render-Funktion ausdrücken

function Child({state}){
  return <div><p>count + 5 = {state.count + 5} </p></div>
}

In einem komplizierteren Fall müssen Sie jedoch möglicherweise state.count + 5 mehrmals wiederverwenden oder die transformierten Daten an mehrere Kinder übergeben.

Eine Möglichkeit, dies zu tun, ist, eine Kopie des Props in der Kindkomponente zu erstellen und dann die Eigenschaften der geklonten Daten zu transformieren. Es gibt ein paar verschiedene Möglichkeiten, Objekte in JavaScript zu klonen, mit verschiedenen Kompromissen. Sie können Objektliterale und Spread-Syntax verwenden

function Child({state}){
const copy = {...state};
  return <div><p>count + 5 = {copy.count + 5} </p></div>
}

Aber wenn es verschachtelte Objekte gibt, werden diese immer noch auf die alte Version verweisen. Stattdessen könnten Sie das Objekt in JSON konvertieren und es dann sofort parsen

JSON.parse(JSON.stringify(myobject))

Dies funktioniert für die meisten einfachen Objekttypen. Aber wenn Ihre Daten exotischere Typen verwenden, möchten Sie vielleicht eine Bibliothek verwenden. Eine beliebte Methode wäre die Verwendung von lodash's deepClone. Hier ist ein Pen, der eine behobene Version zeigt, die Objektliterale und Spread-Syntax verwendet, um das Objekt zu klonen

Eine weitere Option ist die Verwendung einer Bibliothek wie Immutable.js. Wenn Sie eine Regel haben, nur unveränderliche Datenstrukturen zu verwenden, können Sie darauf vertrauen, dass Ihre Daten nicht unerwartet mutiert werden. Hier ist ein weiteres Beispiel, das die unveränderliche Map-Klasse verwendet, um den Zustand der Zähler-App darzustellen

Fehlerhafter Code #2: Abgeleiteter Zustand

Nehmen wir an, wir haben eine Eltern- und eine Kindkomponente. Beide haben useState Hooks, die einen Zähler halten. Und nehmen wir an, die Elternkomponente übergibt ihren Zustand als Prop an die Kindkomponente, die die Kindkomponente zur Initialisierung ihres Zählers verwendet.

function Parent(){
  const [parentCount,setParentCount] = useState(0);
  return <div>
    <p>Parent count: {parentCount}</p>
    <button onClick={()=>setParentCount(c=>c+1)}>Increment Parent</button>
    <Child parentCount={parentCount}/>
  </div>;
}

function Child({parentCount}){
 const [childCount,setChildCount] = useState(parentCount);
  return <div>
    <p>Child count: {childCount}</p>
    <button onClick={()=>setChildCount(c=>c+1)}>Increment Child</button>
  </div>;
}

Was passiert mit dem Zustand der Kindkomponente, wenn sich der Zustand der Elternkomponente ändert und die Kindkomponente mit neuen Props neu gerendert wird? Bleibt der Zustand der Kindkomponente gleich oder ändert er sich, um den neuen Zähler widerzuspiegeln, der an sie übergeben wurde?

Wir haben es mit einer Funktion zu tun, also sollte der Zustand der Kindkomponente gelöscht und ersetzt werden, richtig? Falsch! Der Zustand der Kindkomponente hat Vorrang vor der neuen Prop von der Elternkomponente. Nachdem der Zustand der Kindkomponente im ersten Rendering initialisiert wurde, ist er vollständig unabhängig von den Props, die er empfängt.

React speichert den Zustand der Komponenten für jede Komponente im Baum und der Zustand wird nur gelöscht, wenn die Komponente entfernt wird. Andernfalls wird der Zustand nicht von neuen Props beeinflusst.

Die Verwendung von Props zur Initialisierung des Zustands wird als „abgeleiteter Zustand“ bezeichnet und ist ein ziemliches Anti-Pattern. Es nimmt den Vorteil, dass eine Komponente eine einzige Quelle der Wahrheit für ihre Daten hat.

Verwendung der Key-Prop

Aber was ist, wenn wir eine Sammlung von Elementen haben, die wir mit demselben Typ von Kindkomponente bearbeiten möchten, und wir möchten, dass die Kindkomponente einen Entwurf des bearbeiteten Elements speichert? Wir müssten den Zustand der Kindkomponente jedes Mal zurücksetzen, wenn wir Elemente aus der Sammlung wechseln.

Hier ist ein Beispiel: Schreiben wir eine App, in der wir täglich eine Liste von fünf Dingen aufschreiben können, für die wir jeden Tag dankbar sind. Wir verwenden eine Elternkomponente mit einem Zustand, der als leeres Array initialisiert ist und das wir mit fünf Zeichenketten füllen werden.

Dann haben wir eine Kindkomponente mit einem Texteingabefeld, um unsere Aussage einzugeben.

Wir werden einen kriminellen Grad an Über-Engineering in unserer winzigen App anwenden, aber es dient dazu, ein Muster zu veranschaulichen, das Sie in einem komplexeren Projekt benötigen könnten: Wir werden den Entwurfszustand des Texteingabefelds in der Kindkomponente speichern.

Das Absenken des Zustands in die Kindkomponente kann eine Leistungsoptimierung sein, um zu verhindern, dass die Elternkomponente bei Zustandsänderungen neu gerendert wird. Andernfalls rendert sich die Elternkomponente bei jeder Änderung im Texteingabefeld neu.

Wir werden auch eine Beispiel-Aussage als Standardwert für jede der fünf Notizen übergeben, die wir schreiben werden.

Hier ist eine fehlerhafte Methode, dies zu tun

// These are going to be our default values for each of the five notes
// To give the user an idea of what they might write
const ideaList = ["I'm thankful for my friends",
                  "I'm thankful for my family",
                  "I'm thankful for my health",
                  "I'm thankful for my hobbies",
                  "I'm thankful for CSS Tricks Articles"]

const maxStatements = 5;

function Parent(){
  const [list,setList] = useState([]);
  
  // Handler function for when the statement is completed
  // Sets state providing a new array combining the current list and the new item 
  function onStatementComplete(payload){
    setList(list=>[...list,payload]);
  }
  // Function to reset the list back to an empty array
   function reset(){
    setList([]);
  }
  return <div>
    <h1>Your thankful list</h1>
    <p>A five point list of things you're thankful for:</p>

    {/* First we list the statements that have been completed*/}
    {list.map((item,index)=>{return <p>Item {index+1}: {item}</p>})}

    {/* If the length of the list is under our max statements length, we render 
    the statement form for the user to enter a new statement.
    We grab an example statement from the idealist and pass down the onStatementComplete function.
    Note: This implementation won't work as expected*/}
    {list.length<maxStatements ? 
      <StatementForm initialStatement={ideaList[list.length]} onStatementComplete={onStatementComplete}/>
      :<button onClick={reset}>Reset</button>
    }
  </div>;
}

// Our child StatementForm component This accepts the example statement for it's initial state and the on complete function
function StatementForm({initialStatement,onStatementComplete}){
   // We hold the current state of the input, and set the default using initialStatement prop
 const [statement,setStatement] = useState(initialStatement);

  return <div>
    {/*On submit we prevent default and fire the onStatementComplete function received via props*/}
    <form onSubmit={(e)=>{e.preventDefault(); onStatementComplete(statement)}}>
    <label htmlFor="statement-input">What are you thankful for today?</label><br/>
    {/* Our controlled input below*/}
    <input id="statement-input" onChange={(e)=>setStatement(e.target.value)} value={statement} type="text"/>
    <input type="submit"/>
      </form>
  </div>
}

Hier gibt es ein Problem: Jedes Mal, wenn wir eine abgeschlossene Aussage absenden, behält das Eingabefeld fälschlicherweise die übermittelte Notiz im Textfeld. Wir möchten sie durch eine Beispiel-Aussage aus unserer Liste ersetzen.

Obwohl wir jedes Mal einen anderen Beispielstring übergeben, erinnert sich die Kindkomponente an den alten Zustand und unsere neuere Prop wird ignoriert. Sie könnten möglicherweise prüfen, ob sich die Props bei jedem Rendering in einem useEffect geändert haben, und dann den Zustand zurücksetzen, wenn dies der Fall ist. Das kann jedoch zu Fehlern führen, wenn verschiedene Teile Ihrer Daten dieselben Werte verwenden und Sie den Zustand der Kindkomponente zurücksetzen möchten, obwohl die Prop gleich bleibt.

Die Lösung

Wenn Sie eine Kindkomponente benötigen, bei der die Elternkomponente die Kindkomponente nach Bedarf zurücksetzen kann, gibt es eine Möglichkeit, dies zu tun: Sie ändern die key-Prop der Kindkomponente.

Sie haben diese spezielle key-Prop vielleicht schon beim Rendern von Elementen aus einem Array gesehen und React gibt eine Warnung aus, dass Sie für jedes Element einen Schlüssel angeben sollen. Das Ändern des Schlüssels eines Kindelements stellt sicher, dass React eine brandneue Version des Elements erstellt. Es ist eine Möglichkeit, React mitzuteilen, dass Sie ein konzeptionell anderes Element mit derselben Komponente rendern.

Fügen wir unserer Kindkomponente eine key-Prop hinzu. Der Wert ist der Index, den wir mit unserer Aussage füllen werden

<StatementForm key={list.length} initialStatement={ideaList[list.length]} onStatementComplte={onStatementComplete}/>

So sieht das in unserer Listen-App aus

Beachten Sie, dass sich hier nur die Kindkomponente jetzt eine key-Prop hat, die auf dem Array-Index basiert, den wir gerade füllen. Dennoch hat sich das Verhalten der Komponente vollständig geändert.

Jetzt wird jedes Mal, wenn wir eine Aussage absenden und fertigschreiben, der alte Zustand in der Kindkomponente verworfen und durch die Beispiel-Aussage ersetzt.

Fehlerhafte Code #3: Veraltete Closure-Bugs

Dies ist ein häufiges Problem bei React Hooks. Zuvor gab es einen CSS-Tricks-Artikel über den Umgang mit veralteten Props und Zuständen in Reacts funktionalen Komponenten.

Werfen wir einen Blick auf einige Situationen, in denen Sie in Schwierigkeiten geraten könnten. Die erste tritt auf, wenn Sie useEffect verwenden. Wenn wir innerhalb von useEffect etwas Asynchrones tun, können wir Probleme mit altem Zustand oder Props bekommen.

Hier ist ein Beispiel. Wir müssen einen Zähler jede Sekunde inkrementieren. Wir richten ihn beim ersten Rendering mit einem useEffect ein, das eine Closure bereitstellt, die den Zähler als erstes Argument inkrementiert, und ein leeres Array als zweites Argument. Wir geben ihm das leere Array, da wir nicht möchten, dass React das Intervall bei jedem Rendering neu startet.

function Counter() { 
  let [count, setCount] = useState(0);

  useEffect(() => {
    let id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  },[]);

  return <h1>{count}</h1>;
}

Oh nein! Der Zähler steigt auf 1, aber ändert sich danach nie wieder! Warum passiert das?

Es hat mit zwei Dingen zu tun

Wenn wir uns die MDN-Dokumentation zu Closures ansehen, sehen wir

Eine Closure ist die Kombination aus einer Funktion und der lexikalischen Umgebung, in der diese Funktion deklariert wurde. Diese Umgebung besteht aus allen lokalen Variablen, die zum Zeitpunkt der Erstellung der Closure im Geltungsbereich waren.

Die „lexikalische Umgebung“, in der unsere useEffect-Closure deklariert wird, befindet sich innerhalb unserer Counter React-Komponente. Die lokale Variable, an der wir interessiert sind, ist count, die zum Zeitpunkt der Deklaration (des ersten Renderings) Null ist.

Das Problem ist, dass diese Closure nie wieder deklariert wird. Wenn der Zähler zum Zeitpunkt der Deklaration Null ist, wird er immer Null sein. Jedes Mal, wenn das Intervall ausgelöst wird, wird eine Funktion ausgeführt, die mit einem Zähler von Null beginnt und ihn auf 1 inkrementiert.

Wie könnten wir also die Funktion erneut deklarieren lassen? Hier kommt das zweite Argument des useEffect-Aufrufs ins Spiel. Wir dachten, wir wären extrem clever, indem wir das Intervall nur einmal mit dem leeren Array starten, aber damit haben wir uns ins eigene Fleisch geschnitten. Hätten wir dieses Argument weggelassen, würde die Closure innerhalb von useEffect jedes Mal mit einem neuen Zähler neu deklariert werden.

Die Art und Weise, wie ich darüber denke, ist, dass das useEffect-Abhängigkeitsarray zwei Dinge tut

  • Es löst die useEffect-Funktion aus, wenn sich die Abhängigkeit ändert.
  • Es deklariert die Closure auch mit der aktualisierten Abhängigkeit neu und schützt die Closure so vor veraltetem Zustand oder Props.

Tatsächlich gibt es sogar eine Lint-Regel, um Ihre useEffect-Instanzen vor veraltetem Zustand und Props zu schützen, indem sichergestellt wird, dass Sie die richtigen Abhängigkeiten zum zweiten Argument hinzufügen.

Aber wir wollen unser Intervall auch nicht bei jedem Rendering der Komponente zurücksetzen. Wie lösen wir dieses Problem also?

Die Lösung

Auch hier gibt es mehrere Lösungen für unser Problem. Beginnen wir mit der einfachsten: Wir verwenden den Zähler-Zustand überhaupt nicht und übergeben stattdessen eine Funktion an unseren setState-Aufruf

function Counter() { 
  let [count, setCount] = useState(0);

  useEffect(() => {
    let id = setInterval(() => {
      setCount(prevCount => prevCount+ 1);
    }, 1000);
    return () => clearInterval(id);
  },[]);

  return <h1>{count}</h1>;
}

Das war einfach. Eine weitere Option ist die Verwendung des useRef Hook wie folgt, um eine veränderliche Referenz auf den Zähler zu halten

function Counter() {
  let [count, setCount] = useState(0);
  const countRef = useRef(count)
  
  function updateCount(newCount){
    setCount(newCount);
    countRef.current = newCount;
  }

  useEffect(() => {
    let id = setInterval(() => {
      updateCount(countRef.current + 1);
    }, 1000);
    return () => clearInterval(id);
  },[]);

  return <h1>{count}</h1>;
}

ReactDOM.render(<Counter/>,document.getElementById("root"))

Um tiefer in die Verwendung von Intervallen und Hooks einzutauchen, können Sie sich diesen Artikel über die Erstellung eines useInterval in React von Dan Abramov, einem Mitglied des React-Kernteams, ansehen. Er verfolgt einen anderen Weg, bei dem er anstelle des Zählers in einer ref die gesamte Closure in einer ref platziert.

Um tiefer in useEffect einzutauchen, können Sie sich seinen Beitrag zu useEffect ansehen.

Weitere veraltete Closure-Bugs

Aber veraltete Closures treten nicht nur in useEffect auf. Sie können auch in Event-Handlern und anderen Closures innerhalb Ihrer React-Komponenten auftreten. Schauen wir uns eine React-Komponente mit einem veralteten Event-Handler an. Wir erstellen eine Scroll-Fortschrittsleiste, die Folgendes tut

  • erhöht ihre Breite entlang des Bildschirms, während der Benutzer scrollt
  • beginnt transparent und wird mit zunehmendem Scrollen immer undurchsichtiger
  • bietet dem Benutzer einen Button, der die Farbe der Scrollbar zufällig ändert

Wir werden die Fortschrittsleiste außerhalb des React-Baums belassen und sie im Event-Handler aktualisieren. Hier ist unsere fehlerhafte Implementierung

<body>
<div id="root"></div>
<div id="progress"></div>
</body>
function Scroller(){

  // We'll hold the scroll position in one state
  const [scrollPosition, setScrollPosition] = useState(window.scrollY);
  // And the current color in another
  const [color,setColor] = useState({r:200,g:100,b:100});
  
  // We assign out scroll listener on the first render
  useEffect(()=>{
   document.addEventListener("scroll",handleScroll);
    return ()=>{document.removeEventListener("scroll",handleScroll);}
  },[]);
  
  // A function to generate a random color. To make sure the contrast is strong enough
  // each value has a minimum value of 100
  function onColorChange(){
    setColor({r:100+Math.random()*155,g:100+Math.random()*155,b:100+Math.random()*155});
  }
  
  // This function gets called on the scroll event
  function handleScroll(e){
    // First we get the value of how far down we've scrolled
    const scrollDistance = document.body.scrollTop || document.documentElement.scrollTop;
    // Now we grab the height of the entire document
    const documentHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight;
     // And use these two values to figure out how far down the document we are
    const percentAlong =  (scrollDistance / documentHeight);
    // And use these two values to figure out how far down the document we are
    const progress = document.getElementById("progress");
    progress.style.width = `${percentAlong*100}%`;
    // Here's where our bug is. Resetting the color here will mean the color will always 
    // be using the original state and never get updated
    progress.style.backgroundColor = `rgba(${color.r},${color.g},${color.b},${percentAlong})`;
    setScrollPosition(percentAlong);
  }
  
  return <div className="scroller" style={{backgroundColor:`rgb(${color.r},${color.g},${color.b})`}}>
    <button onClick={onColorChange}>Change color</button>
    <span class="percent">{Math.round(scrollPosition* 100)}%</span>
  </div>
}

ReactDOM.render(<Scroller/>,document.getElementById("root"))

Unsere Leiste wird breiter und mit zunehmender Opazität, während die Seite scrollt. Aber wenn Sie auf den Button „Farbe ändern“ klicken, wirken sich unsere zufälligen Farben nicht auf die Fortschrittsleiste aus. Wir erhalten diesen Fehler, weil die Closure vom Komponentenstatus betroffen ist und diese Closure nie neu deklariert wird, sodass wir nur den ursprünglichen Wert des Status erhalten und keine Updates.

Sie können sehen, wie das Einrichten von Closures, die externe APIs unter Verwendung von React-Status oder Komponenten-Props aufrufen, Ihnen Probleme bereiten kann, wenn Sie nicht vorsichtig sind.

Die Lösung

Auch hier gibt es mehrere Möglichkeiten, dieses Problem zu beheben. Wir könnten den Farbstatus in einer veränderlichen Referenz speichern, die wir später in unserem Event-Handler verwenden könnten

const [color,setColor] = useState({r:200,g:100,b:100});
const colorRef = useRef(color);

function onColorChange(){
  const newColor = {r:100+Math.random()*155,g:100+Math.random()*155,b:100+Math.random()*155};
  setColor(newColor);
  colorRef.current=newColor;
  progress.style.backgroundColor = `rgba(${newColor.r},${newColor.g},${newColor.b},${scrollPosition})`;
}

Das funktioniert gut genug, aber es fühlt sich nicht ideal an. Möglicherweise müssen Sie Code wie diesen schreiben, wenn Sie mit Drittanbieterbibliotheken arbeiten und keinen Weg finden, ihre API in Ihren React-Baum zu integrieren. Aber indem wir eines unserer Elemente aus dem React-Baum nehmen und es in unserem Event-Handler aktualisieren, schwimmen wir gegen den Strom.

Dies ist jedoch eine einfache Lösung, da wir nur mit der DOM-API arbeiten. Ein einfacher Weg, dies zu refaktorisieren, ist, die Fortschrittsleiste in unseren React-Baum aufzunehmen und sie in JSX zu rendern, sodass sie auf den Zustand der Komponente verweisen kann. Jetzt können wir die Event-Handling-Funktion ausschließlich zur Aktualisierung des Zustands verwenden.

function Scroller(){
  const [scrollPosition, setScrollPosition] = useState(window.scrollY);
  const [color,setColor] = useState({r:200,g:100,b:100});  

  useEffect(()=>{
   document.addEventListener("scroll",handleScroll);
    return ()=>{document.removeEventListener("scroll",handleScroll);}
  },[]);
  
  function onColorChange(){
    const newColor = {r:100+Math.random()*155,g:100+Math.random()*155,b:100+Math.random()*155};
    setColor(newColor);
  }

  function handleScroll(e){
    const scrollDistance = document.body.scrollTop || document.documentElement.scrollTop;
    const documentHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight;
    const percentAlong =  (scrollDistance / documentHeight);
    setScrollPosition(percentAlong);
  }
  return <>
    <div class="progress" id="progress"
   style={{backgroundColor:`rgba(${color.r},${color.g},${color.b},${scrollPosition})`,width: `${scrollPosition*100}%`}}></div>
    <div className="scroller" style={{backgroundColor:`rgb(${color.r},${color.g},${color.b})`}}>
    <button onClick={onColorChange}>Change color</button>
    <span class="percent">{Math.round(scrollPosition * 100)}%</span>
  </div>
  </>
}

Das fühlt sich besser an. Nicht nur haben wir die Chance auf veraltete Event-Handler beseitigt, sondern wir haben auch unsere Fortschrittsleiste in eine in sich geschlossene Komponente umgewandelt, die die deklarative Natur von React nutzt.

Außerdem benötigen Sie für eine Scroll-Anzeige wie diese möglicherweise nicht einmal JavaScript – werfen Sie einen Blick auf das aufkommende @scroll-timeline CSS Funktion oder einen Ansatz mit einem Farbverlauf aus Chris' Buch über die größten CSS-Tricks!

Zusammenfassung

Wir haben uns drei verschiedene Arten angesehen, wie Sie Fehler in Ihren React-Anwendungen erstellen können, und einige Möglichkeiten, sie zu beheben. Es kann leicht sein, sich Gegenbeispiele anzusehen, die einen perfekten Pfad befolgen und keine Feinheiten in den APIs zeigen, die Probleme verursachen könnten.

Wenn Sie immer noch ein stärkeres mentales Modell davon aufbauen müssen, was Ihr React-Code tut, finden Sie hier eine Liste von Ressourcen, die Ihnen helfen können