-
Notifications
You must be signed in to change notification settings - Fork 0
/
2024-02-19-react-advanced.html
1555 lines (1361 loc) · 58 KB
/
2024-02-19-react-advanced.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>React Aufbauschulung</title>
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, minimal-ui"
/>
<link rel="stylesheet" href="slides/revealjs/reveal.js/dist/reset.css" />
<link rel="stylesheet" href="slides/revealjs/reveal.js/dist/reveal.css" />
<link rel="stylesheet" href="slides/revealjs/reveal.js/dist/theme/solarized.css" />
<!-- Theme used for syntax hislides/ghlighted code -->
<link rel="stylesheet" href="slides/revealjs/highlight-js-github-theme.css" />
<link rel="stylesheet" href="slides/revealjs/styles.css" />
</head>
<body>
<div class="reveal">
<!-- Any section element inside of this container is displayed as a slide -->
<div class="slides">
<section data-state="title">
<h2 class="title" style="font-size: 7rem">
<b>React Aufbauschulung</b>
</h2>
<h4>
<span class="transparent-bg">
<a href="https://nilshartmann.net" target="_blank">Nils Hartmann</a>
|
<a href="https://twitter.com/nilshartmann" target="_blank">@nilshartmann</a>
</span>
</h4>
<p style="margin-top: 4rem"></p>
<div>
<h3><span class="transparent-bg">Repository</span></h3>
<p>
<span class="transparent-bg"
>Remote:
<a href="https://github.com/nilshartmann/react18-training"
>https://github.com/nilshartmann/react18-training</a
></span
>
</p>
</div>
<p style="margin-top: 4rem"></p>
<div>
<h3><span class="transparent-bg">Slides</span></h3>
<p>
<span class="transparent-bg">Lokal: 2024-02-19-react-advanced.html</span>
</p>
<p>
<span class="transparent-bg"
>Remote:
<a
href="https://nilshartmann.github.io/react18-training/2024-02-19-react-advanced.html"
>https://nilshartmann.github.io/react18-training/2024-02-19-react-advanced.html</a
></span
>
</p>
</div>
</section>
<section>
<h2>Nils Hartmann</h2>
<p>
<a href="https://nilshartmann.net" target="_blank">https://nilshartmann.net</a>
/
<a href="https://twitter.com/nilshartmann" target="_blank">@nilshartmann</a>
</p>
<p>
<em>Freiberuflicher Software-Entwickler, Berater und Trainer aus Hamburg</em>
</p>
<div style="display: flex; justify-content: center">
<div>
<p>Java</p>
<p>JavaScript, TypeScript</p>
<p>React</p>
<p>Single-Page-Applications</p>
<p>GraphQL</p>
<p style="margin-top: 20px">
<a href="https://nilshartmann.net/workshops">Schulungen und Workshops</a>
</p>
</div>
<div style="margin-left: 15px">
<a href="https://reactbuch.de"
><img
style="max-height: 450px"
src="slides/images/react-buch-v2.jpg"
/><br />https://reactbuch.de</a
>
<br />
</div>
</div>
</section>
<!-- ============================================================================= -->
<section data-markdown>
<textarea data-template>
## Agenda
<!-- .slide: class="no-fragment xleft" -->
**_"Modernes Data- und Statemanagement"_**
* **Montag**
* [Context API](#/t-context)
* Neuerungen in [React 19](#/t-react19)
* Renderzyklen optimieren mit [useCallback, useMemo](#/t-context-render)
* [TanStack Query](#/t-tanstack-query)
* "Modernes" React: Data Fetching mit Suspense
* **Dienstag**
* Code Review, mögliche Refaktorings, ...
</textarea>
</section>
<!-- ============================================================================== -->
<!-- ==== ==== -->
<!-- ==== C O N T E X T ==== -->
<!-- ==== ==== -->
<!-- ============================================================================== -->
<section id="t-context">
<h3>Hintergrund: Daten und State</h3>
<ul>
<li>Serverseitige <b>Daten</b> und clientseitiger <b>State</b></li>
<li>Arten von State: <b>lokal</b> und <b>global</b></li>
<li>Überblick: 👉 Daten und State</li>
</ul>
</section>
<section>
<h3>Hintergrund: Globaler Zustand</h3>
<ul>
<li>
Man kann Zustand in <b>lokalen Zustand</b> und <b>globalen Zustand</b> einteilen
</li>
<li>
<b>Lokaler Zustand</b> ist Zustand, der "mehr oder weniger" einer Komponente zur
Verfügung steht
</li>
<li>
<b>Globaler Zustand</b> hingegen ist für die ganze Anwendung oder große Teile davon
zuständig
</li>
<li>Die Übgergänge sind fließend, es gibt keine fixe Definition</li>
<li>Beispiele für globalen Zustand: angemeldeter Benutzer, Theme</li>
</ul>
</section>
<section>
<h2>React Context API</h2>
<p>Kein Statemanagement, aber häufig zusammen erwähnt</p>
<p>"Dependency Injection"</p>
<p>Überblick: 👉 Anwendungshierachie mit State und Props (Miro)</p>
</section>
<!-- ============================================================================= -->
<section>
<h2>Im Detail: Context API</h2>
<p>Beispiel: <code>context-example/context-workspace</code></p>
<p>
In <code>Container.tsx</code> Anzeige der Border einschalten (<code
>hideBorder = false</code
>)
</p>
<p><code>CounterApp</code> rendern!</p>
<p>
In <code>Container.tsx</code> Anzeige der Renderings einschalten (<code
>showRenderings = true</code
>)
</p>
<p>(Material in <code>context-example/material/CounterContext.txt</code>)</p>
</section>
<section>
<h2>Context...</h2>
<p>
<em
>erlaubt das Durchreichen von Informationen ohne explizites angeben als Properties</em
>
</p>
<ul>
<li>funktioniert nur innerhalb einer Hierarchie-Ebene</li>
<li>es können beliebg viele (fachliche) Context definiert werden</li>
<li>besteht aus <code>Provider</code> und <code>Consumer</code></li>
<li>
<a href="https://reactjs.org/docs/context.html" target="_blank">Doku</a>
</li>
</ul>
</section>
<section>
<h2>Context Factory</h2>
<ul>
<li>
<em>erzeugt ein Objekt, mit <b>zwei Komponenten</b></em>
</li>
<li>
<code>Provider</code>, stellt Objekt mit Key-Value-Paaren zur Verfügung (der
Context-"Value")
</li>
<li>
<code>Consumer</code> wird in eigener Komponente verwendet, um auf einen Context
zuzugreifen ("versteckt" durch useContext Hook)
</li>
<li>
<pre><code class="line-numbers" data-leftpad>
import react from "React";
const AuthContext = React.createContext();
// erzeugt:
// AuthContext.Provider
// AuthContext.Consumer (mit Hooks API überflüssig)
export AuthContext;
</code></pre>
</li>
</ul>
</section>
<section data-markdown>
<textarea data-template>
## Context Factory mit TypeScript
* Der `createContext` kann ein Typ-Parameter übergeben werden, der den Context-Wert beschreibt
* `createContext` benötigt dann einen Default-Wert, der diesem Typen entspricht
* Der Default-Wert wird in der Anwendung in der Regel nicht verwendet
* Wird nur verwendet, wenn (fälschlich) auf den Kontext zugegriffen wird, ohne dass es einen Provider
gibt
* ```typescript
type IAuthContext = {
username: string | null;
onLogin(newUser: string): void;
onLogout(): void;
}
const AuthContext = React.createContext<IAuthContext>({
// Dummy-Implementierung vom Default Context
username: null,
onLogin() {},
onLogout() {}
});
export AuthContext;
```
</textarea>
</section>
<section data-markdown>
<textarea data-template>
## Context Provider
* _Eine React-Komponente, die einen Context zur Verfügung stellt_
* wird innerhalb einer eigenen Komponente eingebunden und zurückgeliefert
* Nimmt ein Objekt ("Context") mit beliebigen Werten entgegen (`value`-Property)
* Woher die Werte kommen (State, Props, anderer Kontext, ...) spielt keine Rolle!
* Alle Einträge des Objektes sind für die Konsumenten verfügbar
* ```typescript
const AuthContext = React.createContext<IAuthContext>(/*...*/); // wie gesehen
type AuthProviderProps = {
children: React.ReactElement
}
export function AuthProvider(props: AuthProviderProps) {
const [ currentUser, setCurrentUser ] = React.useState(null);
const contextValue: IAuthContext = {
// the current user
currentUser,
// function to set new user
function onLogin(name) { setCurrentUser(name) },
function onLogout() { setCurrentUser(null) }
};
return <AuthContext.Provider value={contextValue}>
{props.children}
</AuthContext.Provider>;
}
```
</textarea
>
</section>
<section>
<h2>useContext-Hook</h2>
<p><em>Zugriff auf die Werte aus dem Context</em></p>
<p>
In allen Komponenten unterhalb der Provider Komponente, kann mit
<code>useContext</code> auf den Kontext zugegriffen werden
</p>
<pre><code class="javascript">
import { AuthContext } from "auth-context";
function UserBadge() {
const { currentUser } = React.useContext(AuthContext);
return currentUser ? <h1>Welcome, {currentUser}<h1> : null;
}
</code></pre>
<p>Aufrufen einer Funktion aus dem Context</p>
<p>Ändert im Context den Zustand der Provider-Komponente</p>
<p>Alle Konsumer werden neu gerendert und können den neuen Wert verwenden</p>
<pre><code class="javascript">
function UserBadge() {
const { currentUser, logout } = React.useContext(AuthContext);
return currentUser ?
<><h1>Welcome, {currentUser}<h1><button onClick={logut}>Logout</button></>
: null;
}
</code></pre>
</section>
<section data-markdown>
<textarea data-template>
## Zugriff mit Custom Hook
* Übliches Pattern: für den Zugriff auf den Context wird ein eigener Hook zur Verfügung gestellt
* ```typescript
export function useAuthContext(): IAuthContext {
const authContext = useContext(AuthContext);
return authContext;
}
```
* ```typescript
import { useAuthContext } from "auth-context";
export default function UserBadge() {
const authContext = useAuthContext();
}
```
* "Versteckt" den Context (Implementierungsdetail!) vor der Anwendung
* Sieht aus wie fachliche API
* Kann (vergleichsweise einfach) gemockt werden
---
### Zugriff mit Custom Hook in TypeScript
* Beim Erzeugen des Context muss man (in TypeScript) einen Default Context angeben:
* ```typescript
const defaultContext: IAuthContext = {
// Dummy-Implementierung vom Default Context
username: null, onLogin() {}
}
const AuthContext = React.createContext<IAuthContext>(defaultContext);
```
* Das ist nicht immer (sinnvoll) möglich, so dass man auch `ContextType | null` verwenden könnte:
* ```typescript
const AuthContext = React.createContext<IAuthContext | null>(null);
```
* Nun liefert `useContext` aber immer auch `null` zurück, so dass die Verwendung jedes Mal überprüft werden muss:
* ```typescript
function UserBadge() {
const { username } = useContext(AuthContext); // ERR: Property 'username' does not exist on type 'IAuthContext | null'
}
```
* Mit dem Custom Hook kann eine Plausibilitätsprüfung durchgeführt werden und ggf. ein sprechender Fehler erzeugt werden:
* ```typescript
export function useAuthContext(): IAuthContext {
const authContext = useContext(AuthContext);
if (authContext === null) {
throw new Error("AuthContext not correctly initialized. Please wrap your application in a AuthContextProvider component");
}
return authContext;
}
```
---
### Ein "halbglobaler" Context
<!-- .slide: class="left" -->
* Ein Formular hat eine beliebige Menge von Feldern und Button
* Kann man da mit Context was machen? 🤔
* ```typescript
type FormState = Record<string, string>;
function PersonForm() {
const [formState, setFormState] = React.useState<FormState>({});
function onClearForm() {
setFormState({});
}
function onFieldChange(fieldname: string, value: string) {
setFormState({
...formState,
[fieldname]: value
});
}
return (
<Container title="PersonForm">
<FieldSet>
<Input name="firstname" formState={formState} onFieldChange={onFieldChange} />
<Input name="lastname" formState={formState} onFieldChange={onFieldChange} />
</FieldSet>
<ClearButton onClearForm={onClearForm} />
</Container>
);
}
```
---
## Übung: Ein Formular-Context
* *Baue einen Kontext, der Daten eines Formulars hält und diese verändern kann*
* Schritte:
* In `context-example/context-workspace` bitte Abhängigkeiten installieren und npm starten:
* ```bash
cd context-example/context-workspace
npm install
npm start
```
* In der `App.tsx`-Datei ist ein Formular implementiert (ohne Kontext).
* Die Formulardaten und Callback-Funktionen sollen in einen Kontext.
* Nähere Informationen findest Du direkt in der Datei.
* Mögliche Lösung findest Du in `steps/10_PersonForm_mit_context.tsx`
* Wenn Du fertig bist, bitte "Hand heben" in Zoom 🙋♀️
---
### Formular-Beispiel: was gäbe es für eine Alternative?
* Wenn wir _keinen_ Kontext haben wollen, aber ein wiederverwendbares Formular "Framework"? 🤔
* Man könnte `formState` und die Funktionen zum Ändern in einen Custom Hook packen
* ```typescript
function useForm() {
const [formState, setFormState] = React.useState<FormState>({});
function onClearForm() {
setFormState({});
}
function onFieldChange(fieldname: string, value: string) {
setFormState({
...formState,
[fieldname]: value
});
}
return { formState, onClearForm, onFieldChange };
}
function PersonForm() {
const { formState, setFormState, onFieldChange } = useForm();
return (
<div>
<FieldSet>
<Input name="firstname" formState={formState} onFieldChange={onFieldChange} />
<Input name="lastname" formState={formState} onFieldChange={onFieldChange} />
</FieldSet>
<ClearButton onClearForm={onClearForm} />
</div>
);
}
```
* Vorteile? Nachteile? 🤔
---
### Neuerungen in React 19
<!-- .slide: id="t-react19" -->
* [React 19](https://react.dev/blog/2024/02/15/react-labs-what-we-have-been-working-on-february-2024) wird im Laufe des Jahres erscheinen
* Darin wird sich auch die Context API verändern:
* Es wird eine `Context`-Komponente geben (vermutlich statt `createContext` bzw. `Context.Provider`)
* `useContext` wird durch den `use`-Hook ersetzt
* Der `use`-Hook ist flexibler als die bisherigen Hooks
* Kann überall verwendet werden (z.B. in `if`)
* Das ganze ist abwärtskompatibel!
* Die APIs `useCallback`, `useMemo` und `memo` werden durch den **React Compiler** (ehem. Codename: "Forget") ersetzt
---
### Renderverhalten von Context
<!-- .slide: id="t-context-render" -->
* <!-- .element: class="demo" --> Eine Komponente in den Kontext (`CounterContext`) hinzufügen
* <!-- .element: class="demo" --> Was passiert und warum? 🤔
---
### Renderverhalten von Context
* Die `children` werden fertig übergeben
* Deren Renderzyklus wird von der Komponente bestimmt, die die `children` erzeugt
* Das ist hier der Aufrufer von `Form`
* ```typescript
function Form({ children }: FormProps) {
// ...
return (
<Container title="Form">
<FormContext.Provider
value={ /* ... */ }
>
{children}
</FormContext.Provider>
</Container>
);
}
```
---
### Renderverhalten von Context #2
* Unterdrücken von erneutem Rendern
* <!-- .element: class="demo" --> `CounterDispay` aufteilen in lesen und schreiben
---
### Wie können wir das Rendern von Konsumenten unterdrücken?
* Der `Clear`-Button wird immer gerendert, auch wenn der sich gar nicht ändert 😢
* Woran liegt das?
* Was müssen wir tun, damit der nicht gerendert wird?
* Wenn sich _irgendetwas_ im Kontext ändert, werden _alle_ Konsumenten neu gerendert
* Auch wenn sie auf Teile des Kontextes zugreifen, der sich nicht verändert hat.
* Um den `Clear`-Button vom Rendern auszuschliessen, muss also die `onClearForm`-Funktion
aus dem `FormContext`
* Dazu wird ein neuer Context erzeugt. Je nachdem kann eine Komponente dann den einen oder
anderen (oder beide) konsumieren.
---
### Rendern optimieren
<!-- .slide: class="left" -->
* Beispiel: zwei Kontexte für das Formular
* ```typescript
// FormContext wie bisher, aber ohne onFieldChange und ohne onClearForm
// Neuer Context:
type IFormChangeContext = {
onClearForm(): void;
onFieldChange(fieldname: string, value: string): void;
};
const FormChangeContext = createContext<IFormChangeContext | null>(null);
```
* ```typescript
function Form({ children }: FormProps) {
// state + Callback-Funktionen wie bisher
return (
<FormContext.Provider
value={{
formState
}}
>
<FormChangeContext.Provider value={{ onClearForm, onFieldChange }}>
{children}
</FormChangeContext.Provider>
</FormContext.Provider>
);
}
```
* Geht das?
* <!-- .element: class="demo" --> Prüfen!
---
### Memoisieren von Komponenten
* Wenn eine Komponente gerendert wird, werden grundsätzlich _alle_ Unterkomponenten gerendert
* Das bedeutet im gezeigten Fall:
* beim Rendern von `Form` wird *immer* auch `FormContext.Provider` gerendert und *immer* auch `FormChangeContext.Provider`.
* Wir haben also noch nichts gewonnen.
* Oder? 🤔
* Eventuell haben wir immerhin eine sauberere Architektur durch Trennung in "lesenden" und "modifizierenden" Context
---
### Memoisieren von Komponenten
* Man kann das Rendern von Komponenten durch "Memoisieren" (eine Art Caching) unterdrücken
* Komponenten werden dann nur gerendert, wenn sich ihre Properties verändert haben.
* Variante 1 mit `React.memo`
* ```typescript
const FormChangeContextProvider = React.memo(
function FormChangeContextProvider({ children, onClearForm, onFieldChange }) {
return <FormChangeContext.Provider value={{ onClearForm, onFieldChange }}>
{children}
</FormChangeContext.Provider>
}
);
```
* Für den Verwender ist das transparent:
* ```typescript
function Form({ children }: FormProps) {
// ...
return <FormContext.Provider value={/* ... */}>
<FormChangeContextProvider
onClearForm={onClearForm}
onFieldChange={onFieldChange}
>{children}</FormChangeContextProvider/>
</FormContext.Provider>
}
```
* Diese Komponente wird nun neugerendert, wenn sich eines der Properties ändert (`children`, `onClearForm` und/oder `onFieldChange`)
* Reicht das? 🤔
---
### Memoisieren von Komponenten
* Eine Komponente ist eine Funktion
* Die Funktion wird bei jedem Rendern neu ausgeführt
* Alle Dinge, die darin erzeugt werden, sind bei jedem rendern "neu" (neue Referenz)!
* ```typescript
function Form() {
const onClearForm = () => setFormState({});
return ...;
}
```
* `onClearForm` ist bei jedem Rendern von `Form` "neu"
* Dasselbe gilt für Objekte
* ```typescript
function Form() {
const emptyArray = [];
```
* `emptyArray` ist bei jedem Rendern von `Form` "neu"
* Wir müssen also dafür sorgen, dass diese Werte und Funktionen "stabil" über mehrere Renderzyklen sind
---
### useMemo und useCallback
* Um Werte über mehrere Renderzyklen zu erhalten, kann man `useMemo` (Werte) und `useCallback` verwenden
* (`useCallback` ist eine Vereinfachung von `useMemo` für Funktionen)
* Die Verwendung von beiden erinnert an `useEffect` 😱:
* Es gibt eine Callback-Funktion (die liefert den Wert bzw. die Funktion zurück)
* Es gibt ein Dependency-Array, das bestimmt, wann der Wert/Funktion ungültig ist.
* Das Dependency-Array _muss_ angegeben werden - im Gegensatz zu `useEffect`. Warum?
* ```typescript
function Form({title, subtitle}) {
// Funktion wird nur einmal erzeugt
const onClearForm = useCallback( () => setFormState({}), []);
// Array wird immer neu erzeugt, wenn title oder subtitle sich ändert
const titleArray = useMemo( () => { return [title, subtitle] }, [title, subtitle] );
}
```
* Damit haben wir nun unser Problem gelöst?
---
### useMemo und useCallback: stale Values
* Was passiert denn hier:
* ```typescript
function Form() {
const [formState, setFormState] = React.useState({});
const onFieldChange = useCallback(function(fieldname, value) {
setFormState({
...formState,
[fieldname]: value
});
}, []);
}
```
* Der Wert von `formState` wird beim erstmaligen rendern innerhalb der Callback-Funktion "eingefroren"
* Werte in einer Funktion (Closure) haben immer den Wert von dem Zeitpunkt, zu dem die Funktion ausgeführt wurde
* Das fällt häufig gar nicht auf
* Hier schon, denn formState verbleibt immer auf dem ersten Wert 😢
* Was können wir tun?
---
### useMemo und useCallback: stale Values
* Genau wie bei `useEffect` können wir bei `useMemo` und `useCallback` bestimmen, wann der gecachte Wert ungültig ist
* Im gezeigten Beispiel wäre das, wenn `formState` sich ändert:
* ```typescript
function Form() {
const [formState, setFormState] = React.useState({});
const onFieldChange = useCallback(function(fieldname, value) {
setFormState({
...formState,
[fieldname]: value
})
}, [ formState ]);
}
```
* Ist das gut?
* Es kommt drauf an!
---
### useMemo und useCallback: stale Values
* Grundsätzlich kann es richtig sein, bei Änderung von Werten auch neue Werte und Funktionen zu erzeugen
* Wir wollen ja nicht "für immer" cachen, sondern nur so lange "wie es geht"
* Im gezeigten Fall würde das für die Form bedeuten:
* Immer wenn sich der State ändert, ändert sich `onFieldChange`
* Dadurch wird dann auch `FormChangeContextProvider` neu gerendert
* Das bedeutet, wir haben nichts gewonnen: wenn sich der Zustand ändert, wir dauch der Clear-Button neugerendert
---
### Callback-Funktion von useState
* Im konkreten Fall können wir weiter optimieren, in dem wir beim Setzen des Zustandes eine Callback-Funktion angeben
* Die Callback-Funktion wird von React aufgerufen und ihr wird der _aktuellste_ Werte des States übergeben
* Die Callback-Funktion liefert dann den neuen State zurück
* Damit haben wir keinen Wert mehr in der Closure, der "stale" sein könnte, und es reicht, wenn wir die Funktion
einmal erzeugen (leeres Dependency Array)
* ```typescript
function Form() {
const [formState, setFormState] = React.useState({});
const onFieldChange = useCallback(function(fieldname, value) {
setFormState( () => return {
...formState,
[fieldname]: value
})
}, [ ]);
}
```
---
### Memoisieren: memo, useCallback und useMemo
<!-- .slide: class="left" -->
* Sollte man das immer und überall machen?
* Alles mit useMemo und useCallback umschliessen?
* ```typescript
function Greet({name}) {
const greeting = useMemo(`Hello, ${name}!`, [name]); // 🤔
return <h1>{greeting}</h1>
}
```
* Was spricht dafür? Was spricht dagegen? 🤔
---
### Ausblick: Memoisieren: memo, useCallback und useMemo
* Es soll einen eigenen Compiler für React geben ("React forget")
* Der soll die Abhängigkeiten für `useCallback` und `useMemo` automatisch erkennen
* Dann wird die Arbeit damit hoffentlich einfacher
---
### Übung: Memoisieren von Komponenten
* Teile den `FormContext` in zwei Teile:
* `FormContext`: die beiden Callback-Funktionen entfernen
* `FormChangeContext`: hierein die beiden Funktionen zum Modifizieren des Zustands
* Einzige Änderung im Verhalten: die `ClearButton`-Komponente soll den neuen Kontext verwenden (und sich _nicht_ neu rendern, wenn das Formular ausgefüllt wird)
* Kannst Du statt der gesehenen `FormChangeContextProvider` eine Lösung mit `React.useMemo` statt `React.memo` bauen? 😳
* Wenn Du vorhin nicht fertig geworden bist, oder deine Lösung nicht funktioniert, kopiere `steps/10_PersonForm_mit_context.tsx` in deine `App.tsx`-Datei als Basis für die Übung
* Eine fertige Lösung findest Du in `steps/20_PersonForm_mit_context_und_memo.tsx`
* Wenn Du fertig bist, bitte die Hand heben ✋
</textarea>
</section>
<!-- ============================================================================= -->
<section data-markdown>
<textarea data-template>
<!-- .slide: id="t-tanstack-query" -->
# Mordernes Data Fetching in React
---
## Modernes Data Fetching in React
* Mit `useEffect`, `fetch` und `axios` stehen dir "Low-Level-APIs" zur Verfügung, um mit serverseitigen Daten zu arbeiten
* Diese APIs sind React (`useEffect`) bzw. Browser (`fetch`) Standard APIs
* Es gibt aber spezialisierte Bibliotheken, die das Arbeiten mit Daten erleichtern können.
* [TanStack Query](https://tanstack.com/query/latest) / und [Vercel SWR](https://swr.vercel.app/): Zwei Bibliotheken zum Laden/Speichern von Daten inklusive Cache-Funktion
* [Redux Toolkit Query](https://redux-toolkit.js.org/rtk-query/overview): Arbeiten mit APIs in Redux-Anwendungen
* [Apollo GraphQL Client](https://www.apollographql.com/docs/react/): Client für GraphQL APIs mit Cache und Statemanagement Möglichkeiten
* Diese Bibliotheken haben alle ähnliche Konzepte:
* Hooks zum Laden/Speichern von Daten
* globales Caching von Daten (auch zur Sicherstellung der konsistenten Darstellung)
* Strategien zur Aktualisierung von Daten (auch automatisch im Hintergrund)
---
## TanStack Query
### Schritt-für-Schritt: Laden von Daten mit "TanStack Query"
* 👉 `PostListPage`
* 👉 später: `PostEditorPage`
* 👉 später: Custom Hooks
* 👉 später: zod
* 👉 Arbeiten in `advanced/workspace`
---
### Der QueryClient
* Zentrales Konfigurationsobject: `QueryClient`
* React-unabhängig
* Wird beim Starten der Anwendung initialisiert
* Oft reichen Default-Einstellung
* Es können aber z.B. globale Refetch-Policies eingestellt werden
* Das Objekt wird per QueryClientProvider in die Anwendung gereicht
* ```typescript
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false
}
}
});
root.render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
);
```
---
### Laden von Daten: useQuery
* [Queries](https://tanstack.com/query/latest/docs/react/guides/queries) werden mit dem `useQuery`-Hook ausgeführt
* [Der `useQuery`-Hook](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery) erwartet ein Konfigurationsobjekt
* `queryKey`: Array mit Query Keys (zur Interaktion mit dem Cache)
* `queryFn`: Funktion zum Laden der Daten
* Weitere Konfigurationen (optional)
* ```typescript
import { useQuery } from "react-query";
import { loadBlogPosts } from "./blog-api";
function BlogListPage() {
const result = useQuery({queryKey: ['posts'], queryFn: loadBlogPosts});
// ...
}
```
---
### Query Function
* `useQuery` erwartet eine [Query-Function](https://tanstack.com/query/latest/docs/react/guides/query-functions), die den eigentlichen Request ausführt
* Die Signatur ist fast beliebig, die Funktion muss aber ein Promise zurückliefern:
* Wenn die Daten erfolgreich geladen wurden, muss das Promise mit den Daten "aufgelöst" werden
* Wenn es einen Fehler gab, muss die Funktion einen Fehler werfen
* ```typescript
// async function gibt IMMER ein Promise zurück
export async function loadBlogPost(postId) {
const response = await fetch("http://localhost:7000/posts" + postId);
if (!response.ok) {
throw new Error("Could not load blog post: " + response.status);
}
return response.json();
}
```
---
### Rückagebwert von `useQuery` (Query Ergebnis)
* `useQuery` liefert ein Objekt zurück:
* `isLoading`: Der Query lädt noch (und es sind keine Daten im Cache)
* `isSuccess`: Daten sind geladen
* `isError`: Es ist ein Fehler aufgetreten
* `data` enthält die geladenen Daten
* `error`: Fehlerobjekt aus der Query-Funktion
* Weitere [siehe Doku](https://tanstack.com/query/latest/docs/react/reference/useQuery)
---
### Query Keys
* Mit den [Query Keys](https://tanstack.com/query/latest/docs/react/guides/query-keys) wird ein Ergebnis im Cache gespeichert
* Ein Query Key besteht aus einem Array von Werten
* Üblicherweise ist es ein Name (z.B. "posts") und dann ggf. weitere Parameter, zum Beispiel die Id eines Posts ("P1")
oder die Sortierreihenfolge
* Also alle Daten, die den Query exakt beschreiben
* ```typescript
import { useQuery } from "react-query";
import { loadBlogPosts } from "./blog-api";
function BlogPage({blogPostId}) {
// Für jeden Aufruf mit einer neuen blogPostId
// wird das Ergebnis separat in den Cache gelegt
const result = useQuery({
queryKey: ['blogPost', blogPostId],
queryFn: () => loadPost(blogPostId)
});
// ...
}
```
* Wenn ein Query mit denselben Query Keys in mehr als einer Komponente ausgeführt wird
* stellt TanStack Query sicher, dass der Query nur einmal ausgeführt wird
* wenn sich das Ergebnis ändert, werden alle Komponenten, die den Query verwenden,
automatisch aus dem Cache aktualisiert
* 👉 dieses Verhalten sehen wir uns später noch an
---
## Übung: Daten lesen mit TanStack Query
* **Vorbereitung**
* Workspace in der IDE öffnen: `react18-training/advanced/workspace`
* Schritt 1: Backend starten
* ```bash
cd react18-training/blog-example/backend-rest
npm start
```
* Danach sollte unter [http://localhost:7000/posts](http://localhost:7000/posts)
die Liste mit den (JSON-)Posts zurückkommen
* Schritt 2: Frontend (Vite) starten
* ```bash
cd react18-training/advanced/workspace
npm start
```
* Die Anwendung sollte auf [http://localhost:3000](http://localhost:3000) laufen
* Die Anwendung benutzt noch `useEffect` und `fetch`... das wollen wir ändern!
---
### Übung: Daten lesen mit TanStack Query
* In der Komponente `PostListPage` wird `fetch` bzw. `useEffect` zum Laden der Daten verwendet
* Stelle diese Komponente auf `useQuery` um.
* Zeige eine Warte-Meldung an, während die Daten geladen werden
* Du kannst den Request künstlich langsam machen, in dem Du an die Url `?slow` hängst
* TanStack Doku:
* [Queries](https://tanstack.com/query/v5/docs/framework/react/guides/queries)
* [useQuery](https://tanstack.com/query/v5/docs/framework/react/reference/useQuery)
* Mögliche Lösung: `steps/10_useQuery`
---
### TanStack Query: Mutations
* [Mutations](https://tanstack.com/query/latest/docs/framework/react/guides/mutations) werden verwendet, um Daten zu *verändern* (speichern, löschen)
* Der entsprechende Hook heißt [`useMutation`](https://tanstack.com/query/latest/docs/framework/react/reference/useMutation)
* Dessen API ist vergleichbar mit `useQuery`
* Auch der `useMutation`-Hook liefert Informationen über den Zustand der Mutation zurück
* ```typescript
import { useMutation } from "react-query";
import { savePost } from "./blog-api";
function PostEditorPage() {
const mutation = useMutation({
mutationFn: savePost,
onSuccess() {
// optional: wird aufgerufen, wenn die Mutation erfolgreich war
// ...
}
});
if (mutation.status === "error") {
return <h1>Error!</h1>;
}
if (mutation.status === "loading") {
return <h1>Saving, please wait!</h1>;
}
// ...
}
```
---
### TanStack Query: Mutations
* Im Gegensatz zu `useQuery` wird eine Mutation aber nicht automatisch ausgeführt, sondern wird explizit gestartet
* Dazu liefert `useMutation` die Funktion `mutate` zurück
* Übergeben wird der Funktion die zu schreibenden Daten
* ```typescript
const mutation = useMutation(/* ... */ );
function saveBlogPost(newPost: NewBlogPost) {
mutation.mutate(newPost);
}
```
---
### Parameter für die Mutations
* Üblicherweise benötigt eine Mutation Daten, die erst bei der Ausführung `mutate` feststehen
* Dazu kann der `mutate`-Funktion genau **ein** Parameter übergeben werden
* Wie dieser aussieht bestimmt ihr in der Definition der Mutation selbst
* Dieser Parameter entspricht nämlich dem ersten Parameter der `mutationFn`:
* ```typescript
const addPostMutation = useMutation({
mutationFn(newBlogPost: NewBlogPost) { /* ... */ }
})
```
* Wenn ihr mehr als einen "logischen" Parameter benötigt, müsst ihr ein Objekt verwenden:
* ```typescript
type AddCommentParam = { postId: string, comment: string };
const addCommentMutation = useMutation({
mutationFn(param: AddCommentParam) {
const url = `/api/posts/${postId}/comments;
fetch(url, {
body: JSON.stringify({comment: param.comment})
});
}
});
```
---
### Arbeiten mit dem Ergebnis
* Wenn eine Mutation ausgeführt wurde, bekommt ihr `data` bzw. `error` zurück
* Damit könnt ihr - wie bei `useQuery` - nach der Ausführung einer Mutation die UI aktualisieren, um zum Beispiel Fehlermeldungen anzuzeigen
* ```typescript
function PostEditor() {
const savePostMutation = useMutation(/*...*/);
return <form>
{ /* ... */}
{saveMutation.isError && <p>Fehler beim Speichern des Posts: {String(saveMutation.error)}</p>}
{saveMutation.isSuccess && <p>Der Blogpost wurde erfolgreich gespeichert!</p>}
</form>
}
```
---
### Auf das Ergebnis warten
* Um direkt nach Beendingung einer Mutation weitere Aktionen auszuführen, kann man `on`-Callback-Funktionen bzw. [`mutateAsync`](https://tanstack.com/query/latest/docs/framework/react/guides/mutations#promises) verwenden
* `onSuccess` und `onFailure` könnt ihr bei `useMutation` angeben. Das ist sinnvoll für Aktionen, die immer ausgeführt werden sollen
* Sehen wir später noch im Zusammenhang mit Caching.
* Mit `mutateAsync` könnt ihr _in einer Komponente_ auf das Ergebnis der Mutation warten. Das ist sinnvoll, wenn man Komponenten-spezfische Aktionen ausführen möchte.
* `mutateAsync` liefert ein Promise mit den Daten der Mutation zurück.