Grundwissen:
API-Anfragen in React:
useEffect
-Hook (kann komplex sein)Promises: moderne Möglichkeit, asynchronen Code zu verwenden:
async
/ await
.then()
Senden von Netzwerkanfragen (basierend auf Promises):
fetch
asynchrone Funktion, die Todos von einem API lädt:
// todosApi.ts
async function fetchTodos(): Promise<Array<Todo>> {
const url = 'https://jsonplaceholder.typicode.com/todos';
const res = await fetch(url);
if (!res.ok) {
throw new Error('could not fetch data from network');
}
const apiTodos: Array<any> = await res.json();
// convert data format (don't include userId)
const todos = apiTodos.map((todo) => ({
id: todo.id,
title: todo.title,
completed: todo.completed,
}));
return todos;
}
mögliche Hilfsfunktion zum Laden von JSON-Daten:
async function fetchJson(url): Promise<any> {
const res = await fetch(url);
if (!res.ok) {
throw new Error(res);
}
const data = await res.json();
return data;
}
const [data, setData] = useState(null);
async function loadData() {
const newData = await fetchData();
setData(newData);
}
<button onClick={loadData}>
load some data from an API
</button>
Aufgabe: In unserer bestehenden Todo-Anwendung, lade beim Klick eines Buttons Todos von diesem API:
Funktionalität, die meist gewünscht ist:
gewünschte Funktionalität ist umsetzbar mittels state, side effects und context (allerdings kompliziert)
... oder mit Hilfe einer Query Library
Query Libraries:
Beispiel: Verwalten einer Suchanfrage mit react query
const [searchTerm, setSearchTerm] = useState('foo');
const { status, data } = useQuery({
queryKey: ['search', searchTerm],
queryFn: () => fetchJson(`/api/search/${searchTerm}`),
});
Beispiel: Nachbau von Teilen dieser Funktionalität in reinem React:
const [searchTerm, setSearchTerm] = useState('foo');
const [status, setStatus] = useState('loading');
const [data, setData] = useState<any>(null);
useEffect(() => {
let ignore = false;
async function loadData() {
setStatus('loading');
setData(null);
const url = `/api/search/${searchTerm}`;
try {
const data = await fetchJson(url);
if (!ignore) {
setStatus('success');
setData(data);
}
} catch {
setStatus('error');
}
}
loadData();
return () => {
ignore = true;
};
}, [searchTerm]);
Laden von Daten mit react query:
const [searchTerm, setSearchTerm] = useState('foo');
const { status, data } = useQuery({
queryKey: ['search', searchTerm],
queryFn: () => fetchJson(`/api/search/${searchTerm}`),
});
npm-Pakete:
globales Setup in index.tsx / main.tsx (umschließt <App />
)
import {
QueryClient,
QueryClientProvider,
} from 'react-query';
const queryClient = new QueryClient();
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
der Ladestatus ist automatisch verfügbar:
status
: loading
/ success
/ error
typische Verwendung des Ladestatus:
const { status, data } = useQuery(/* ... */);
if (status === 'loading') {
return <LoadingIndicator />;
} else if (status === 'error') {
return <ErrorMessage />;
} else {
return <DataDisplay data={data} />;
}
React Query verwendet einen globalen Store/Cache mit Hilfe einer Provider-Komponente
jede Query wird durch einen eindeutigen Key identifiziert / mitverfolgt (der mehrere Teile haben kann)
Beispiele für keys in einem Onlineshop:
["product", 123]
["order", 7453]
Kompliziertere Query, die ein Objekt im Key enthält:
[
"product-search",
{
"category": "phones",
"maxPrice": 700,
"color": "black"
}
]
mögliche API-URL dafür:
/api/product-search?category=phones&maxPrice=700&color=black
Beispiel: Laden von Wechselkursdaten:
const [from, setFrom] = useState('usd');
const [to, setTo] = useState('eur');
const rateQuery = useQuery({
queryKey: ['exchange-rate', { from: from, to: to }],
queryFn: () =>
fetchJson(
`https://api.exchangerate.host/convert?from=${from}&to=${to}`
),
});
Anzeigen der Daten:
if (rateQuery.isLoading) {
return <LoadingIndicator />;
} else if (rateQuery.isError) {
return <ErrorMessage />;
} else {
return <RateDisplay data={rateQuery.data} />;
}
Ãœbung:
Arbeite mit der "Rick and Morty" API und lasse den User Charactere erkunden (durch klicken von next oder previous)
aktivieren eines Devtools-Popups während der Entwicklung:
import { ReactQueryDevtools } from 'react-query/devtools';
// ...
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
standardmäßig: Query startet, sobald eine zugehörige Komponente eingebunden wird, die den Hook aufruft
alternatives Verhalten: Query startet bei einer bestimmten Benutzerinteraktion (z.B. Buttonklick)
Beispiel: State für eine Suchfunktionalität:
// text content of an input (changes when user types)
const [search, setSearch] = useState('');
// active search (changes when user hits enter)
const [submittedSearch, setSubmittedSearch] = useState<
string | null
>(null);
Aktivieren einer Query, wenn der Benutzer zum ersten mal eine Suche absendet:
const searchQuery = useQuery({
queryKey: ['search', submittedSearch],
enabled: submittedSearch !== null,
queryFn: () =>
fetchJson(`/api/search/${submittedSearch}`),
});
Definieren von Mutationen:
import { useMutation, useQueryClient } from 'react-query';
import { apiAddTodo } from './todosApi';
// ...
const queryClient = useQueryClient();
function invalidateTodos() {
queryClient.invalidateQueries({ queryKey: ['todos'] });
}
const addTodoMutation = useMutation({
mutationFn: apiAddTodo,
onSuccess: invalidateTodos,
});
// this function is connected to a submit form
function handleAddTodo(newTitle) {
addTodoMutation.mutate({ title: newTitle });
}
// ...