Kapitel 4. Funktionales Excel
Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com
Erinnerst du dich an Funktionskomponenten? Irgendwann in Kapitel 2, als der Zustand ins Spiel kam, wurden die Funktionskomponenten aus der Diskussion gestrichen. Es ist an der Zeit, sie wieder ins Spiel zu bringen.
Eine kurze Auffrischung: Funktion versus Klassenkomponenten
In ihrer einfachsten Form braucht eine Klassenkomponente nur eine render()
Methode. Hier baust du die Benutzeroberfläche auf, optional mit this.props
und this.state
:
class
Widget
extends
React
.
Component
{
render
()
{
let
ui
;
// fun with this.props and this.state
return
<
div
>
{
ui
}
</
div
>;
}
}
In einer Funktionskomponente ist die gesamte Komponente die Funktion und die Benutzeroberfläche ist das, was die Funktion zurückgibt. Die Requisiten werden an die Funktion übergeben, wenn die Komponente erstellt wird:
function
Widget
(
props
)
{
let
ui
;
// fun with props but where's the state?
return
<
div
>
{
ui
}
</
div
>;
}
Die Nützlichkeit von Funktionskomponenten endete mit React v16.8: Du kannst sie nur für Komponenten verwenden, die keinen Zustand verwalten(zustandslose Komponenten). Mit der Einführung von Hooks in Version 16.8 ist es nun möglich, Funktionskomponenten überall zu verwenden. Im weiteren Verlauf dieses Kapitels wirst du sehen, wie die Komponente Excel
aus Kapitel 3 als Funktionskomponente implementiert werden kann.
Rendering der Daten
Der erste Schritt besteht darin, die an die Komponente übergebenen Daten zu rendern(Abbildung 4-1). Wie die Komponente verwendet wird, ändert sich nicht. Mit anderen Worten: Ein Entwickler, der deine Komponente verwendet, muss nicht wissen, ob es sich um eine Klassen- oder eine Funktionskomponente handelt. Die Requisiten von initialData
und headers
sehen gleich aus. Sogar die propTypes
Definitionen sind identisch.
function
Excel
(
props
)
{
// implement me...
}
Excel
.
propTypes
=
{
headers
:
PropTypes
.
arrayOf
(
PropTypes
.
string
)
,
initialData
:
PropTypes
.
arrayOf
(
PropTypes
.
arrayOf
(
PropTypes
.
string
))
,
};
const
headers
=
[
'Book'
,
'Author'
,
'Language'
,
'Published'
,
'Sales'
];
const
data
=
[
[
'A Tale of Two Cities'
,
'Charles Dickens'
,
// ...
]
,
// ...
];
ReactDOM
.
render
(
<
Excel
headers
=
{
headers
}
initialData
=
{
data
}
/>,
document
.
getElementById
(
'app'
)
,
);
Die Implementierung des Körpers der Funktionskomponente besteht im Wesentlichen aus dem Kopieren des Körpers der render()
Methode der Klassenkomponente:
function
Excel
({
headers
,
initialData
})
{
return
(
<
table
>
<
thead
>
<
tr
>
{
headers
.
map
((
title
,
idx
)
=>
(
<
th
key
=
{
idx
}
>
{
title
}
</
th
>
))}
</
tr
>
</
thead
>
<
tbody
>
{
initialData
.
map
((
row
,
idx
)
=>
(
<
tr
key
=
{
idx
}
>
{
row
.
map
((
cell
,
idx
)
=>
(
<
td
key
=
{
idx
}
>
{
cell
}
</
td
>
))}
</
tr
>
))}
</
tbody
>
</
table
>
);
}
Im obigen Code kannst du sehen, dass du anstelle von function Excel(props){}
die Destrukturierungssyntax function Excel({headers, initialData}){}
verwenden kannst, um dir später die Eingabe von props.headers
und props.initialData
zu ersparen.
Der Staatshaken
Um den Zustand in deinen Funktionskomponenten zu erhalten, brauchst du Hooks. Was ist ein Hook? Eine Funktion mit dem vorangestellten Wort use*
, mit der du verschiedene React-Funktionen nutzen kannst, z. B. Werkzeuge zur Verwaltung von Zuständen und Lebenszyklen von Komponenten. Du kannst auch deine eigenen Hooks erstellen. Am Ende dieses Kapitels lernst du, wie du verschiedene eingebaute Hooks nutzen und deine eigenen schreiben kannst.
Beginnen wir mit dem State Hook. Es handelt sich um eine Funktion namens useState()
, die als Eigenschaft des React
Objekts (React.useState()
) verfügbar ist. Sie nimmt einen Wert an, den Anfangswert einer Zustandsvariablen (ein Teil der Daten, die du verwalten willst), und gibt ein Array mit zwei Elementen (ein Tupel) zurück. Das erste Element ist die Zustandsvariable und das zweite ist eine Funktion, mit der du diese Variable ändern kannst. Schauen wir uns ein Beispiel an.
In einer Klassenkomponente definierst du in der constructor()
den Anfangswert wie folgt:
this
.
state
=
{
data
:
initialData
;
};
Wenn du später den Status von data
ändern möchtest, kannst du stattdessen Folgendes tun:
this
.
setState
({
data
:
newData
,
});
In einer Funktion Komponente definierst du den Ausgangszustand und erhältst eine Aktualisierungsfunktion:
const
[
data
,
setData
]
=
React
.
useState
(
initialData
);
Hinweis
Beachte die Destrukturierungssyntax des Arrays , bei der du die beiden Elemente des Arrays, das von useState()
zurückgegeben wird, zwei Variablen zuweist: data
und setData
. Das ist ein kürzerer und saubererer Weg, um die beiden Rückgabewerte zu erhalten, als wenn du z. B:
const
stateArray
=
React
.
useState
(
initialData
);
const
data
=
stateArray
[
0
];
const
setData
=
stateArray
[
1
];
Zum Rendern kannst du jetzt die Variable data
verwenden. Wenn du diese Variable aktualisieren willst, benutze:
setData
(
newData
);
Wenn du die Komponente so umschreibst, dass sie den State Hook verwendet, kann das folgendermaßen aussehen:
function
Excel
({
headers
,
initialData
})
{
const
[
data
,
setData
]
=
React
.
useState
(
initialData
);
return
(
<
table
>
<
thead
>
<
tr
>
{
headers
.
map
((
title
,
idx
)
=>
(
<
th
key
=
{
idx
}
>
{
title
}
</
th
>
))}
</
tr
>
</
thead
>
<
tbody
>
{
data
.
map
((
row
,
idx
)
=>
(
<
tr
key
=
{
idx
}
>
{
row
.
map
((
cell
,
idx
)
=>
(
<
td
key
=
{
idx
}
>
{
cell
}
</
td
>
))}
</
tr
>
))}
</
tbody
>
</
table
>
);
}
Auch wenn dieses Beispiel (siehe 04.02.fn.table-state.html) nicht setData()
verwendet, kannst du sehen, wie es den data
Status verwendet. Gehen wir nun zum Sortieren der Tabelle über, wo du die Möglichkeit brauchst, den Status zu ändern.
Sortieren der Tabelle
In einer Komponente der Klasse werden alle verschiedenen Zustandsdaten in das Objekt this.state
gespeichert, ein Sammelsurium von oft zusammenhangslosen Informationen. Mit dem State Hook kannst du das Gleiche tun, aber du kannst auch entscheiden, ob du Teile des Zustands in verschiedenen Variablen speichern willst. Wenn es darum geht, eine Tabelle zu sortieren, ist die data
, die in der Tabelle enthalten ist, eine Information, während die sortierungsspezifischen Zusatzinformationen eine andere Information sind. Mit anderen Worten: Du kannst den State Hook so oft verwenden, wie du willst.
function
Excel
({
headers
,
initialData
})
{
const
[
data
,
setData
]
=
React
.
useState
(
initialData
);
const
[
sorting
,
setSorting
]
=
React
.
useState
({
column
:
null
,
descending
:
false
,
});
// ....
}
Das data
ist das, was du in der Tabelle anzeigst; das sorting
Objekt ist eine separate Angelegenheit. Es geht darum, wie du sortierst (aufsteigend oder absteigend) und nach welcher Spalte (Titel, Autor, etc.).
Die Funktion, die die Sortierung vornimmt, ist jetzt inline innerhalb der Funktion Excel
:
function
Excel
({
headers
,
initialData
})
{
// ..
function
sort
(
e
)
{
// implement me
}
return
(
<
table
>
{
/* ... */
}
</
table
>
);
}
Die Funktion sort()
findet heraus, nach welcher Spalte sortiert werden soll (anhand ihres Index) und ob die Sortierung absteigend ist:
const
column
=
e
.
target
.
cellIndex
;
const
descending
=
sorting
.
column
===
column
&&
!
sorting
.
descending
;
Dann wird das Array data
geklont, denn es ist immer noch eine schlechte Idee, den Zustand direkt zu ändern:
const
dataCopy
=
clone
(
data
);
Hinweis
Eine Erinnerung , dass die Funktion clone()
immer noch die schnelle und schmutzige JSON-Encodierung/Decodierung für das Deep Copying ist:
function
clone
(
o
)
{
return
JSON
.
parse
(
JSON
.
stringify
(
o
));
}
Die eigentliche Sortierung ist dieselbe wie zuvor:
dataCopy
.
sort
((
a
,
b
)
=>
{
if
(
a
[
column
]
===
b
[
column
])
{
return
0
;
}
return
descending
?
a
[
column
]
<
b
[
column
]
?
1
:
-
1
:
a
[
column
]
>
b
[
column
]
?
1
:
-
1
;
});
Und schließlich muss die Funktion sort()
die beiden Zustandsdaten mit den neuen Werten aktualisieren:
setData
(
dataCopy
);
setSorting
({
column
,
descending
});
Und das war's dann auch schon mit dem Sortieren. Jetzt muss nur noch die Benutzeroberfläche (der Rückgabewert der Funktion Excel()
) aktualisiert werden, um zu zeigen, welche Spalte für die Sortierung verwendet wird, und um Klicks auf eine der Überschriften zu verarbeiten:
<
thead
onClick
=
{
sort
}
>
<
tr
>
{
headers
.
map
((
title
,
idx
)
=>
{
if
(
sorting
.
column
===
idx
)
{
title
+=
sorting
.
descending
?
' \u2191'
:
' \u2193'
;
}
return
<
th
key
=
{
idx
}
>
{
title
}
</
th
>;
})}
</
tr
>
</
thead
>
Du kannst das Ergebnis mit dem Sortierpfeil in Abbildung 4-2 sehen.
Vielleicht hast du eine weitere nette Sache bei der Verwendung von State Hooks bemerkt: Du musst keine Callback-Funktionen binden, wie du es im Konstruktor einer Klassenkomponente tust. Nichts von dieser this.sort = this.sort.bind(this)
Angelegenheit. Kein this
, kein constructor()
. Eine Funktion ist alles, was du brauchst, um eine Komponente zu definieren.
Daten bearbeiten
Wie du dich aus Kapitel 3 erinnerst, besteht die Bearbeitungsfunktion aus den folgenden Schritten:
-
Wenn du auf eine Tabellenzelle doppelklickst, verwandelt sie sich in ein Texteingabeformular.
-
Du tippst in das Texteingabeformular.
-
Wenn du fertig bist, drückst du die Eingabetaste, um das Formular abzuschicken.
Um den Überblick über diesen Prozess zu behalten, fügen wir ein edit
Statusobjekt hinzu. Es ist null
, wenn keine Bearbeitung stattfindet; ansonsten speichert es die Zeilen- und Spaltenindizes der bearbeiteten Zelle.
const
[
edit
,
setEdit
]
=
useState
(
null
);
In der Benutzeroberfläche musst du Doppelklicks behandeln (onDoubleClick={showEditor}
) und, wenn der Benutzer etwas bearbeitet, ein Formular anzeigen. Ansonsten zeigst du nur die Daten an. Wenn der Benutzer die Eingabetaste drückt, fängst du das Submit-Ereignis ab (onSubmit={save}
).
<
tbody
onDoubleClick
=
{
showEditor
}
>
{
data
.
map
((
row
,
rowidx
)
=>
(
<
tr
key
=
{
rowidx
}
data
-
row
=
{
rowidx
}
>
{
row
.
map
((
cell
,
columnidx
)
=>
{
if
(
edit
&&
edit
.
row
===
rowidx
&&
edit
.
column
===
columnidx
)
{
cell
=
(
<
form
onSubmit
=
{
save
}
>
<
input
type
=
"text"
defaultValue
=
{
cell
}
/>
</
form
>
);
}
return
<
td
key
=
{
columnidx
}
>
{
cell
}
</
td
>;
})}
</
tr
>
))}
</
tbody
>
Es gibt zwei Kurzfunktionen, die noch implementiert werden müssen: showEditor()
und save()
.
Die showEditor()
wird durch einen Doppelklick auf eine Zelle im Tabellenkörper aufgerufen. Dort aktualisierst du den edit
Status (über setEdit()
) mit Zeilen- und Spaltenindizes, damit das Rendering weiß, welche Zellen durch ein Formular ersetzt werden sollen.
function
showEditor
(
e
)
{
setEdit
({
row
:
parseInt
(
e
.
target
.
parentNode
.
dataset
.
row
,
10
)
,
column
:
e
.
target
.
cellIndex
,
});
}
Die Funktion save()
fängt das Submit-Ereignis des Formulars ab, verhindert das Absenden und aktualisiert den data
Status mit dem neuen Wert in der bearbeiteten Zelle. Außerdem ruft siesetEdit()
auf und übergibt null
als neuen Bearbeitungsstatus, was bedeutet, dass die Bearbeitung abgeschlossen ist.
function
save
(
e
)
{
e
.
preventDefault
();
const
input
=
e
.
target
.
firstChild
;
const
dataCopy
=
clone
(
data
);
dataCopy
[
edit
.
row
][
edit
.
column
]
=
input
.
value
;
setEdit
(
null
);
setData
(
dataCopy
);
}
Und damit ist die Bearbeitungsfunktion fertig. Den vollständigen Code findest du in der Datei 04.04.fn.table-edit.html im Repo des Buches.
Suche
Das Durchsuchen/Filtern der Daten stellt keine neuen Herausforderungen dar, wenn es um React und Hooks geht. Du kannst versuchen, es selbst zu implementieren und die Implementierung in 04.05.fn.table-search.html im Repo des Buches referenzieren.
Du brauchst zwei neue Teile des Staates:
-
Der boolesche Wert
search
, der angibt, ob der Benutzer filtert oder sich die Daten nur ansieht -
Die Kopie von
data
alspreSearchData
, denn nun wirddata
eine gefilterte Teilmenge aller Daten
const
[
search
,
setSearch
]
=
useState
(
false
);
const
[
preSearchData
,
setPreSearchData
]
=
useState
(
null
);
Du musst darauf achten, dass preSearchData
immer auf dem neuesten Stand ist, da data
(die gefilterte Teilmenge) aktualisiert werden kann, wenn der Benutzer bearbeitet und gleichzeitig filtert. Schau zur Auffrischung in Kapitel 3 nach.
Weiter geht es mit , wo du die Wiederholungsfunktion implementierst und dich mit zwei neuen Konzepten vertraut machen kannst:
-
Lebenszyklus-Haken verwenden
-
Eigene Aufhänger schreiben
Lebenszyklen in einer Welt voller Haken
Die Wiederholungsfunktion in Kapitel 3 verwendet zwei Lebenszyklusmethoden der Klasse Excel
:
componentDidMount()
und componentWillUnmount()
.
Probleme mit Lebenszyklusmethoden
Wenn du dir das Beispiel 03.14.table-fetch.html auf noch einmal ansiehst, wirst du feststellen, dass jede dieser Aufgaben zwei Aufgaben hat, die nichts miteinander zu tun haben:
componentDidMount
()
{
document
.
addEventListener
(
'keydown'
,
this
.
keydownHandler
);
fetch
(
'https://www...'
)
.
then
(
/*...*/
)
.
then
((
initialData
)
=>
{
/*...*/
this
.
setState
({
data
});
});
}
componentWillUnmount
()
{
document
.
removeEventListener
(
'keydown'
,
this
.
keydownHandler
);
clearInterval
(
this
.
replayID
);
}
In componentDidMount()
richtest du einen keydown
Listener ein, um die Wiedergabe zu starten und Daten von einem Server abzurufen. In componentWillUnmount()
entfernst du den keydown
Listener und bereinigst außerdem eine setInterval()
ID. Dies veranschaulicht zwei Probleme im Zusammenhang mit der Verwendung von Lifecycle-Methoden in Klassenkomponenten (die bei der Verwendung von Hooks gelöst werden):
- Unzusammenhängende Aufgaben werden gemeinsam ausgeführt
-
Zum Beispiel das Abrufen von Daten und das Einrichten von Event-Listenern an einem Ort. Das führt dazu, dass die Lebenszyklusmethoden immer länger werden, während sie die nicht miteinander verbundenen Aufgaben ausführen. Bei einfachen Komponenten ist das in Ordnung, aber bei größeren Komponenten musst du auf Codekommentare zurückgreifen oder Teile des Codes in verschiedene andere Funktionen verschieben, um die nicht zusammenhängenden Aufgaben aufzuteilen und den Code besser lesbar zu machen.
- Zusammenhängende Aufgaben werden aufgeteilt
-
Betrachte zum Beispiel das Hinzufügen und Entfernen desselben Ereignis-Listeners. Je größer die Lebenszyklusmethoden werden, desto schwieriger ist es, die einzelnen Teile desselben Anliegens auf einen Blick zu erfassen, weil sie beim späteren Lesen einfach nicht mehr in denselben Code passen.
useEffect()
Der eingebaute Haken, der die beiden oben genannten Lebenszyklusmethoden ersetzt, ist React.useEffect()
.
Hinweis
Das Wort "Effekt" steht für "Nebeneffekt" und bezeichnet eine Art von Arbeit, die nichts mit der Hauptaufgabe zu tun hat, aber ungefähr zur gleichen Zeit passiert. Die Hauptaufgabe jeder React-Komponente besteht darin, etwas auf der Grundlage von Zuständen und Requisiten zu rendern. Aber das Rendering kann gleichzeitig (in derselben Funktion) neben einigen Nebenaufträgen (wie dem Abrufen von Daten von einem Server oder dem Einrichten von Ereignis-Listenern) notwendig sein.
In der Komponente Excel
zum Beispiel ist das Einrichten eines keydown
Handlers ein Nebeneffekt der Hauptaufgabe, die Daten in einer Tabelle darzustellen.
Der Haken useEffect()
nimmt zwei Argumente entgegen:
-
Eine Callback-Funktion, die von React zum richtigen Zeitpunkt aufgerufen wird
-
Ein optionales Array von Abhängigkeiten
Die Liste der Abhängigkeiten enthält Variablen, die geprüft werden, bevor der Callback aufgerufen wird, und die bestimmen, ob der Callback überhaupt aufgerufen werden soll.
-
Wenn sich die Werte der abhängigen Variablen nicht geändert haben, ist es nicht nötig, den Callback aufzurufen.
-
Wenn die Liste der Abhängigkeiten ein leeres Array ist, wird der Callback nur einmal aufgerufen, ähnlich wie bei
componentDidMount()
. -
Wenn die Abhängigkeiten weggelassen werden, wird der Callback bei jedem Rerender aufgerufen
useEffect
(()
=>
{
// logs only if `data` or `headers` have changed
console
.
log
(
Date
.
now
());
},
[
data
,
headers
]);
useEffect
(()
=>
{
// logs once, after initial render, like `componentDidMount()`
console
.
log
(
Date
.
now
());
},
[]);
useEffect
(()
=>
{
// called on every re-render
console
.
log
(
Date
.
now
());
},
/* no dependencies here */
);
Aufräumen von Nebeneffekten
Jetzt weißt du, wie du mit Hooks das erreichen kannst, was componentDidMount()
an Klassenkomponenten zu bieten hat. Aber wie sieht es mit einer Entsprechung zu componentWillUnmount()
aus? Für diese Aufgabe verwendest du den Rückgabewert der Callback-Funktion, die du an useEffect()
übergibst:
useEffect
(()
=>
{
// logs once, after initial render, like `componentDidMount()`
console
.
log
(
Date
.
now
());
return
()
=>
{
// log when the component will be removed form the DOM
// like `componentDidMount()`
console
.
log
(
Date
.
now
());
};
},
[]);
Schauen wir uns ein ausführlicheres Beispiel an(04.06.useEffect.html im Repo):
function
Example
()
{
useEffect
(()
=>
{
console
.
log
(
'Rendering <Example/>'
,
Date
.
now
());
return
()
=>
{
// log when the component will be removed form the DOM
// like `componentDidMount()`
console
.
log
(
'Removing <Example/>'
,
Date
.
now
());
};
}
,
[]);
return
<
p
>
I
am
an
example
child
component
.
</
p
>;
}
function
ExampleParent
()
{
const
[
visible
,
setVisible
]
=
useState
(
false
);
return
(
<
div
>
<
button
onClick
=
{()
=>
setVisible
(
!
visible
)}
>
Hello
there
,
press
me
{
visible
?
'again'
:
''
}
</
button
>
{
visible
?
<
Example
/>
:
null
}
</
div
>
);
}
Wenn du einmal auf die Schaltfläche klickst, wird eine untergeordnete Komponente angezeigt, und wenn du erneut darauf klickst, wird sie entfernt. Wie du in Abbildung 4-3 sehen kannst, wird der Rückgabewert von useEffect()
(eine Funktion) aufgerufen, wenn die Komponente aus dem DOM entfernt wird.
Beachte, dass die Funktion cleanup (auch bekannt als teardown) aufgerufen wurde, wenn die Komponente aus dem DOM entfernt wird, weil das Abhängigkeitsarray leer ist. Wäre ein Wert im Abhängigkeitsfeld vorhanden, würde die Abrissfunktion immer dann aufgerufen werden, wenn sich der Wert der Abhängigkeit ändert.
Störungsfreie Lebenszyklen
Wenn du dir noch einmal den Anwendungsfall vor Augen führst, bei dem es um das Einrichten und Löschen von Ereignis-Listenern geht, kann das so umgesetzt werden:
useEffect
(()
=>
{
function
keydownHandler
()
{
// do things
}
document
.
addEventListener
(
'keydown'
,
keydownHandler
);
return
()
=>
{
document
.
removeEventListener
(
'keydown'
,
keydownHandler
);
};
},
[]);
Das obige Muster löst das zweite Problem, das mit klassenbasierten Lebenszyklusmethoden hat, nämlich das Problem, zusammenhängende Aufgaben über die ganze Komponente zu verteilen. Hier kannst du sehen, wie die Verwendung von Hooks es dir ermöglicht, die Handler-Funktion, ihre Einrichtung und ihr Entfernen an einem Ort zu haben.
Das erste Problem (nicht zusammenhängende Aufgaben am selben Ort) lässt sich durch mehrere useEffect
Aufrufe lösen, die jeweils einer bestimmten Aufgabe gewidmet sind. Ähnlich wie man statt eines Grab-Bag-Objekts verschiedene Zustände haben kann, kann man auch getrennte useEffect
Aufrufe haben, die sich jeweils um ein bestimmtes Anliegen kümmern, anstatt einer einzigen Klassenmethode, die sich um alles kümmern muss:
function
Example
()
{
const
[
data
,
setData
]
=
useState
(
null
);
useEffect
(()
=>
{
// fetch() and then call setData()
});
useEffect
(()
=>
{
// event handlers
});
return
<
div
>
{
data
}
<
/div>;
}
useLayoutEffect()
Zum Abschluss der Diskussion über useEffect()
wollen wir einen weiteren eingebauten Haken namens useLayoutEffect()
betrachten.
Hinweis
Es gibt nur ein paar eingebaute Hooks, also mach dir keine Sorgen, dass du eine lange Liste neuer APIs auswendig lernen musst.
useLayoutEffect()
funktioniert wie useEffect()
, mit dem einzigen Unterschied, dass aufgerufen wird, bevor React alle DOM-Knoten eines Renderings gezeichnet hat. Im Allgemeinen solltest du useEffect()
verwenden, es sei denn, du musst etwas auf der Seite messen (z. B. die Abmessungen einer gerenderten Komponente oder die Scroll-Position nach einer Aktualisierung) und dann auf der Grundlage dieser Informationen neu rendern. Wenn nichts dergleichen erforderlich ist, ist useEffect()
besser, da es asynchron ist und dem Leser deines Codes außerdem anzeigt, dass DOM-Veränderungen für deine Komponente nicht relevant sind.
Da useLayoutEffect()
früher aufgerufen wird, kannst du neu berechnen und rendern und der Nutzer sieht nur das letzte Rendering. Andernfalls sieht er zuerst das erste Rendering und dann das zweite Rendering. Je nachdem, wie kompliziert das Layout ist, kann der Nutzer ein Flackern zwischen den beiden Renderings wahrnehmen.
Das nächste Beispiel(04.07.useLayoutEffect.html im Repo) rendert eine lange Tabelle mit zufälligen Zellenbreiten (nur um es dem Browser schwerer zu machen). Dann wird die Breite der Tabelle in einem Effekt-Hook festgelegt.
function
Example
({
layout
})
{
if
(
layout
===
null
)
{
return
null
;
}
if
(
layout
)
{
useLayoutEffect
(()
=>
{
const
table
=
document
.
getElementsByTagName
(
'table'
)[
0
];
console
.
log
(
table
.
offsetWidth
);
table
.
width
=
'250px'
;
}
,
[]);
}
else
{
useEffect
(()
=>
{
const
table
=
document
.
getElementsByTagName
(
'table'
)[
0
];
console
.
log
(
table
.
offsetWidth
);
table
.
width
=
'250px'
;
}
,
[]);
}
return
(
<
table
>
<
thead
>
<
tr
>
<
th
>
Random
</
th
>
</
tr
>
</
thead
>
<
tbody
>
{
Array
.
from
(
Array
(
10000
)).
map
((
_
,
idx
)
=>
(
<
tr
key
=
{
idx
}
>
<
td
width
=
{
Math
.
random
()
*
800
}
>
{
Math
.
random
()}
</
td
>
</
tr
>
))}
</
tbody
>
</
table
>
);
}
function
ExampleParent
()
{
const
[
layout
,
setLayout
]
=
useState
(
null
);
return
(
<
div
>
<
button
onClick
=
{()
=>
setLayout
(
false
)}
>
useEffect
</
button
>
{
' '
}
<
button
onClick
=
{()
=>
setLayout
(
true
)}
>
useLayoutEffect
</
button
>
{
' '
}
<
button
onClick
=
{()
=>
setLayout
(
null
)}
>
clear
</
button
>
<
Example
layout
=
{
layout
}
/>
</
div
>
);
}
Je nachdem, ob du den Pfad useEffect()
oder useLayoutEffect()
auslöst, kann es sein, dass du ein Flackern siehst, wenn die Tabelle von ihrem zufälligen Wert (ca. 600 px) auf die fest eingestellten 250 px verkleinert wird (siehe Abbildung 4-4).
Beachte, dass du in beiden Fällen die Geometrie der Tabelle abrufen kannst (z. B. table.offsetWidth
). Wenn du dies also nur zu Informationszwecken brauchst und nicht neu rendern willst, bist du mit der asynchronen useEffect()
besser dran. useLayoutEffect()
sollte dafür reserviert sein, Flackern in Fällen zu vermeiden, in denen du auf der Grundlage einer Messung handeln (neu rendern) musst, z. B. bei der Positionierung einer ausgefallenen Tooltip-Komponente auf der Grundlage der Größe des Elements, auf das sie zeigt.
Ein maßgeschneiderter Haken
Gehen wir zurück zu Excel
und sehen wir uns an, wie wir die Wiederholungsfunktion implementieren können. Im Fall von Klassenkomponenten war es notwendig, eine logSetState()
zu erstellen und dann alle this.setState()
Aufrufe durch this.logSetState()
zu ersetzen. Bei Funktionskomponenten kannst du alle Aufrufe des useState()
Hooks durch useLoggedState()
ersetzen. Das ist etwas bequemer, da es nur wenige Aufrufe gibt (für jedes unabhängige Bit des Zustands) und sie sich alle am Anfang der Funktion befinden.
// before
function
Excel
({
headers
,
initialData
})
{
const
[
data
,
setData
]
=
useState
(
initialData
);
const
[
edit
,
setEdit
]
=
useState
(
null
);
// ... etc
}
// after
function
Excel
({
headers
,
initialData
})
{
const
[
data
,
setData
]
=
useLoggedState
(
initialData
,
true
);
const
[
edit
,
setEdit
]
=
useLoggedState
(
null
);
// ... etc
}
Es gibt keinen eingebauten useLoggedState()
Haken, aber das ist OK. Du kannst deine eigenen Hooks erstellen. Wie die eingebauten Hooks ist ein Custom Hook einfach eine Funktion, die mit use*()
beginnt. Hier ist ein Beispiel:
function
useLoggedState
(
initialValue
,
isData
)
{
// ...
}
Die Signatur des Hooks kann alles sein, was du willst. In diesem Fall, , gibt es ein zusätzliches isData
Argument. Es dient dazu, den Datenstatus vom Nicht-Datenstatus zu unterscheiden. Im Beispiel der Klassenkomponente aus Kapitel 3 ist der gesamte Zustand ein einziges Objekt, aber hier sind mehrere Teile des Zustands vorhanden. Bei der Wiedergabe geht es in erster Linie darum, die Datenänderungen zu zeigen und dann zu zeigen, dass alle unterstützenden Informationen (Sortieren, Absteigen usw.) zweitrangig sind. Da die Wiedergabe jede Sekunde aktualisiert wird, macht es keinen Spaß, sich die Änderungen der unterstützenden Daten einzeln anzusehen; die Wiedergabe wäre zu langsam. Wir haben also ein Hauptprotokoll (dataLog
array) und ein Zusatzprotokoll (auxLog
array). Außerdem ist es sinnvoll, ein Flag einzubauen, das angibt, ob sich der Zustand aufgrund einer Benutzerinteraktion oder (automatisch) während der Wiedergabe ändert:
let
dataLog
=
[];
let
auxLog
=
[];
let
isReplaying
=
false
;
Der benutzerdefinierte Hook soll die regulären Statusaktualisierungen nicht beeinträchtigen und delegiert diese Aufgabe daher an die ursprüngliche useState
. Das Ziel ist es, den Status zusammen mit einem Verweis auf die Funktion zu protokollieren, die weiß, wie dieser Status während der Wiedergabe aktualisiert werden kann. Die Funktion sieht in etwa so aus:
function
useLoggedState
(
initialValue
,
isData
)
{
const
[
state
,
setState
]
=
useState
(
initialValue
);
// fun here...
return
[
state
,
setState
];
}
Der obige Code verwendet den Standard useState
. Aber jetzt hast du die Verweise auf einen Teil des Zustands und die Möglichkeit, ihn zu aktualisieren. Das musst du protokollieren. Hier können wir von dem useEffect()
Hook profitieren:
function
useLoggedState
(
initialValue
,
isData
)
{
const
[
state
,
setState
]
=
useState
(
initialValue
);
useEffect
(()
=>
{
// todo
}
,
[
state
]);
return
[
state
,
setState
];
}
Diese Methode stellt sicher, dass die Protokollierung nur erfolgt, wenn sich der Wert von state
ändert. Die Funktion useLoggedState()
kann während verschiedener Rerender mehrmals aufgerufen werden, aber du kannst diese Aufrufe ignorieren, es sei denn, sie beinhalten eine Änderung eines interessanten Zustands.
In dem Rückruf von useEffect()
wirst du:
-
Mache nichts, wenn der/die Benutzer/in die Wiedergabe wiederholt.
-
Protokolliere jede Änderung des Datenzustands auf
dataLog
. -
Protokolliere jede Änderung an den unterstützenden Daten auf
auxLog
, indiziert durch die zugehörige Datenänderung.
useEffect
(()
=>
{
if
(
isReplaying
)
{
return
;
}
if
(
isData
)
{
dataLog
.
push
([
clone
(
state
)
,
setState
]);
}
else
{
const
idx
=
dataLog
.
length
-
1
;
if
(
!
auxLog
[
idx
])
{
auxLog
[
idx
]
=
[];
}
auxLog
[
idx
].
push
([
state
,
setState
]);
}
}
,
[
state
]);
Warum gibt es benutzerdefinierte Hooks? Sie helfen dir dabei, ein Stück Logik, das in einer Komponente verwendet und oft von mehreren Komponenten genutzt wird, zu isolieren und ordentlich zu verpacken. Die obige benutzerdefinierte useLoggedState()
kann in jede Komponente eingefügt werden, die von der Protokollierung ihres Zustands profitieren kann. Außerdem können benutzerdefinierte Hooks andere Hooks aufrufen, was normale Funktionen (ohne Hook und ohne Komponente) nicht können.
Abschluss der Wiederholung
Jetzt, wo du einen benutzerdefinierten Hook hast, der die Änderungen an verschiedenen Zustandsgrößen protokolliert, ist es an der Zeit, mit der Wiedergabefunktion zu verbinden.
Die Funktion replay()
ist kein aufregender Aspekt der React-Diskussion, aber sie richtet eine Intervall-ID ein. Du brauchst diese ID, um das Intervall aufzuräumen, falls Excel
während der Wiedergabe aus dem DOM entfernt wird. Bei der Wiedergabe werden die Datenänderungen jede Sekunde wiedergegeben, während die Hilfsdaten zusammen gespült werden:
function
replay
()
{
isReplaying
=
true
;
let
idx
=
0
;
replayID
=
setInterval
(()
=>
{
const
[
data
,
fn
]
=
dataLog
[
idx
];
fn
(
data
);
auxLog
[
idx
]
&&
auxLog
[
idx
].
forEach
((
log
)
=>
{
const
[
data
,
fn
]
=
log
;
fn
(
data
);
});
idx
++
;
if
(
idx
>
dataLog
.
length
-
1
)
{
isReplaying
=
false
;
clearInterval
(
replayID
);
return
;
}
},
1000
);
}
Der letzte Teil der Arbeit besteht darin, einen Effekt-Hook einzurichten. Nachdem Excel
gerendert wurde, ist der Hook dafür verantwortlich, Listener einzurichten, die eine bestimmte Tastenkombination überwachen, um die Wiedergabeshow zu starten. ist auch der Ort, an dem aufgeräumt wird, wenn die Komponente zerstört wird.
useEffect
(()
=>
{
function
keydownHandler
(
e
)
{
if
(
e
.
altKey
&&
e
.
shiftKey
&&
e
.
keyCode
===
82
)
{
// ALT+SHIFT+R(eplay)
replay
();
}
}
document
.
addEventListener
(
'keydown'
,
keydownHandler
);
return
()
=>
{
document
.
removeEventListener
(
'keydown'
,
keydownHandler
);
clearInterval
(
replayID
);
dataLog
=
[];
auxLog
=
[];
};
},
[]);
Um den gesamten Code zu sehen, schau dir 04.08.fn.table-replay.html im Repo des Buches an.
useReducer
Schließen wir das Kapitel mit einem weiteren eingebauten Hook namens useReducer()
ab. Die Verwendung eines Reduzierers ist eine Alternative zu useState()
. Anstatt verschiedene Teile der Komponente aufzurufen, die ihren Zustand ändern, können alle Änderungen an einer einzigen Stelle behandelt werden.
Ein Reducer ist einfach eine JavaScript-Funktion, die zwei Eingaben - den alten Zustand und eine Aktion - entgegennimmt und den neuen Zustand zurückgibt. Stell dir die Aktion als etwas vor, das in der App passiert ist, z. B. ein Klick, ein Datenabruf oder eine Zeitüberschreitung. Alle drei Variablen (neuer Zustand, alter Zustand, Aktion) können von beliebigem Typ sein, am häufigsten sind es jedoch Objekte.
Reduktionsfunktionen
Eine Reduktionsfunktion sieht in ihrer einfachsten Form wie folgt aus:
function
myReducer
(
oldState
,
action
)
{
const
newState
=
{};
// do something with `oldState` and `action`
return
newState
;
}
Stell dir vor, dass die Reduktionsfunktion dafür verantwortlich ist, der Realität einen Sinn zu geben, wenn etwas in der Welt passiert. Die Welt ist ein mess
, dann passiert ein event
. Die Funktion, die die Welt makeSense()
soll, bringt das Chaos mit dem neuen Ereignis in Einklang und reduziert die ganze Komplexität auf einen schönen Zustand oder order
:
function
makeSense
(
mess
,
event
)
{
const
order
=
{};
// do something with mess and event
return
order
;
}
Eine weitere Analogie stammt aus der Welt des Kochens. Manche Soßen und Suppen werden auch als Reduktionen bezeichnet, die durch den Prozess der Reduktion (Eindicken, Intensivierung des Geschmacks) entstehen. Der Ausgangszustand ist ein Topf mit Wasser, dann verändern verschiedene Aktionen (Kochen, Hinzufügen von Zutaten, Umrühren) den Zustand des Topfinhalts mit jeder Aktion.
Aktionen
Die Reduzierfunktion kann alles annehmen (einen String, ein Objekt), aber eine gängige Implementierung ist ein event
Objekt mit:
-
A
type
(z.B.click
in der DOM-Welt) -
Optional einige
payload
andere Informationen über das Ereignis
Aktionen werden dann "versendet". Wenn die Aktion versendet wird, wird die entsprechende Reduzierfunktion von React mit dem aktuellen Zustand und deinem neuen Ereignis (Aktion) aufgerufen.
Mit useState
hast du:
const
[
data
,
setData
]
=
useState
(
initialData
);
Diese kann durch das Reduzierstück ersetzt werden:
const
[
data
,
dispatch
]
=
useReducer
(
myReducer
,
initialData
);
Die data
wird immer noch auf die gleiche Weise verwendet, um die Komponente zu rendern. Aber wenn etwas passiert, ruft , anstatt ein wenig Arbeit zu erledigen und dann setData()
aufzurufen, die Funktion dispatch()
auf, die von useReducer()
zurückgegeben wird. Von dort aus übernimmt der Reducer und gibt die neue Version von data
zurück. Es gibt keine weitere Funktion, die aufgerufen werden muss, um den neuen Status zu setzen; die neue data
wird von React verwendet, um die Komponente neu zu rendern.
Abbildung 4-5 zeigt ein Diagramm dieses Prozesses.
Ein Beispiel für einen Reduzierer
Schauen wir uns ein schnelles, isoliertes Beispiel für die Verwendung eines Reduzierers an. Angenommen, du hast eine Tabelle mit zufälligen Daten und Schaltflächen, die entweder die Daten aktualisieren oder die Hintergrund- und Vordergrundfarben der Tabelle in zufällige Farben ändern können (wie in Abbildung 4-6 dargestellt).
Zu Beginn gibt es keine Daten und die Farben Schwarz und Weiß werden als Standardwerte verwendet:
const
initialState
=
{
data
:
[],
color
:
'black'
,
background
:
'white'
};
Der Reducer wird am Anfang der Komponente <RandomData>
initialisiert:
function
RandomData
()
{
const
[
state
,
dispatch
]
=
useReducer
(
myReducer
,
initialState
);
// ...
}
Hier ist state
wieder ein Sammelobjekt für verschiedene Zustandsdaten (aber das muss nicht der Fall sein). Der Rest der Komponente wird wie gewohnt auf der Grundlage von state
gerendert, mit einem Unterschied. Früher war der onClick
Handler eines Buttons eine Funktion, die den Status aktualisiert, jetzt rufen alle Handler nur nochdispatch()
auf und senden Informationen über das Ereignis:
return
(
<
div
>
<
div
className
=
"toolbar"
>
<
button
onClick
=
{()
=>
dispatch
({
type
:
'newdata'
})}
>
Get
data
</
button
>
{
' '
}
<
button
onClick
=
{()
=>
dispatch
({
type
:
'recolor'
,
payload
:
{
what
:
'color'
}})}
>
Recolor
text
</
button
>
{
' '
}
<
button
onClick
=
{
()
=>
dispatch
({
type
:
'recolor'
,
payload
:
{
what
:
'background'
}})
}
>
Recolor
background
</
button
>
</
div
>
<
table
style
=
{{
color
,
background
}}
>
<
tbody
>
{
data
.
map
((
row
,
idx
)
=>
(
<
tr
key
=
{
idx
}
>
{
row
.
map
((
cell
,
idx
)
=>
(
<
td
key
=
{
idx
}
>
{
cell
}
</
td
>
))}
</
tr
>
))}
</
tbody
>
</
table
>
</
div
>
);
Jedes versendete Ereignis-/Aktionsobjekt hat eine type
Eigenschaft, damit die Reduzierfunktion erkennen kann, was getan werden muss. Es kann eine payload
geben, die weitere Details des Ereignisses angibt, muss aber nicht.
Schließlich der Reducer. Er hat eine Anzahl von if
/else
Anweisungen (oder eine switch
, wenn du das bevorzugst), die prüfen, welche Art von Ereignis gesendet wurde. Dann werden die Daten entsprechend der Aktion manipuliert und eine neue Version des Zustands wird zurückgegeben:
function
myReducer
(
oldState
,
action
)
{
const
newState
=
clone
(
oldState
);
if
(
action
.
type
===
'recolor'
)
{
newState
[
action
.
payload
.
what
]
=
`rgb(
${
rand
(
256
)
}
,
${
rand
(
256
)
}
,
${
rand
(
256
)
}
)`
;
}
else
if
(
action
.
type
===
'newdata'
)
{
const
data
=
[];
for
(
let
i
=
0
;
i
<
10
;
i
++
)
{
data
[
i
]
=
[];
for
(
let
j
=
0
;
j
<
10
;
j
++
)
{
data
[
i
][
j
]
=
rand
(
10000
);
}
}
newState
.
data
=
data
;
}
return
newState
;
}
// couple of helpers
function
clone
(
o
)
{
return
JSON
.
parse
(
JSON
.
stringify
(
o
));
}
function
rand
(
max
)
{
return
Math
.
floor
(
Math
.
random
()
*
max
);
}
Beachte, dass der alte Zustand mit dem dir bereits bekannten clone()
geklont wird. Bei useState()/setState()
war das in vielen Fällen nicht unbedingt nötig. Oft reichte es aus, eine bestehende Variable zu ändern und sie an setState()
zu übergeben. Aber wenn du hier nicht klonst, sondern nur das gleiche Objekt im Speicher änderst, sieht React, dass der alte und der neue Zustand auf das gleiche Objekt zeigen und überspringt das Rendern, weil es denkt, dass sich nichts geändert hat. Du kannst es selbst ausprobieren: Entferne den Aufruf von clone()
und beobachte, dass das Rerendering nicht stattfindet .
Unit Testing Reducers
Der Wechsel zu useReducer()
für die Verwaltung des Zustands macht es viel einfacher, Unit-Tests zu schreiben. Du musst die Komponente, ihre Eigenschaften und ihren Zustand nicht einrichten. Du musst keinen Browser einbinden oder einen anderen Weg finden, um Click-Events zu simulieren. Du musst nicht einmal React einbeziehen. Um die Zustandslogik zu testen, musst du nur den alten Zustand und eine Aktion an die Funktion reducer übergeben und prüfen, ob der gewünschte neue Zustand zurückgegeben wird. Das ist reines JavaScript: zwei Objekte rein, ein Objekt raus. Die Unit-Tests sollten nicht viel komplizierter sein als die Tests des kanonischen Beispiels:
function
add
(
a
,
b
)
{
return
a
+
b
;
}
Später im Buch werden wir uns mit dem Thema Testen beschäftigen, aber um dir einen Vorgeschmack zu geben, könnte ein Beispieltest so aussehen:
const
initialState
=
{
data
:
[],
color
:
'black'
,
background
:
'white'
};
it
(
'produces a 10x10 array'
,
()
=>
{
const
{
data
}
=
myReducer
(
initialState
,
{
type
:
'newdata'
});
expect
(
data
.
length
).
toEqual
(
10
);
expect
(
data
[
0
].
length
).
toEqual
(
10
);
});
Excel-Komponente mit einem Reduzierer
Als letztes Beispiel für die Verwendung von Reduzierern sehen wir uns an, wie du vonuseState()
zu useReducer()
in der Komponente Excel
wechseln kannst.
Im Beispiel aus dem vorigen Abschnitt war der vom Reducer verwaltete Zustand wieder ein Objekt mit unverbundenen Daten. Das muss aber nicht so sein. Du kannst mehrere Reduzierer einsetzen, um deine Anliegen zu trennen. Du kannst sogar useState()
mit useReducer()
mischen und kombinieren. Probieren wir das mit Excel
aus.
Zuvor wurde die data
in der Tabelle von useState()
verwaltet:
const
[
data
,
setData
]
=
useState
(
initialData
);
// ...
const
[
edit
,
setEdit
]
=
useState
(
null
);
const
[
search
,
setSearch
]
=
useState
(
false
);
Wenn du zur Verwaltung von data
auf useReducer()
wechselst und den Rest unberührt lässt, sieht das folgendermaßen aus:
const
[
data
,
dispatch
]
=
useReducer
(
reducer
,
initialData
);
// ...
const
[
edit
,
setEdit
]
=
useState
(
null
);
const
[
search
,
setSearch
]
=
useState
(
false
);
Da data
mit identisch ist, musst du im Rendering-Bereich nichts ändern. Änderungen sind nur bei den Aktionshandlern erforderlich. Zum Beispiel wird filter()
verwendet, um die Filterung durchzuführen und setData()
aufzurufen:
function
filter
(
e
)
{
const
needle
=
e
.
target
.
value
.
toLowerCase
();
if
(
!
needle
)
{
setData
(
preSearchData
);
return
;
}
const
idx
=
e
.
target
.
dataset
.
idx
;
const
searchdata
=
preSearchData
.
filter
((
row
)
=>
{
return
row
[
idx
].
toString
().
toLowerCase
().
indexOf
(
needle
)
>
-
1
;
});
setData
(
searchdata
);
}
In der umgeschriebenen Version wird stattdessen eine Aktion ausgelöst. Das Ereignis hat eine type
von "search" und einige zusätzliche Nutzdaten (wonach sucht der Nutzer und wo?):
function
filter
(
e
)
{
const
needle
=
e
.
target
.
value
;
const
column
=
e
.
target
.
dataset
.
idx
;
dispatch
({
type
:
'search'
,
payload
:
{
needle
,
column
},
});
setEdit
(
null
);
}
Ein weiteres Beispiel wäre das Umschalten der Suchfelder:
// before
function
toggleSearch
()
{
if
(
search
)
{
setData
(
preSearchData
);
setSearch
(
false
);
setPreSearchData
(
null
);
}
else
{
setPreSearchData
(
data
);
setSearch
(
true
);
}
}
// after
function
toggleSearch
()
{
if
(
!
search
)
{
dispatch
({
type
:
'startSearching'
});
}
else
{
dispatch
({
type
:
'doneSearching'
});
}
setSearch
(
!
search
);
}
Hier kannst du die Mischung aus setSearch()
und dispatch()
sehen, um den Zustand zu verwalten. Der!search
Toggle ist ein Flag für die Benutzeroberfläche, um Eingabefelder ein- oder auszublenden, während der dispatch()
für die Verwaltung der Daten ist.
Werfen wir zum Schluss einen Blick auf die Funktion reducer()
. Hier werden die Daten gefiltert und manipuliert. Auch hier handelt es sich um eine Reihe von if
/else
Blöcken, die jeweils einen anderen Aktionstyp behandeln:
let
originalData
=
null
;
function
reducer
(
data
,
action
)
{
if
(
action
.
type
===
'sort'
)
{
const
{
column
,
descending
}
=
action
.
payload
;
return
clone
(
data
).
sort
((
a
,
b
)
=>
{
if
(
a
[
column
]
===
b
[
column
])
{
return
0
;
}
return
descending
?
a
[
column
]
<
b
[
column
]
?
1
:
-
1
:
a
[
column
]
>
b
[
column
]
?
1
:
-
1
;
});
}
if
(
action
.
type
===
'save'
)
{
data
[
action
.
payload
.
edit
.
row
][
action
.
payload
.
edit
.
column
]
=
action
.
payload
.
value
;
return
data
;
}
if
(
action
.
type
===
'startSearching'
)
{
originalData
=
data
;
return
originalData
;
}
if
(
action
.
type
===
'doneSearching'
)
{
return
originalData
;
}
if
(
action
.
type
===
'search'
)
{
return
originalData
.
filter
((
row
)
=>
{
return
(
row
[
action
.
payload
.
column
]
.
toString
()
.
toLowerCase
()
.
indexOf
(
action
.
payload
.
needle
.
toLowerCase
())
>
-
1
);
});
}
}
Get React: Up & Running, 2. Auflage now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.