basic knowledge:
API queries in React:
useEffect
hook (can be complex)promises: modern way of handling asynchronous code:
async
/ await
.then()
sending network requests (based on promises):
fetch
asynchronous function that fetches todos from an API:
// 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;
}
possible helper function for fetching JSON data:
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>
Task: in our todo application, load todos from this API upon a button click:
functionality that is commonly desired:
functionality can be achieved "manually" by using state, side effects and context
... or by using a query library
query libraries:
example: managing a search query with react query
const [searchTerm, setSearchTerm] = useState('foo');
const { status, data } = useQuery({
queryKey: ['search', searchTerm],
queryFn: () => fetchJson(`/api/search/${searchTerm}`),
});
example: reproducing some of this functionality in pure 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]);
fetching data with react query:
const [searchTerm, setSearchTerm] = useState('foo');
const { status, data } = useQuery({
queryKey: ['search', searchTerm],
queryFn: () => fetchJson(`/api/search/${searchTerm}`),
});
npm packages:
global setup in index.tsx / main.tsx (surrounding <App />
):
import {
QueryClient,
QueryClientProvider,
} from 'react-query';
const queryClient = new QueryClient();
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
The loading status is available automatically:
status
: loading
/ success
/ error
typical use of the loading status:
const { status, data } = useQuery(/* ... */);
if (status === 'loading') {
return <LoadingIndicator />;
} else if (status === 'error') {
return <ErrorMessage />;
} else {
return <DataDisplay data={data} />;
}
React query uses a global store/cache through a provider component
each query is identified / tracked by a unique key - which can have multiple parts
examples of query keys in an online shop:
["product", 123]
["order", 7453]
more complicated query key which includes an object:
[
"product-search",
{
"category": "phones",
"maxPrice": 700,
"color": "black"
}
]
possible API query URL:
/api/product-search?category=phones&maxPrice=700&color=black
example: loading exchange rates
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}`
),
});
data display:
if (rateQuery.isLoading) {
return <LoadingIndicator />;
} else if (rateQuery.isError) {
return <ErrorMessage />;
} else {
return <RateDisplay data={rateQuery.data} />;
}
exercise:
work with the "Rick and Morty" API and let the user explore characters (by clicking next or previous)
enabling a devtools popup during development:
import { ReactQueryDevtools } from 'react-query/devtools';
// ...
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
by default: query starts when a component is mounted
alternative behavior: query starts on a specific user action (e.g. button click, hitting enter)
example: state for a search functionality:
// 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);
only activate a query when the user has submitted for the first time:
const searchQuery = useQuery({
queryKey: ['search', submittedSearch],
enabled: submittedSearch !== null,
queryFn: () => fetchSearch(submittedSearch as string),
});
writing mutations:
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 });
}
// ...