Modern React applications often need to communicate with APIs to retrieve data from a server. The JavaScript fetch() function can be used to request data from an API and then display that data inside React components.
useEffect is commonly
used when data should be fetched as a side effect of rendering, while
useState can store the fetched data, loading state, and error
state.
An API (Application Programming Interface) allows different software systems to communicate with each other.
A React application can request data from a server through an API.
React Application
↓
API
↓
Server
↓
Data
Many applications need data that is stored on a server rather than directly inside the React application.
Examples include:
The JavaScript fetch() function is used to make HTTP requests
and retrieve data from a URL.
fetch("https://example.com/api/students");
The request is asynchronous and returns a Promise.
fetch("API_URL")
.then(response => response.json())
.then(data => {
console.log(data);
});
The first then() receives the response, and
response.json() converts a JSON response into JavaScript data.
Many APIs return data in JSON format.
{
"id": 1,
"name": "Rahul",
"course": "React"
}
JSON is commonly used for exchanging structured data between a client and server.
The json() method reads the response body and parses JSON data.
fetch("/api/student")
.then(response => response.json())
.then(data => {
console.log(data);
});
In React, API data is often fetched inside a useEffect Hook when
the request should happen after the component is rendered.
import {
useEffect
} from "react";
function Students() {
useEffect(() => {
fetch("/api/students")
.then(response => response.json())
.then(data => {
console.log(data);
});
}, []);
return <h2>Students</h2>;
}
Fetching data is an external operation. useEffect can be used
when the request should occur as a side effect of the component.
useEffect(() => {
// API request
}, []);
An empty dependency array commonly means the effect is set up after the initial render and is not re-run because of changing dependencies.
Fetched data can be stored in React state.
import {
useEffect,
useState
} from "react";
function Students() {
const [students, setStudents] = useState([]);
useEffect(() => {
fetch("/api/students")
.then(response => response.json())
.then(data => {
setStudents(data);
});
}, []);
return <h2>Students</h2>;
}
After storing the data in state, you can display it in JSX.
{students.map(student => (
<p key={student.id}>
{student.name}
</p>
))}
The map() method can be used when the API returns an array.
API requests can take some time. A loading state can show the user that data is being requested.
const [loading, setLoading] = useState(true);
Initially, loading can be set to true.
fetch("/api/students")
.then(response => response.json())
.then(data => {
setStudents(data);
setLoading(false);
});
After the data is received, the loading state can be changed to
false.
if (loading) {
return <p>Loading...</p>;
}
This provides immediate feedback while the request is in progress.
API requests can fail because of network problems, server errors, or invalid URLs. An error state can store information about a failed request.
const [error, setError] = useState(null);
fetch("/api/students")
.then(response => response.json())
.then(data => {
setStudents(data);
})
.catch(error => {
setError(error.message);
});
The catch() method can handle rejected Promises.
if (error) {
return (
<p>
Error: {error}
</p>
);
}
This allows the application to show an appropriate message when a request fails.
The fetch() Promise does not automatically reject for every HTTP
error status. You can check response.ok before parsing the data.
fetch("/api/students")
.then(response => {
if (!response.ok) {
throw new Error("Request failed");
}
return response.json();
});
import {
useEffect,
useState
} from "react";
function Students() {
const [students, setStudents] = useState([]);
useEffect(() => {
fetch("/api/students")
.then(response => response.json())
.then(data => {
setStudents(data);
});
}, []);
return (
<div>
<h2>Students</h2>
{students.map(student => (
<p key={student.id}>
{student.name}
</p>
))}
</div>
);
}
export default Students;
An API may return a single object instead of an array.
{
"id": 101,
"name": "Rahul",
"course": "React"
}
The object can be stored directly in state.
const [student, setStudent] = useState(null);
{student && (
<div>
<h2>
{student.name}
</h2>
<p>
Course: {student.course}
</p>
</div>
)}
The condition prevents the component from trying to access properties before the data has been loaded.
The async/await syntax can make asynchronous code easier to
read.
async function getStudents() {
const response = await fetch(
"/api/students"
);
const data = await response.json();
console.log(data);
}
An inner asynchronous function can be defined inside an effect.
useEffect(() => {
async function loadStudents() {
const response = await fetch(
"/api/students"
);
const data = await response.json();
setStudents(data);
}
loadStudents();
}, []);
import {
useEffect,
useState
} from "react";
function Students() {
const [students, setStudents] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function loadStudents() {
try {
const response = await fetch(
"/api/students"
);
if (!response.ok) {
throw new Error(
"Failed to load students"
);
}
const data = await response.json();
setStudents(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
loadStudents();
}, []);
if (loading) {
return <p>Loading...</p>;
}
if (error) {
return <p>Error: {error}</p>;
}
return (
<div>
<h2>Students</h2>
{students.map(student => (
<p key={student.id}>
{student.name}
</p>
))}
</div>
);
}
export default Students;
API requests can use route parameters to load specific data.
const { id } = useParams();
useEffect(() => {
fetch(`/api/students/${id}`)
.then(response => response.json())
.then(data => {
setStudent(data);
});
}, [id]);
For example, the URL may be:
/students/101
const { id } = useParams();
useEffect(() => {
async function loadProduct() {
const response = await fetch(
`/api/products/${id}`
);
const data = await response.json();
setProduct(data);
}
loadProduct();
}, [id]);
The parameter identifies which product should be requested.
response.json().response.ok when HTTP errors matter.A common React API workflow is:
Component Renders
↓
useEffect
↓
fetch()
↓
Server/API
↓
Response
↓
response.json()
↓
setState()
↓
Component Re-renders
↓
Display Data
A complete API interface commonly needs three states:
Loading → Request → Success
↘ Error
import {
useEffect,
useState
} from "react";
function Students() {
const [students, setStudents] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
useEffect(() => {
async function fetchStudents() {
try {
const response = await fetch(
"/api/students"
);
if (!response.ok) {
throw new Error(
"Unable to fetch students"
);
}
const data = await response.json();
setStudents(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
fetchStudents();
}, []);
if (loading) {
return (
<h3>
Loading students...
</h3>
);
}
if (error) {
return (
<h3>
Error: {error}
</h3>
);
}
return (
<div>
<h2>Student List</h2>
{students.map(student => (
<div
key={student.id}
className="card mt-3"
>
<div className="card-body">
<h4>
{student.name}
</h4>
<p>
Course: {student.course}
</p>
</div>
</div>
))}
</div>
);
}
export default Students;
useEffect for appropriate data-fetching effects.response.json().fetch()
↓
Response
↓
JSON
↓
State
↓
UI
fetch() is used to make HTTP requests.response.json() converts JSON response data into JavaScript data.useEffect is commonly used for data-fetching side effects.useState can store fetched data.response.ok can be checked before processing a response.Question: Which JavaScript function is commonly used to request data from an API?