Effortless Data Fetching: Implementing Axios Package and Context API in a Simple Blog Site
Welcome to our blog post, where we delve into the world of data fetching in a simple blog site using the popular Axios package. If you're a web developer looking for a seamless way to retrieve data from APIs and integrate it into your blog site, you're in for a treat.
In this article, we'll guide you through the process of leveraging Axios, a powerful HTTP client for JavaScript, to effortlessly fetch data from external sources. We'll explore how to make GET requests to retrieve blog posts, handle response data, and seamlessly update your site's content.
Whether you're a beginner or an experienced developer, understanding how to effectively fetch and handle data is crucial in building dynamic and interactive web applications. With Axios, you'll have a reliable tool at your disposal that simplifies the process, providing a clean and intuitive API for managing HTTP requests.
By the end of this tutorial, you'll have a solid understanding of how to integrate Axios into your simple blog site and fetch data with ease. So, let's dive in and explore the powerful capabilities of Axios in enhancing your data fetching experience for a more dynamic and engaging blog site. Let's get started!
Tutorial
This article is in continuation to the previous project tutorial called: Building an Interactive Blog with React Routing: Unlocking Dynamic Navigation and Seamless User Experience which we would recommend to read before attempting this project tutorial in order to understand the setup of the app with React and the in built fetch function.
Fetch Posts using Axios Package
Axios allows us to fetch data from APIs with more simplicity than a simple fetch call in JavaScript. First, we will create a folder in the root directory called ‘data
’ and create a file called db.json
where we will place the posts data. We will cut the data we statically coded in App.js
within the posts state and replace it with an empty array. In db.json
file we will place a posts
object in the following way:
{
"posts": [
{
"id": 1,
"title": "React Router",
"body": "React Router is a collection of navigational components that compose declaratively with your application.",
"datetime": "July 26, 2021 11:17:00 AM"
},
{
"id": 2,
"title": "React.js",
"body": "React is a JavaScript library for building user interfaces. It is maintained by Facebook and a community of individual developers and companies.",
"datetime": "July 26, 2021 12:17:00 AM"
},
{
"id": 3,
"title": "React Hooks",
"body": "Hooks are a new addition in React 16.8. They let you use state and other React features without writing a class.",
"datetime": "July 26, 2021 10:17:00 AM"
},
{
"id": 4,
"title": "React Context",
"body": "Context provides a way to pass data through the component tree without having to pass props down manually at every level.",
"datetime": "July 26, 2021 09:17:00 AM"
}
]
}
Next, we will run a local json server using the following command:
npx json-server -p 3500 -w data/db.json
This will initiate a server at: http://localhost:3500
Once the server is ready, we will install the Axios package from axios - npm (npmjs.com) using the command: npm install axios --save
which will place the package in dependencies.
Now we will create an ‘api’ folder within the ‘src’ folder and within the ‘api’ folder, we will create a posts.js
file where we will place the logic to connect with the API in the following way:
import axios from "axios";
export default axios.create({
baseURL: "<http://localhost:3500>",
});
Next, we will create the logic to fetch data from the API in the App.js
file in the following way:
import api from "./api/posts";
const [posts, setPosts] = useState([]);
useEffect(() => {
const fetchPosts = async () => {
try {
const response = await api.get("/posts");
setPosts(response.data);
} catch (err) {
if (err.response) {
//Not in the 200 response range
console.log(err.response.data);
console.log(err.response.status);
console.log(err.response.headers);
} else {
console.log(`Error: ${err.message}`);
}
}
};
fetchPosts();
}, []);
The above code snippet demonstrates the usage of the useState
and useEffect
hooks in React to fetch data from an API and store it in the component's state. Here's a breakdown of the code:
- Importing the API module:
import api from "./api/posts";
: This line imports theapi
module from the "./api/posts" file. It suggests that the file contains functions to interact with an API, specifically related to fetching posts.
- Declaring state variables:
const [posts, setPosts] = useState([]);
: This line declares a state variable namedposts
using theuseState
hook. The initial value ofposts
is an empty array[]
, andsetPosts
is a function that can be used to update the value ofposts
.
- Fetching data from the API:
- The
useEffect
hook is used to perform side effects in the component. In this case, it's used to fetch posts data from the API when the component mounts (empty dependency array[]
). useEffect(() => { ... }, []);
: This hook receives a function as the first argument and an empty dependency array as the second argument.- The function passed to
useEffect
is an asynchronous function namedfetchPosts
, which is responsible for fetching the posts data. - Inside the
fetchPosts
function:- An asynchronous API request is made using
api.get("/posts")
, assuming it returns a promise. - If the request is successful (
response
object is received), the posts data is extracted fromresponse.data
and set as the new value for theposts
state variable usingsetPosts(response.data)
. - If the request encounters an error (
err
object is received), the error is logged to the console. If the error response (err.response
) is available, it logs the error data, status, and headers. Otherwise, it logs the general error message.
- An asynchronous API request is made using
- The
- Invoking the fetchPosts function:
fetchPosts();
: This line invokes thefetchPosts
function defined inside theuseEffect
hook, causing it to be executed when the component mounts.
In summary, this code fetches posts data from an API using the api
module, stores the data in the component's state variable posts
, and handles potential errors during the API request. The useState
hook is used to declare the state variable, and the useEffect
hook is used to fetch the data and update the state when the component mounts.
Post Data with Axios Package
Next, we will edit the handleSubmit
function to implement the post method using axios in App.js
file in the following way:
const handleSubmit = async (e) => {
e.preventDefault();
const id = posts.length ? posts[posts.length - 1] + 1 : 1;
//npm install date-fns --save
const datetime = format(new Date(), "MMMM dd, yyyy pp");
const newPost = { id, title: postTitle, datetime, body: postBody };
try {
const response = await api.post("/posts", newPost);
const allPosts = [...posts, response.data];
setPosts(allPosts);
setPostTitle("");
setPostBody("");
navigate("/");
} catch (err) {
console.log(`Error: ${err.message}`);
}
};
The above code snippet shows an asynchronous function named handleSubmit
that handles the submission of a new post. Here's a breakdown of the code:
- Function declaration:
const handleSubmit = async (e) => { ... }
: This line declares an asynchronous function namedhandleSubmit
. It takes an event objecte
as its parameter, which represents the form submission event.
- Preventing default form behavior:
e.preventDefault();
: This line prevents the default behavior of the form submission event, which typically refreshes the page.
- Generating a new post ID:
const id = posts.length ? posts[posts.length - 1] + 1 : 1;
: This line generates a new post ID. If theposts
array is not empty, it takes the ID of the last post and increments it by 1. Otherwise, it assigns the value 1 as the initial ID.
- Formatting the current datetime:
const datetime = format(new Date(), "MMMM dd, yyyy pp");
: This line uses theformat
function from thedate-fns
library to format the current date and time. It generates a string representation of the date in the format "Month day, year time".
- Creating a new post object:
const newPost = { id, title: postTitle, datetime, body: postBody };
: This line creates a new post object using theid
,postTitle
,datetime
, andpostBody
variables. It represents the data of the new post to be submitted.
- Making a POST request to the API:
const response = await api.post("/posts", newPost);
: This line sends a POST request to the "/posts" endpoint of the API using theapi
module. It includes thenewPost
object as the request payload. The response object is stored in theresponse
variable.
- Updating the posts state:
const allPosts = [...posts, response.data];
: This line creates a new arrayallPosts
by spreading the existingposts
array and adding the new postresponse.data
received from the API response.setPosts(allPosts);
: This line updates theposts
state variable with the new array of posts, triggering a re-render of the component.
- Clearing form input values:
setPostTitle("");
andsetPostBody("");
: These lines reset thepostTitle
andpostBody
state variables to empty strings, clearing the form input fields.
- Navigating to the home page:
navigate("/");
: This line uses thenavigate
function from thereact-router-dom
library to navigate to the home page ("/") after the form submission.
- Error handling:
- The code inside the
try
block handles any potential errors that may occur during the API request. - If an error occurs, it is caught by the
catch
block, and the error message is logged to the console usingconsole.log()
.
- The code inside the
In summary, this code handles the submission of a new post by preventing the default form behavior, generating an ID for the new post, formatting the current datetime, making a POST request to the API with the new post data, updating the posts state with the received response, clearing the form input values, and navigating to the home page. It also includes error handling to catch and log any errors that may occur during the API request.
Delete Post with Axios Package
Next, we will implement the simplest logic to delete a post by editing the previously created handleDelete
function in App.js
file in the following way:
const handleDelete = async (id) => {
try {
await api.delete(`/posts/${id}`);
const postsList = posts.filter((post) => post.id !== id);
setPosts(postsList);
navigate("/");
} catch (err) {
console.log(`Error: ${err.message}`);
}
};
The above code snippet defines an asynchronous function named handleDelete
that handles the deletion of a post. Here's a breakdown of the code:
- Function declaration:
const handleDelete = async (id) => { ... }
: This line declares an asynchronous function namedhandleDelete
. It takes anid
parameter that represents the ID of the post to be deleted.
- Sending a DELETE request to the API:
await api.delete(
/posts/${id});
: This line sends a DELETE request to the API endpoint corresponding to the specified post ID. Theapi
module is used to perform the request. Theawait
keyword is used to wait for the response before proceeding.
- Updating the posts state:
const postsList = posts.filter((post) => post.id !== id);
: This line creates a new arraypostsList
by filtering the existingposts
array. It excludes the post with the specifiedid
, effectively removing it from the list.setPosts(postsList);
: This line updates theposts
state variable with the new array of posts, triggering a re-render of the component.
- Navigating to the home page:
navigate("/");
: This line uses thenavigate
function from thereact-router-dom
library to navigate to the home page ("/") after the post deletion.
- Error handling:
- The code inside the
try
block handles any potential errors that may occur during the API request. - If an error occurs, it is caught by the
catch
block, and the error message is logged to the console usingconsole.log()
.
- The code inside the
In summary, this code handles the deletion of a post by sending a DELETE request to the API, updating the posts state by removing the deleted post, and navigating to the home page. It also includes error handling to catch and log any errors that may occur during the API request.
Updating Post Data with Axios
Next, we will create a function to edit an existing post in the App.js
file in the following way:
const [editTitle, setEditTitle] = useState("");
const [editBody, setEditBody] = useState("");
const handleEdit = async (id) => {
const datetime = format(new Date(), "MMMM dd, yyyy pp");
const updatedPost = { id, title: editTitle, datetime, body: editBody };
try {
const response = await api.put(`/posts/${id}`, updatedPost);
setPosts(
posts.map((post) => (post.id === id ? { ...response.data } : post))
);
setEditTitle("");
setEditBody("");
navigate("/");
} catch (err) {
console.log(`Error: ${err.message}`);
}
};
The above code snippet defines two state variables, editTitle
and editBody
, using the useState
hook. It also defines an asynchronous function named handleEdit
that handles the editing of a post. Here's a breakdown of the code:
- State variables:
const [editTitle, setEditTitle] = useState("");
: This line initializes theeditTitle
state variable with an empty string as its initial value. ThesetEditTitle
function is used to update the value ofeditTitle
.const [editBody, setEditBody] = useState("");
: This line initializes theeditBody
state variable with an empty string as its initial value. ThesetEditBody
function is used to update the value ofeditBody
.
- Function declaration:
const handleEdit = async (id) => { ... }
: This line declares an asynchronous function namedhandleEdit
. It takes anid
parameter that represents the ID of the post to be edited.
- Creating an updated post object:
const datetime = format(new Date(), "MMMM dd, yyyy pp");
: This line creates a formatted date and time string using theformat
function from thedate-fns
library. It represents the current date and time.const updatedPost = { id, title: editTitle, datetime, body: editBody };
: This line creates anupdatedPost
object that contains the updated title, body, and datetime values. TheeditTitle
andeditBody
state variables hold the updated values.
- Sending a PUT request to the API:
await api.put(
/posts/${id}, updatedPost);
: This line sends a PUT request to the API endpoint corresponding to the specified post ID. It includes theupdatedPost
object as the request payload. Theapi
module is used to perform the request. Theawait
keyword is used to wait for the response before proceeding.
- Updating the posts state:
setPosts(...);
: This line updates theposts
state variable. It maps over the existingposts
array and replaces the post with the matchingid
with the updated post object from the response.- The
setPosts
function is used to update the state variable, triggering a re-render of the component.
- Clearing the input fields and navigating to the home page:
setEditTitle("");
andsetEditBody("");
: These lines reset theeditTitle
andeditBody
state variables to empty strings, clearing the input fields.navigate("/");
: This line uses thenavigate
function from thereact-router-dom
library to navigate to the home page ("/") after the post is edited.
- Error handling:
- The code inside the
try
block handles any potential errors that may occur during the API request. - If an error occurs, it is caught by the
catch
block, and the error message is logged to the console usingconsole.log()
.
- The code inside the
In summary, this code handles the editing of a post by sending a PUT request to the API with the updated post data. It updates the posts state by replacing the existing post with the updated post object, clears the input fields, and navigates to the home page. Error handling is included to catch and log any errors that may occur during the API request.
Next, we will create an Edit.js
file which will hold the component to display that will allow us to make edits to the existing post in the following way:
import { useEffect } from "react";
import { useParams, Link } from "react-router-dom";
const Edit = ({
posts,
handleEdit,
editBody,
setEditBody,
editTitle,
setEditTitle,
}) => {
const { id } = useParams();
const post = posts.find((post) => post.id.toString() === id);
useEffect(() => {
if (post) {
setEditBody(post.body);
setEditTitle(post.title);
}
}, [post, setEditBody, setEditTitle]);
return (
<main className="NewPost">
{editTitle && (
<>
<h2>Edit Post</h2>
<form className="newPostForm" onSubmit={(e) => e.preventDefault()}>
<label htmlFor="postTitle">Title:</label>
<input
type="text"
id="postTitle"
required
value={editTitle}
onChange={(e) => setEditTitle(e.target.value)}
/>
<label htmlFor="postBody">Post:</label>
<textarea
id="postBody"
required
value={editBody}
onChange={(e) => setEditBody(e.target.value)}
/>
<button type="submit" onClick={() => handleEdit(post.id)}>
Submit
</button>
</form>
</>
)}
{!editTitle && (
<>
<h2>Post Not Found</h2>
<p>Well, that's disappointing.</p>
<p>
<Link to="/">Visit Our Home Page</Link>
</p>
</>
)}
</main>
);
};
export default Edit;
The above code snippet defines a component named Edit
responsible for editing a post. Here's an explanation of the code:
- Import statements:
import { useEffect } from "react";
: This imports theuseEffect
hook from the React library, which allows performing side effects in functional components.import { useParams, Link } from "react-router-dom";
: This imports theuseParams
hook from thereact-router-dom
library, which allows accessing the parameters from the URL, and theLink
component for navigation.
- Function declaration:
const Edit = ({ posts, handleEdit, editBody, setEditBody, editTitle, setEditTitle }) => { ... }
: This declares a functional component namedEdit
. It takes several props:posts
(the list of posts),handleEdit
(a function to handle the post editing),editBody
andsetEditBody
(state variables for the post body during editing), andeditTitle
andsetEditTitle
(state variables for the post title during editing).
- Getting the post to edit:
const { id } = useParams();
: This line uses theuseParams
hook to retrieve theid
parameter from the URL.const post = posts.find((post) => post.id.toString() === id);
: This line searches for the post with the matchingid
in theposts
array. If found, it assigns the post object to thepost
variable.
- Setting initial values for editing:
- The
useEffect
hook is used to set the initial values ofeditBody
andeditTitle
when thepost
object changes. useEffect(() => { ... }, [post, setEditBody, setEditTitle]);
: This effect runs whenpost
,setEditBody
, orsetEditTitle
change.- Inside the effect, if a
post
is found, theeditBody
andeditTitle
state variables are set to the respective values from the post object using thesetEditBody
andsetEditTitle
functions.
- The
- Render:
- The component's return statement contains the JSX code that will be rendered.
- The rendering is conditionally done based on the existence of
editTitle
. - If
editTitle
is truthy, the editing form is rendered, allowing the user to edit the post title and body. When the form is submitted, thehandleEdit
function is called with thepost.id
as an argument. - If
editTitle
is falsy (post not found), a message is displayed with a link to the home page.
- Export statement:
export default Edit;
: This exports theEdit
component as the default export of this module.
In summary, this code sets up the Edit
component, which allows users to edit a post. It retrieves the post based on the id
parameter from the URL, sets the initial values for editing, renders an editing form if the post is found, and provides a "Post Not Found" message with a link to the home page if the post is not found.
Once this is done, we will import this file in App.js and write the Route logic for the same in the following way:
import EditPost from "./Edit.js";
<Routes>
<Route
path="/edit/:id"
element={
<EditPost
posts={posts}
handleEdit={handleEdit}
editBody={editBody}
editTitle={editTitle}
setEditBody={setEditBody}
setEditTitle={setEditTitle}
/>
}
/>
</Routes>
Once the route is set up, we will create a button with a link in the PostPage.js
file right above the delete button created earlier like this:
<>
<h2>{post.title}</h2>
<p className="postDate">{post.datetime}</p>
<p className="postBody">{post.body}</p>
<Link to={`/edit/${post.id}`}>
<button className="editButton">Edit Post</button>
</Link>
<button
className="deleteButton"
onClick={() => handleDelete(post.id)}
>
Delete Post
</button>
</>
When the user clicks the "Edit Post" button, it will navigate to the URL path /edit/{post.id}
. The specific post.id
value will be inserted into the URL, allowing the application to identify which post needs to be edited. This URL navigation is handled by the react-router-dom
library, which will update the URL in the browser's address bar and render the appropriate component associated with the /edit/{post.id}
route.
For these buttons, we will add some styling in the index.css
file in the following way:
.PostPage button {
height: 48px;
min-width: 48px;
border-radius: 0.25rem;
padding: 0.5rem;
margin-right: 0.5rem;
font-size: 1rem;
color: #fff;
cursor: pointer;
}
.deleteButton {
background-color: red;
}
.editButton {
background-color: #333;
}
And with this, our CRUD operations with axios package are fully functional.
Using React Custom Hooks
In this we will create a custom hooks that will look for any changes in the window resize. We will create a ‘hooks’ folder in ‘src’ and create a useWindowSize.js
file where will write the following logic:
import { useState, useEffect } from "react";
const useWindowSize = () => {
const [windowSize, setWindowSize] = useState({
width: undefined,
height: undefined,
});
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
handleResize();
window.addEventListener("resize", handleResize);
const cleanUp = () => {
console.log("runs if a useEffect dep changes");
window.removeEventListener("resize", handleResize);
};
return cleanUp;
}, []);
return windowSize;
};
export default useWindowSize;
The above code defines a custom React hook called useWindowSize
that allows you to track and access the current size of the browser window. Here's an explanation of the code:
const useWindowSize = () => { ... }
: This line declares a function calleduseWindowSize
that serves as the custom hook.const [windowSize, setWindowSize] = useState({ width: undefined, height: undefined })
: This line initializes the state variablewindowSize
using theuseState
hook. The initial state is an object withwidth
andheight
properties set toundefined
.useEffect(() => { ... }, [])
: ThisuseEffect
hook is used to handle the side effect of updating the window size. It is executed after the component has rendered, and the empty dependency array[]
ensures that the effect only runs once, similar tocomponentDidMount
in class components.const handleResize = () => { ... }
: This is the event handler function that is called when the window is resized. It updates thewindowSize
state by setting thewidth
andheight
properties to the current inner width and inner height of the window, respectively.handleResize()
: This line invokes thehandleResize
function immediately to set the initial size of the window.window.addEventListener("resize", handleResize)
: This line adds the event listener to theresize
event of the window, calling thehandleResize
function whenever the window is resized.const cleanUp = () => { ... }
: This is a cleanup function that removes the event listener when the component is unmounted or when the dependency array ofuseEffect
changes. It is returned from theuseEffect
hook.return windowSize;
: Finally, thewindowSize
object is returned from the custom hook.
By using the useWindowSize
hook in a component, you can access the current width
and height
of the window. Whenever the window is resized, the windowSize
state will be updated automatically.
Link for all react hooks: Collection of React Hooks (nikgraf.github.io), react-use - npm (npmjs.com)
Next, we will import the custom hook into the App.js
file and deconstruct and add it to the route in the following way:
import useWindowSize from "./hooks/useWindowSize";
const { width } = useWindowSize();
<Route
element={<Layout search={search} setSearch={setSearch} width={width} />}
path="/"
>
Next, we will add the props to the Layout.js
file in the following way:
const Layout = ({ search, setSearch, width }) => {
return (
<div className="App">
<Header title="React JS Blog" width={width} />
<Nav search={search} setSearch={setSearch} />
<Outlet />
<Footer />
</div>
);
};
Next we will install the react icons using the command:
npm install react-icons
Next, we will use these icons to be displayed at different window sizes in Header.js
file in the following way:
import { FaLaptop, FaTabletAlt, FaMobileAlt } from "react-icons/fa";
const Header = ({ title, width }) => {
return (
<header className="Header">
<h1>{title}</h1>
{width < 768 ? (
<FaMobileAlt />
) : width < 992 ? (
<FaTabletAlt />
) : (
<FaLaptop />
)}
</header>
);
};
export default Header;
The above code defines a Header
component that displays a title and an icon based on the width passed as a prop.
- The
Header
component receives two props:title
andwidth
. - Inside the
header
element with the class "Header", it renders anh1
element containing thetitle
prop value. - It also renders an icon based on the
width
prop value:- If the
width
is less than 768 pixels, it renders theFaMobileAlt
icon from thereact-icons/fa
package, which represents a mobile device. - If the
width
is between 768 and 992 pixels, it renders theFaTabletAlt
icon, representing a tablet. - If the
width
is greater than or equal to 992 pixels, it renders theFaLaptop
icon, representing a laptop.
- If the
- The icons are imported from the
react-icons/fa
package, which provides a collection of Font Awesome icons.
This code allows the Header
component to display different icons based on the width of the screen, providing a responsive visual representation. The specific breakpoints and icons chosen can be adjusted according to the desired design and functionality.
Create a Custome Axios Fetch Hook
In this we will create our own custom axios fetch hook which will be used to display the posts data. First, we will create another hook file in ‘hooks’ folder called useAxiosFetch.js
and write the following logic in it:
import { useState, useEffect } from "react";
import axios from "axios";
const useAxiosFetch = (dataUrl) => {
const [data, setData] = useState([]);
const [fetchError, setFetchError] = useState(null);
const [isLoading, setIsLoading] = useState(null);
useEffect(() => {
let isMounted = true;
const source = axios.CancelToken.source();
const fetchData = async (url) => {
setIsLoading(true);
try {
const response = await axios.get(url, {
cancelToken: source.token,
});
if (isMounted) {
setData(response.data);
setFetchError(null);
}
} catch (err) {
if (isMounted) {
setFetchError(err.message);
setData([]);
}
} finally {
isMounted && setTimeout(() => setIsLoading(false), 1000);
}
};
fetchData(dataUrl);
const cleanUp = () => {
console.log("clean up function");
isMounted = false;
source.cancel();
};
return cleanUp;
}, [dataUrl]);
return { data, fetchError, isLoading };
};
export default useAxiosFetch;
The above code defines a custom hook called useAxiosFetch
that facilitates fetching data from a specified URL using the Axios library. It returns an object containing the fetched data, any fetch errors, and a loading indicator.
- The hook receives a
dataUrl
parameter, which represents the URL from which the data should be fetched. - It initializes state variables using the
useState
hook:data
is initialized with an empty array and is used to store the fetched data.fetchError
is initially set tonull
and will be updated with any fetch error message.isLoading
is initially set tonull
and will be updated to indicate whether the fetch operation is in progress.
- The
useEffect
hook is used to handle the data fetching and lifecycle of the component:- It runs whenever the
dataUrl
dependency changes. - It creates a cancel token source using
axios.CancelToken.source()
to handle cancellation of the HTTP request. - The
fetchData
function is defined, which performs the actual data fetching usingaxios.get
with the provided URL and cancel token. - Inside the
fetchData
function:- It sets the
isLoading
state totrue
to indicate that the fetch operation is in progress. - It tries to make the HTTP request using
axios.get
and awaits the response. - If the response is received and the component is still mounted (
isMounted
istrue
), it updates thedata
state with the fetched data and clears any fetch error. - If an error occurs during the request and the component is still mounted, it updates the
fetchError
state with the error message and sets thedata
state to an empty array. - Finally, it sets
isLoading
tofalse
after a 1-second delay, usingsetTimeout
.
- It sets the
- The
fetchData
function is called immediately when the component mounts, using the provideddataUrl
. - A cleanup function is defined within the hook's
useEffect
. This function will be called when the component unmounts or when thedataUrl
changes. It cancels any ongoing HTTP request by callingsource.cancel()
and updates theisMounted
variable tofalse
.
- It runs whenever the
- The hook returns an object containing the
data
,fetchError
, andisLoading
states. These can be used in the component that utilizes the hook to access the fetched data, handle fetch errors, and display a loading indicator.
This custom hook encapsulates the logic for data fetching using Axios and provides a convenient way to handle the fetching process and state management in components.
Once the hook is defined, we will import it in App.js
file, comment out the previously created useEffect where we fetched data using axios api, and use our custome hook in the following way:
import useAxiosFetch from "./hooks/useAxiosFetch";
function App() {
const { data, fetchError, isLoading } = useAxiosFetch(
"<http://localhost:3500/posts>");
useEffect(() => {setPosts(data);}, [data]);
}
Next, we will add the props to the Home route like this:
<Route
index
element={
<Home
posts={searchResults}
isLoading={isLoading}
fetchError={fetchError}
/>
}
/>
Now, in the Home.js page, we will first comment out or remove all the JSX written in the return statement and write new JSX which will use the newly added props in the following way:
const Home = ({ posts, fetchError, isLoading }) => {
return (
<main className="Home">
{isLoading && <p className="statusMsg">Loading posts...</p>}
{fetchError && (
<p className="statusMsg" style={{ color: "red" }}>
{fetchError}
</p>
)}
{!isLoading &&
!fetchError &&
(posts.length ? (
<Feed posts={posts} />
) : (
<p className="statusMsg">No posts to display.</p>
))}
</main>
);
};
The above code defines a functional component called Home
that renders the content of the home page. It receives the posts
, fetchError
, and isLoading
as props.
- Inside the
Home
component's JSX:- It creates a
main
element with the className "Home". - It conditionally renders different elements based on the values of
isLoading
,fetchError
, and theposts
array:- If
isLoading
istrue
, it renders a<p>
element with the className "statusMsg" displaying the text "Loading posts..." to indicate that the posts are currently being fetched. - If
fetchError
has a truthy value (an error message), it renders a<p>
element with the className "statusMsg" and a style that sets the text color to red. The error message is displayed within the paragraph element. - If neither
isLoading
norfetchError
are true, it checks the length of theposts
array:- If
posts
is not empty, it renders aFeed
component passing theposts
as a prop. TheFeed
component is responsible for rendering the list of posts. - If
posts
is empty, it renders a<p>
element with the className "statusMsg" displaying the text "No posts to display." to indicate that there are no posts available.
- If
- If
- It creates a
This component provides a basic structure for rendering the content of the home page, handling different states during the data fetching process. It displays loading messages, error messages, and the list of posts when available. And with this our two custom hooks are complete.
State Management with Context API
Here we will refactor our previously written code to include the context hook. We will remove all the functions set up in App.js and reduce the props drilling. First, we will create a ‘context’ folder within the ‘src’ folder and then create a DataContext.js
file where we will initiate the logic in the following way:
import { useState, createContext, useEffect } from "react";
const DataContext = createContext();
export const DataProvider = ({ children }) => {
return <DataContext.Provider value={{}}>{children}</DataContext.Provider>;
};
export default DataContext;
Next, we will import the DataProvider in App.js file like this:
import { DataProvider } from "./context/DataContext";
Next, we will wrap the <Routes></Routes>
tags within the <DataProvider></DataProvider>
tags within the return statement.
Next, we will transfer all the hook imports, state declarations, and functions from App.js
file to DataContext.js
file in the following way:
import { useState, createContext, useEffect } from "react";
import api from "../api/posts";
import { format } from "date-fns";
import { Route, Routes, useNavigate } from "react-router-dom";
import useWindowSize from "../hooks/useWindowSize";
import useAxiosFetch from "../hooks/useAxiosFetch";
const DataContext = createContext();
export const DataProvider = ({ children }) => {
const [posts, setPosts] = useState([]);
const [search, setSearch] = useState([]);
const [searchResults, setSearchResults] = useState([]);
const [postTitle, setPostTitle] = useState("");
const [postBody, setPostBody] = useState("");
const [editTitle, setEditTitle] = useState("");
const [editBody, setEditBody] = useState("");
const navigate = useNavigate();
const { data, fetchError, isLoading } = useAxiosFetch(
"<http://localhost:3500/posts>"
);
const { width } = useWindowSize();
useEffect(() => {
setPosts(data);
}, [data]);
useEffect(() => {
const filteredResults = posts.filter(
(post) =>
post.body.toLowerCase().includes(search) ||
post.title.toLowerCase().includes(search)
);
setSearchResults(filteredResults.reverse());
}, [posts, search]);
const handleDelete = async (id) => {
try {
await api.delete(`/posts/${id}`);
const postsList = posts.filter((post) => post.id !== id);
setPosts(postsList);
navigate("/");
} catch (err) {
console.log(`Error: ${err.message}`);
}
};
const handleSubmit = async (e) => {
e.preventDefault();
const id = posts.length ? posts[posts.length - 1] + 1 : 1;
//npm install date-fns --save
const datetime = format(new Date(), "MMMM dd, yyyy pp");
const newPost = { id, title: postTitle, datetime, body: postBody };
try {
const response = await api.post("/posts", newPost);
const allPosts = [...posts, response.data];
setPosts(allPosts);
setPostTitle("");
setPostBody("");
navigate("/");
} catch (err) {
console.log(`Error: ${err.message}`);
}
};
const handleEdit = async (id) => {
const datetime = format(new Date(), "MMMM dd, yyyy pp");
const updatedPost = { id, title: editTitle, datetime, body: editBody };
try {
const response = await api.put(`/posts/${id}`, updatedPost);
setPosts(
posts.map((post) => (post.id === id ? { ...response.data } : post))
);
setEditTitle("");
setEditBody("");
navigate("/");
} catch (err) {
console.log(`Error: ${err.message}`);
}
};
return (
<DataContext.Provider
value={{
width,
}}
>
{children}
</DataContext.Provider>
);
};
export default DataContext;
Above, we have added the width prop to the value within the provider. Next, we will use this in Header.js
file where instead of passing the width prop, we will use the context in the following way:
import { FaLaptop, FaTabletAlt, FaMobileAlt } from "react-icons/fa";
import { useContext } from "react";
import DataContext from "./context/DataContext";
const Header = ({ title }) => {
const { width } = useContext(DataContext);
return (
<header className="Header">
<h1>{title}</h1>
{width < 768 ? (
<FaMobileAlt />
) : width < 992 ? (
<FaTabletAlt />
) : (
<FaLaptop />
)}
</header>
);
};
export default Header;
Above, you’ll notice that we have removed the width props. We have imported the context hook and context file which is used to destructure the width prop within the function.
Since, we are using the context, now we will remove the props (width={width}
) being passed in Layout Route in App.js
file and Layout.js
file.
Next, we will perform similar steps for all the components where props are passed. First we will add the props for Nav component in the DataContext.js file under the return statement in the following way:
return (
<DataContext.Provider
value={{
width, search, setSearch
}}
>
{children}
</DataContext.Provider>
);
Next, we will remove the state variables, search={search} setSearch={setSearch}
, from Layout Route in App.js file and also from Layout.js file.
Now, we will add the context to the Nav.js file in the following way:
import { Link } from "react-router-dom";
import { useContext } from "react";
import DataContext from "./context/DataContext";
const Nav = () => {
const { search, setSearch } = useContext(DataContext);
return (
<nav className="Nav">
<form className="searchForm" onSubmit={(e) => e.preventDefault()}>
<label htmlFor="search">Search Posts</label>
<input
type="text"
id="search"
placeholder="Search Posts"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</form>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/post">New Post</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
</ul>
</nav>
);
};
export default Nav;
Next, we will add the state variables for the Home component in DataContext.js file in the following way:
return (
<DataContext.Provider
value={{
width,
search,
setSearch,
searchResults,
isLoading,
fetchError,
}}
>
{children}
</DataContext.Provider>
);
We will remove the following state variables from the Home Route from the App.js file:
posts={searchResults}
isLoading={isLoading}
fetchError={fetchError}
Next, we will introduce the context within the Home.js file in the following way:
import Feed from "./Feed";
import { useContext } from "react";
import DataContext from "./context/DataContext";
const Home = () => {
const { searchResults, fetchError, isLoading } = useContext(DataContext);
return (
<main className="Home">
{isLoading && <p className="statusMsg">Loading posts...</p>}
{fetchError && (
<p className="statusMsg" style={{ color: "red" }}>
{fetchError}
</p>
)}
{!isLoading &&
!fetchError &&
(searchResults.length ? (
<Feed posts={searchResults} />
) : (
<p className="statusMsg">No posts to display.</p>
))}
</main>
);
};
export default Home;
Note that we had to change the prop name from posts
to searchResults
everywhere in the JSX return statement.
Next, we will do the same procedure with the NewPost page. First we will remove the following props from the NewPost route in the App.js file:
handleSubmit={handleSubmit}
postTitle={postTitle}
postBody={postBody}
setPostTitle={setPostTitle}
setPostBody={setPostBody}
Next, we will add all these props to the provider in DataContext.js
file in the following way:
return (
<DataContext.Provider
value={{
width,
search,
setSearch,
searchResults,
isLoading,
fetchError,
handleSubmit,
postTitle,
setPostTitle,
postBody,
setPostBody,
}}
>
{children}
</DataContext.Provider>
);
Now, within the NewPost.js file, we will remove the destructured props and add the context to the function in the following way:
import { useContext } from "react";
import DataContext from "./context/DataContext";
const NewPost = () => {
const {
handleSubmit, postTitle, setPostTitle, postBody, setPostBody
} = useContext(DataContext);
return (
<main className="NewPost">
<h2>New Post</h2>
<form className="newPostForm" onSubmit={handleSubmit}>
<label htmlFor="postTitle">Title: </label>
<input
type="text"
required
value={postTitle}
onChange={(e) => setPostTitle(e.target.value)}
id="postTitle"
/>
<label htmlFor="postBody">Post: </label>
<textarea
type="text"
id="postBody"
required
value={postBody}
onChange={(e) => setPostBody(e.target.value)}
/>
<button type="submit">Submit</button>
</form>
</main>
);
};
export default NewPost;
Next, we will perform a similar process on the Edit page. First we will remove the following props from the Edit Route in App.js
:
posts={posts}
handleEdit={handleEdit}
editBody={editBody}
editTitle={editTitle}
setEditBody={setEditBody}
setEditTitle={setEditTitle}
Now we will add the above props to the DataContext.js
file like this:
return (
<DataContext.Provider
value={{
width,
search,
setSearch,
searchResults,
isLoading,
fetchError,
handleSubmit,
postTitle,
setPostTitle,
postBody,
setPostBody,
posts,
handleEdit,
editBody,
editTitle,
setEditBody,
setEditTitle,
}}
>
{children}
</DataContext.Provider>
);
Next, we will add the context to the Edit.js
file in the following way:
import { useContext, useEffect } from "react";
import { useParams, Link } from "react-router-dom";
import DataContext from "./context/DataContext";
const Edit = () => {
const { posts, handleEdit, editBody, setEditBody, editTitle, setEditTitle } =
useContext(DataContext);
const { id } = useParams();
const post = posts.find((post) => post.id.toString() === id);
useEffect(() => {
if (post) {
setEditBody(post.body);
setEditTitle(post.title);
}
}, [post, setEditBody, setEditTitle]);
return (
<main className="NewPost">
{editTitle && (
<>
<h2>Edit Post</h2>
<form className="newPostForm" onSubmit={(e) => e.preventDefault()}>
<label htmlFor="postTitle">Title:</label>
<input
type="text"
id="postTitle"
required
value={editTitle}
onChange={(e) => setEditTitle(e.target.value)}
/>
<label htmlFor="postBody">Post:</label>
<textarea
id="postBody"
required
value={editBody}
onChange={(e) => setEditBody(e.target.value)}
/>
<button type="submit" onClick={() => handleEdit(post.id)}>
Submit
</button>
</form>
</>
)}
{!editTitle && (
<>
<h2>Post Not Found</h2>
<p>Well, that's disappointing.</p>
<p>
<Link to="/">Visit Our Home Page</Link>
</p>
</>
)}
</main>
);
};
export default Edit;
Lastly we will work on the PostPage in a similar way. First we will remove the following props from the PostPage Route in App.js file:
posts={posts}
handleDelete={handleDelete}
Next, we will simply add the handleDelete
prop to the DataContext.js
file in the provider.
Next, we will add the context to the PostPage.js in the following way:
import { useContext } from "react";
import { useParams, Link } from "react-router-dom";
import DataContext from "./context/DataContext";
const PostPage = () => {
const { posts, handleDelete } = useContext(DataContext);
const { id } = useParams();
const post = posts.find((post) => post.id.toString() === id);
return (
<main className="PostPage">
<article className="post">
{post && (
<>
<h2>{post.title}</h2>
<p className="postDate">{post.datetime}</p>
<p className="postBody">{post.body}</p>
<Link to={`/edit/${post.id}`}>
<button className="editButton">Edit Post</button>
</Link>
<button
className="deleteButton"
onClick={() => handleDelete(post.id)}
>
Delete Post
</button>
</>
)}
{!post && (
<>
<h2>Post Not Found</h2>
<p>Well, that's disappointing</p>
<p>
<Link to="/">Visit Our Home Page</Link>
</p>
</>
)}
</article>
</main>
);
};
export default PostPage;
And with this, we have completed our use of the useContext hook.
Conclusion
In conclusion, we have explored the power and convenience of using the Axios package for data fetching in a simple blog site. By leveraging Axios, we have seen how easy it is to retrieve data from APIs and seamlessly integrate it into our site's content.
Throughout this tutorial, we learned the process of making GET requests, handling response data, and updating our blog site dynamically. With Axios, we have a reliable and efficient tool that simplifies the complexities of HTTP requests, allowing us to focus more on delivering a dynamic and engaging user experience.
Remember to always handle errors and implement proper error handling mechanisms when making API requests. This ensures a robust and reliable data fetching process, improving the overall performance and user experience of your blog site.
As you continue to enhance your web development skills, Axios will prove to be a valuable asset in your toolkit. Explore its additional features, such as making POST, PUT, and DELETE requests, customizing request headers, and handling authentication, to further expand your capabilities in working with external APIs.
We hope this tutorial has equipped you with the knowledge and confidence to incorporate Axios into your own blog site or future web projects. By harnessing the power of Axios, you can take your data fetching capabilities to new heights and provide your users with an exceptional browsing experience.
Thank you for joining us on this journey of using Axios in a simple blog site. We hope you found this blog post informative and insightful. Happy coding!