Table of contents
- API call using fetch API
- Alternate to fetch API - Axios
- Digging deeper with Axios
- Request method aliases
- axios.request(config)
- axios.get(url[, config])
- axios.delete(url[, config])
- axios.head(url[, config])
- axios.options(url[, config])
- axios.post(url[, data[, config]])
- axios.put(url[, data[, config]])
- axios.patch(url[, data[, config]])
- axios.postForm(url[, data[, config]])
- axios.putForm(url[, data[, config]])
- axios.patchForm(url[, data[, config]])
- Customizing Axios
- API call in a react application
- Conclusion
Interacting with external applications is a necessary thing when we work on front-end applications by API calls. APIs allow different software applications to communicate with each other, enabling developers to access and retrieve data from various sources. There were many ways to API calls, like fetchAPI, XMLHttpRequest (xhr) & etc.
Here we will discuss fetching API calls and some tools for API calls.
API call using fetch API
The Fetch API provides an interface for fetching resources (including across the network). It is a more powerful and flexible replacement for XMLHttpRequest.
The Fetch API provides a JavaScript interface for accessing and manipulating parts of the protocol, such as requests and responses. It also provides a global fetch() method that provides an easy, logical way to fetch resources asynchronously across the network.
Example of fetch API
Here is a simple API call using fetch API
const url = "https://jsonplaceholder.typicode.com/todos"
// using async-await
async function getData(){
const response = await fetch(url)
const data = await response.json()
return data
}
// using then method
function getData() {
fetch(url)
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.log(error))
}
I have shown two types of API calling methods, in this article we will use the first one, to how we catch the error in the first method, Okay, for that we have to wrap the API call with a try-catch
block like this.
const url = "https://jsonplaceholder.typicode.com/todos"
// using async-await
async function getData(){
try{
const response = await fetch(url)
const data = await response.json()
return data
}catch(error){
// return the error or throw the error
}
}
Let's call another API call with more options.
const url = "https://jsonplaceholder.typicode.com/todos"
async function postData(data = {}) {
// Default options are marked with *
const response = await fetch(url, {
method: "POST", // *GET, POST, PUT, DELETE, etc.
mode: "cors", // no-cors, *cors, same-origin
cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
credentials: "same-origin", // include, *same-origin, omit
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
redirect: "follow", // manual, *follow, error
referrerPolicy: "no-referrer", // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
body: JSON.stringify(data), // body data type must match "Content-Type" header
});
return response.json() // parses JSON response into native JavaScript objects
}
const data = await postData({ title: 'Update the API call' })
console.log(data) // JSON data parsed by `response.json()` call
// {"title": "Update the API call", "id": 201}
You can pass your header by the example above or you can also pass a header instance
const myHeaders = new Headers({
"Content-Type": "text/plain",
"Accept": "application/json"
"X-Custom-Header": "ProcessThisImmediately",
})
Canceling a request
Sometimes we need to cancel an API call after calling the API, To abort incomplete fetch()
operations, fetch-API can do it easily with the AbortController
and AbortSignal
interfaces.
const controller = new AbortController()
const signal = controller.signal
const url = "https://jsonplaceholder.typicode.com/todos"
const fetchBtn = document.querySelector("#fetch-todos")
const abortBtn = document.querySelector("#abort")
fetchBtn.addEventListener("click", async () => {
try {
const response = await fetch(url, { signal })
console.log("Fetch complete", response)
} catch (error) {
console.error(`Fetch error: ${error.message}`)
}
});
abortBtn.addEventListener("click", () => {
controller.abort()
console.log("Fetch aborted")
});
In the current world, all the API responses are within a few milliseconds so you can test this API call by modifying your network interceptor to slow 3G connection.
Upload file using fetch API
In modern web applications, there must be file upload requirements, so how we can do it with fetch API, for the file upload methodology we have to add the data as FormData
is an example below.
async function upload(formData) {
try {
const response = await fetch("https://example.com/profile/avatar", {
method: "PUT",
body: formData,
});
const result = await response.json();
console.log("Success:", result);
} catch (error) {
console.error("Error:", error);
}
}
const formData = new FormData()
const fileField = document.querySelector('input[type="file"]')
formData.append("username", "msar")
formData.append("avatar", fileField.files[0])
upload(formData)
Enough for fetch API if you want to learn more about the fetch API you can browse MDN documentation or JavaScript.info docs.
Alternate to fetch API - Axios
There were many tools for API calls, Axios is a great alternative tool for API calls in JS, and Axios is a simple promise-based HTTP client for the browser and node.js. Axios provides a simple-to-use library in a small package with a very extensible interface.
Loading the Axios library
To load the Axios library you can add this line to your HTML page <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
or you can add the package via npm/yarn npm install axios
or yarn add axios
in this article we use as yarn package.
Basic example to send an API call using Axios.
import axios from 'axios'
// Make a request for a user with a given ID
axios.get('/user?ID=12345')
.then(function (response) {
// handle success
console.log(response)
})
.catch(function (error) {
// handle error
console.log(error)
})
.finally(function () {
// always executed
})
// Want to use async/await? Add the `async` keyword to your outer function/method.
async function getUser() {
try {
const response = await axios.get('/user?ID=12345')
console.log(response)
} catch (error) {
console.error(error)
}
}
In this example, the API request will send an API call to this URL, /user
Digging deeper with Axios
We can call the APIs via Axios in many ways, we can pass a full config to the Axios instance
const base_url "https://jsonplaceholder.typicode.com"
// Send a POST request
axios({
method: 'post',
url: `${base_url}/users`,
data: {
firstName: 'Fred',
lastName: 'Flintstone'
}
})
// Send a GET request (default method)
// GET request without options or anything.
axios(`${base_url}/users`)
// the structure of axios
axios(url[, config])
here is a list of available methods Axios provides.
Request method aliases
For convenience, aliases have been provided for all supported request methods.
axios.request(config)
axios.get(url[, config])
axios.delete(url[, config])
axios.head(url[, config])
axios.options(url[, config])
axios.post(url[, data[, config]])
axios.put(url[, data[, config]])
axios.patch(url[, data[, config]])
axios.postForm(url[, data[, config]])
axios.putForm(url[, data[, config]])
axios.patchForm(url[, data[, config]])
Customizing Axios
We can create multiple instances for the Axios and use it as our necessity.
we can create an instance, and use it in our next API calls.
const instance = axios.create({
baseURL: 'https://jsonplaceholder.typicode.com',
headers: {
'Accept': 'application/json'
},
transformRequest: [function (data, headers) {
// Do whatever you want to transform the data
return data;
}],
// `transformResponse` allows changes to
// the response data to be made before
// it is passed to then/catch
transformResponse: [function (data) {
// Do whatever you want to transform the data
return data;
}],
});
// API call
instance.get('/todos')
.then(response => {
console.log(response.data)
})
// response will be
// [
// {
// "userId": 1,
// "id": 1,
// "title": "delectus aut autem",
// "completed": false
// },
// {
// "userId": 1,
// "id": 2,
// "title": "quis ut nam facilis et officia qui",
// "completed": false
// },
// ......
Now we can just call the rest of the path, we don't need to pass the whole URLs,
transformRequest
will transform our request config or data before calling the API, transformResponse
will transform the response after successfully calling the API.
more config can be found here. We can create a global instance, so every time we use the axios
we do not need to pass the config on each request, we can do that by,
axios.defaults.baseURL = 'https://jsonplaceholder.typicode.com';
axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;
axios.defaults.headers.post['Content-Type'] = 'application/json';
So in this process, we can use the axios method directly as below.
axios.get('/todos')
.then(response => {
console.log(response.data)
})
In Axios, we can use interceptors, for the request and response.
// Add a request interceptor
axios.interceptors.request.use(function (config) {
// Do something before request is sent
return config;
}, function (error) {
// Do something with request error
return Promise.reject(error);
});
// Add a response interceptor
axios.interceptors.response.use(function (response) {
// Any status code that lie within the range of 2xx cause this function to trigger
// Do something with response data
return response;
}, function (error) {
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error
return Promise.reject(error);
});
If you see we log the response.data
- not the response, so what contains the response property, the response property contains the data as below.
{
// `data` is the response that was provided by the server
data: {},
// `status` is the HTTP status code from the server response
status: 200,
// `statusText` is the HTTP status message from the server response
// As of HTTP/2 status text is blank or unsupported.
// (HTTP/2 RFC: https://www.rfc-editor.org/rfc/rfc7540#section-8.1.2.4)
statusText: 'OK',
// `headers` the HTTP headers that the server responded with
// All header names are lower cased and can be accessed using the bracket notation.
// Example: `response.headers['content-type']`
headers: {},
// `config` is the config that was provided to `axios` for the request
config: {},
// `request` is the request that generated this response
// It is the last ClientRequest instance in node.js (in redirects)
// and an XMLHttpRequest instance in the browser
request: {}
}
Axios provides a handy configuration for request cancelation as before we saw the fetch API abort controller.
const controller = new AbortController();
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('/user/12345', {
cancelToken: source.token,
signal: controller.signal
}).catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// handle error
}
});
axios.post('/user/12345', {
name: 'new name'
}, {
cancelToken: source.token
})
// cancel the request (the message parameter is optional)
source.cancel('Operation canceled by the user.');
// OR
controller.abort(); // the message parameter is not supported
More details about cancel tokens can be found here.
Okay, let's deep dive into the API calling process, The above two processes can be used anywhere in a javascript application. But what if we use those in a React application or any front-end application? Okay now let's talk about it.
API call in a react application
There are many ways to call the API in a React application.
We can use useEffect
hook and useState
to store the data an example below.
const [data, setData] = useState(null);
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/posts?_limit=10')
.then(response => response.json())
.then(json => setData(json))
.catch(error => console.error(error));
}, []);
In this process, the problem is if we have to call the data again in another component we can't do that, yes if we separate the logic to a custom hook it can be possible.
const usePosts = ({ per_page = 10, page = 1 }) => {
const [data, setData] = useState(null);
useEffect(() => {
fetch(
`https://jsonplaceholder.typicode.com/posts?_limit=${per_page}&_page=${page}`
)
.then((response) => response.json())
.then((json) => setData(json))
.catch((error) => console.error(error));
}, []);
return data;
};
But every time we call the hook the same API will be called as our data is the same, we can prevent it by saving the first API-called data to a global store or something else. In this process we can't get the metadata like the loading state, fetching state or maybe the re-fetch functionality, then we have to create all the things from scratch.
But there were many smooth and good ways to do that, like.
useSWR hook by Vercel
ReactQuery by tanstack
Okay, let's call our API with that.
Vercel useSWR hook
SWR is a handy and lightweight tool to manage these API calls and data fetching.
SWR features as their website
Lightweight
Realtime
Suspense
Pagination
Backend Agnostic
SSR / SSG Ready
TypeScript Ready
Remote + Local
To use SWR in our application first, we have to install it.yarn add swr
after successfully installed we can use it like this.
import useSWR from 'swr'
function Profile() {
const { data, error, isLoading } = useSWR('/api/user', fetcher)
if (error) return <div>failed to load</div>
if (isLoading) return <div>loading...</div>
return <div>hello {data.name}!</div>
}
Here fetcher
could be the Axios instance or the fetch instance.
Let's make a hook, so our SWR does not need to pass the key every time.
import useSWR from "swr";
export function useUser(userId) {
const { data, error, isLoading } = useSWR(
"/api/user", // key
() => fetch(`/api/user/${userId}`).then((res) => res.json()) //fetcher
);
return {
user: data,
isLoading,
error,
};
}
So what is it?
Okay, here in the useSWR
hooks' first attribute is the unique key for the request, and the second one is the fetcher, there are other attribute options which is optional, so the SWR looks like this.
const { data, error, isLoading, isValidating, mutate } = useSWR(key, fetcher, options)
By default, key
will be passed to fetcher
as the argument. So the following 3 expressions are equivalent:
useSWR('/api/user', () => fetcher('/api/user'))
useSWR('/api/user', url => fetcher(url))
useSWR('/api/user', fetcher)
In options, we can pass a custom config for the request.
Options receive these properties.
suspense = false
: enable React Suspense modefetcher(args)
: the fetcher functionrevalidateIfStale = true
: automatically revalidate even if there is stale datarevalidateOnMount
: enable or disable automatic revalidation when the component is mountedrevalidateOnFocus = true
: automatically revalidate when the window gets focusedrevalidateOnReconnect = true
: automatically revalidate when the browser regains a network connection (vianavigator.onLine
) (details)refreshInterval
(details):Disabled by default:
refreshInterval = 0
If set to a number, the polling interval in milliseconds
If set to a function, the function will receive the latest data and should return the interval in milliseconds
refreshWhenHidden = false
: polling when the window is invisible (ifrefreshInterval
is enabled)refreshWhenOffline = false
: polling when the browser is offline (determined bynavigator.onLine
)shouldRetryOnError = true
: retry when fetcher has an errordedupingInterval = 2000
: dedupe requests with the same key in this time span in millisecondsfocusThrottleInterval = 5000
: only revalidate once during a time span in millisecondsloadingTimeout = 3000
: timeout to trigger the onLoadingSlow event in millisecondserrorRetryInterval = 5000
: error retry interval in millisecondserrorRetryCount
: max error retry countfallback
: a key-value object of multiple fallback data (example)fallbackData
: initial data to be returned (note: This is per-hook)keepPreviousData = false
: return the previous key's data until the new data has been loaded (details)onLoadingSlow(key, config)
: callback function when a request takes too long to load (seeloadingTimeout
)onSuccess(data, key, config)
: callback function when a request finishes successfullyonError(err, key, config)
: callback function when a request returns an erroronErrorRetry(err, key, config, revalidate, revalidateOps)
: handler for error retryonDiscarded(key)
: callback function when a request is ignored due to race conditionscompare(a, b)
: comparison function used to detect when returned data has changed, to avoid spurious rerenders. By default, stable-hash is used.isPaused()
: function to detect whether pause revalidations, will ignore fetched data and errors when it returnstrue
. Returnsfalse
by default.use
: array of middleware functions
More details can be found here.
Instead of passing the configuration, we can use a global configuration for the SWR.
import useSWR, { SWRConfig } from 'swr'
function Dashboard () {
const { data: events } = useSWR('/api/events')
const { data: projects } = useSWR('/api/projects')
const { data: user } = useSWR('/api/user', { refreshInterval: 0 }) // override
// ...
}
function App () {
return (
<SWRConfig
value={{
refreshInterval: 3000,
fetcher: (resource, init) => fetch(resource, init).then(res => res.json())
}}
>
<Dashboard />
</SWRConfig>
)
}
In return, the hooks give us
data
: data for the given key resolved by thefetcher
(or undefined if not loaded)error
: error thrown byfetcher
(or undefined)isLoading
: if there's an ongoing request and no "loaded data". Fallback data and previous data are not considered "loaded data"isValidating
: if there's a request or revalidation loadingmutate(data?, options?)
: function to mutate the cached data
Axios example
import axios from 'axios'
const fetcher = url => axios.get(url).then(res => res.data)
function App () {
const { data, error } = useSWR('/api/data', fetcher)
// ...
}
For data mutation, SWR provides a mutate
method to mutate the cached data
import useSWR from 'swr'
function Profile () {
const { data, mutate } = useSWR('/api/user', fetcher)
const handleClick = async () => {
const newName = data.name.toUpperCase()
// send a request to the API to update the data
await requestUpdateUsername(newName)
// update the local data immediately and revalidate (refetch)
// NOTE: key is not required when using useSWR's mutate as it's pre-bound
mutate({ ...data, name: newName })
}
return (
<div>
<h1>My name is {data.name}.</h1>
<button onClick={handleClick}>Uppercase my name!</button>
</div>
)
}
More about mutation can be found here.
Conditional
Use null
or pass a function as key
to conditionally fetch data. If the function throws or returns a false value, SWR will not start the request.
// conditionally fetch
const { data } = useSWR(shouldFetch ? '/api/data' : null, fetcher)
// ...or return a falsy value
const { data } = useSWR(() => shouldFetch ? '/api/data' : null, fetcher)
// ...or throw an error when user.id is not defined
const { data } = useSWR(() => '/api/data?uid=' + user.id, fetcher)
Prefetch
SWR provides the preload
API to prefetch the resources programmatically and store the results in the cache. preload
accepts key
and fetcher
as the arguments.
You can call preload
even outside of React.
import { useState } from 'react'
import useSWR, { preload } from 'swr'
const fetcher = (url) => fetch(url).then((res) => res.json())
// Preload the resource before rendering the User component below,
// this prevents potential waterfalls in your application.
// You can also start preloading when hovering the button or link, too.
preload('/api/user', fetcher)
function User() {
const { data } = useSWR('/api/user', fetcher)
...
}
export default function App() {
const [show, setShow] = useState(false)
return (
<div>
<button onClick={() => setShow(true)}>Show User</button>
{show ? <User /> : null}
</div>
)
}
There were beautiful docs for SWR, you can look at them for more details.
Okay, SWR is the simplest way to handle the API request, but maybe you need something more than you must use the react-query to handle your API calls.
React Query
Powerful asynchronous state management for TS/JS, React, Solid, Vue and Svelte
Toss out that granular state management, manual re-fetching and endless bowls of async-spaghetti code. TanStack Query gives you declarative, always-up-to-date auto-managed queries and mutations that directly improve both your developer and user experiences.
React Query is often described as the missing data-fetching library for React, but in more technical terms, it makes fetching, caching, synchronizing and updating server state in your React applications a breeze.
#Enough talk, show me some code already!
import { QueryClient, QueryClientProvider, useQuery } from 'react-query'
const queryClient = new QueryClient()
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Example />
</QueryClientProvider>
)
}
function Example() {
const { isLoading, error, data } = useQuery('repoData', () =>
fetch('https://api.github.com/repos/tannerlinsley/react-query').then(res =>
res.json()
)
)
if (isLoading) return 'Loading...'
if (error) return 'An error has occurred: ' + error.message
return (
<div>
<h1>{data.name}</h1>
<p>{data.description}</p>
<strong>👀 {data.subscribers_count}</strong>{' '}
<strong>✨ {data.stargazers_count}</strong>{' '}
<strong>🍴 {data.forks_count}</strong>
</div>
)
}
So let's install the react-query
npm i react-query
# or
yarn add react-query
A quick start
Here is a quick start example.
This example very briefly illustrates the 3 core concepts of React Query:
import {
useQuery,
useMutation,
useQueryClient,
QueryClient,
QueryClientProvider,
} from 'react-query'
import { getTodos, postTodo } from '../my-api'
// Create a client
const queryClient = new QueryClient()
function App() {
return (
// Provide the client to your App
<QueryClientProvider client={queryClient}>
<Todos />
</QueryClientProvider>
)
}
function Todos() {
// Access the client
const queryClient = useQueryClient()
// Queries
const query = useQuery('todos', getTodos)
// Mutations
const mutation = useMutation(postTodo, {
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries('todos')
},
})
return (
<div>
<ul>
{query.data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
<button
onClick={() => {
mutation.mutate({
id: Date.now(),
title: 'Do Laundry',
})
}}
>
Add Todo
</button>
</div>
)
}
render(<App />, document.getElementById('root'))
These three concepts make up most of the core functionality of React Query. The next sections of the documentation will go over each of these core concepts in great detail.
Let's describe this.
At first, we have to create a QueryClient
instance and provide it to the QueryClientProvider
and the QueryClientProvider
should be on top of the application so we can use the client inside the components.
So in the Todos
component you see there were some hooks from the react-query we used, useQueryClient
gives us the instance of the react-query client initiated by the provider, useQuery
is the actual query hook that we will use each time and the useMutation
hook is to revalidate the data after a successful update or delete operation.
More about useQuery hook
A query is a declarative dependency on an asynchronous source of data that is tied to a unique key. A query can be used with any Promise-based method (including GET and POST methods) to fetch data from a server.
To subscribe to a query in your components or custom hooks, call the useQuery
hook with at least:
A unique key for the query
A function that returns a promise that:
Resolves the data, or
Throws an error
const result = useQuery('todos', fetchTodoList)
The result
object contains a few very important states you'll need to be aware of to be productive. A query can only be in one of the following states at any given moment:
isLoading
orstatus === 'loading'
- The query has no data and is currently fetchingisError
orstatus === 'error'
- The query encountered an errorisSuccess
orstatus === 'success'
- The query was successful and data is availableisIdle
orstatus === 'idle'
- The query is currently disabled (you'll learn more about this in a bit)
Beyond those primary states, more information is available depending on the state of the query:
error
- If the query is in anisError
state, the error is available via theerror
property.data
- If the query is in asuccess
state, the data is available via thedata
property.isFetching
- In any state, if the query is fetching at any time (including background refetching)isFetching
will betrue
.
For most queries, it's usually sufficient to check for the isLoading
state, then the isError
state, then finally, assume that the data is available and render the successful state:
function Todos() {
const { isLoading, isError, data, error } = useQuery('todos', fetchTodoList)
if (isLoading) {
return <span>Loading...</span>
}
if (isError) {
return <span>Error: {error.message}</span>
}
// We can assume by this point that `isSuccess === true`
return (
<ul>
{data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)
}
The Query Key
The query key can be a string, array, or object.
If something is dependable you should include it as a query key.
function Todos({ todoId }) {
const result = useQuery(['todos', todoId], () => fetchTodoById(todoId))
}
// Example of key
// A list of todos
useQuery('todos', ...) // queryKey === ['todos']
// Something else, whatever!
useQuery('somethingSpecial', ...) // queryKey === ['somethingSpecial']
// An individual todo
useQuery(['todo', 5], ...)
// queryKey === ['todo', 5]
// An individual todo in a "preview" format
useQuery(['todo', 5, { preview: true }], ...)
// queryKey === ['todo', 5, { preview: true }]
// A list of todos that are "done"
useQuery(['todos', { type: 'done' }], ...)
// queryKey === ['todos', { type: 'done' }]
Query Functions
A query function can be literally any function that returns a promise. The promise that is returned should either resolve the data or throw an error.
useQuery(['todos'], fetchAllTodos)
useQuery(['todos', todoId], () => fetchTodoById(todoId))
useQuery(['todos', todoId], async () => {
const data = await fetchTodoById(todoId)
return data
})
useQuery(['todos', todoId], ({ queryKey }) => fetchTodoById(queryKey[1]))
// throughing an error
useQuery(['todos', todoId], async () => {
const response = await fetch('/todos/' + todoId)
if (!response.ok) {
throw new Error('Network response was not ok')
}
return response.json()
})
Using a Query Object instead of parameters
Anywhere the [queryKey, queryFn, config]
signature is supported throughout React Query's API, you can also use an object to express the same configuration:
import { useQuery } from 'react-query'
useQuery({
queryKey: ['todo', 7],
queryFn: fetchTodo,
...config,
})
react-query
is a huge feature list that is used as needed, as it is a large library to handle the API calls I think this basic is enough for now, to more details and dive I will share another individual article for this, IA.
for now, you can look at the documentation of react-query and the tkdodo blogs.
Conclusion
In conclusion, JavaScript offers a plethora of options for making API calls, each with its own advantages and specific use cases. The Fetch API provides a built-in method for fetching resources across the web, offering a straightforward and modern interface. Axios, on the other hand, simplifies the process of making HTTP requests with additional features like interceptors and support for older browsers. For React applications, libraries like useSWR and react-query enhance data fetching by providing caching, revalidation, and a more streamlined approach to handling asynchronous data.
Ultimately, the choice of which method or library to use depends on the project requirements, developer preferences, and the specific functionalities needed. Understanding the nuances and strengths of each approach empowers developers to efficiently retrieve and manage data, ensuring a smoother and more responsive user experience in their web applications.