Querying APIs

Querying APIs

basic knowledge:

  • asynchronous behavior in JavaScript
  • network requests via fetch

API queries in React:

  • libraries, e.g. react-query, react-swr
  • built into frameworks, e.g. remix
  • built-in useEffect hook (can be complex)

Network requests in JavaScript

Network requests in JavaScript

promises: modern way of handling asynchronous code:

  • promises and async / await
  • promises and .then()

sending network requests (based on promises):

  • fetch

Network requests in JavaScript

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;
}

Network requests in JavaScript

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;
}

Querying APIs from React: fundamental example

Querying APIs from React: fundamental example

const [data, setData] = useState(null);
async function loadData() {
  const newData = await fetchData();
  setData(newData);
}
<button onClick={loadData}>
  load some data from an API
</button>

Querying APIs from React: fundamental example

Task: in our todo application, load todos from this API upon a button click:

https://jsonplaceholder.typicode.com/todos

Query libraries

Query libraries

functionality that is commonly desired:

  • tracking the loading status (loading / success / error)
  • automatically sending a query:
    • when a component is first included
    • when a query parameter has changed
  • caching previous responses for reuse (globally)
  • cancelling / ignoring obsolete queries

Query libraries

functionality can be achieved "manually" by using state, side effects and context

... or by using a query library

Query libraries

query libraries:

  • react query
  • swr
  • apollo (for GraphQL APIs)

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}`),
});

Query libraries

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]);

React Query

React Query

fetching data with react query:

const [searchTerm, setSearchTerm] = useState('foo');

const { status, data } = useQuery({
  queryKey: ['search', searchTerm],
  queryFn: () => fetchJson(`/api/search/${searchTerm}`),
});

React Query: setup

npm packages:

  • @tanstack/react-query
  • @tanstack/react-query-devtools

React Query: setup

global setup in index.tsx / main.tsx (surrounding <App />):

import {
  QueryClient,
  QueryClientProvider,
} from 'react-query';
const queryClient = new QueryClient();
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>

React Query: loading status

The loading status is available automatically:

status: loading / success / error

React Query: loading status

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: cache

React query uses a global store/cache through a provider component

React Query: keys

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]

React Query: keys

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

React Query

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}`
    ),
});

React Query

data display:

if (rateQuery.isLoading) {
  return <LoadingIndicator />;
} else if (rateQuery.isError) {
  return <ErrorMessage />;
} else {
  return <RateDisplay data={rateQuery.data} />;
}

React Query

exercise:

work with the "Rick and Morty" API and let the user explore characters (by clicking next or previous)

https://rickandmortyapi.com/

React Query: Intermediate

Devtools

enabling a devtools popup during development:

import { ReactQueryDevtools } from 'react-query/devtools';

// ...

  <QueryClientProvider client={queryClient}>
    <App />
    <ReactQueryDevtools initialIsOpen={false} />
  </QueryClientProvider>

Query start on user action

by default: query starts when a component is mounted

alternative behavior: query starts on a specific user action (e.g. button click, hitting enter)

Query start on user action

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);

Query start on user action

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),
});

Mutations

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 });
}
// ...