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 theapimodule 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 namedpostsusing theuseStatehook. The initial value ofpostsis an empty array[], andsetPostsis a function that can be used to update the value ofposts.
- Fetching data from the API:
- The
useEffecthook 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
useEffectis an asynchronous function namedfetchPosts, which is responsible for fetching the posts data. - Inside the
fetchPostsfunction:- An asynchronous API request is made using
api.get("/posts"), assuming it returns a promise. - If the request is successful (
responseobject is received), the posts data is extracted fromresponse.dataand set as the new value for thepostsstate variable usingsetPosts(response.data). - If the request encounters an error (
errobject 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 thefetchPostsfunction defined inside theuseEffecthook, 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 objecteas 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 thepostsarray 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 theformatfunction from thedate-fnslibrary 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, andpostBodyvariables. 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 theapimodule. It includes thenewPostobject as the request payload. The response object is stored in theresponsevariable.
- Updating the posts state:
const allPosts = [...posts, response.data];: This line creates a new arrayallPostsby spreading the existingpostsarray and adding the new postresponse.datareceived from the API response.setPosts(allPosts);: This line updates thepostsstate variable with the new array of posts, triggering a re-render of the component.
- Clearing form input values:
setPostTitle("");andsetPostBody("");: These lines reset thepostTitleandpostBodystate variables to empty strings, clearing the form input fields.
- Navigating to the home page:
navigate("/");: This line uses thenavigatefunction from thereact-router-domlibrary to navigate to the home page ("/") after the form submission.
- Error handling:
- The code inside the
tryblock handles any potential errors that may occur during the API request. - If an error occurs, it is caught by the
catchblock, 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 anidparameter 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. Theapimodule is used to perform the request. Theawaitkeyword 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 arraypostsListby filtering the existingpostsarray. It excludes the post with the specifiedid, effectively removing it from the list.setPosts(postsList);: This line updates thepostsstate variable with the new array of posts, triggering a re-render of the component.
- Navigating to the home page:
navigate("/");: This line uses thenavigatefunction from thereact-router-domlibrary to navigate to the home page ("/") after the post deletion.
- Error handling:
- The code inside the
tryblock handles any potential errors that may occur during the API request. - If an error occurs, it is caught by the
catchblock, 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 theeditTitlestate variable with an empty string as its initial value. ThesetEditTitlefunction is used to update the value ofeditTitle.const [editBody, setEditBody] = useState("");: This line initializes theeditBodystate variable with an empty string as its initial value. ThesetEditBodyfunction is used to update the value ofeditBody.
- Function declaration:
const handleEdit = async (id) => { ... }: This line declares an asynchronous function namedhandleEdit. It takes anidparameter 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 theformatfunction from thedate-fnslibrary. It represents the current date and time.const updatedPost = { id, title: editTitle, datetime, body: editBody };: This line creates anupdatedPostobject that contains the updated title, body, and datetime values. TheeditTitleandeditBodystate 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 theupdatedPostobject as the request payload. Theapimodule is used to perform the request. Theawaitkeyword is used to wait for the response before proceeding.
- Updating the posts state:
setPosts(...);: This line updates thepostsstate variable. It maps over the existingpostsarray and replaces the post with the matchingidwith the updated post object from the response.- The
setPostsfunction 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 theeditTitleandeditBodystate variables to empty strings, clearing the input fields.navigate("/");: This line uses thenavigatefunction from thereact-router-domlibrary to navigate to the home page ("/") after the post is edited.
- Error handling:
- The code inside the
tryblock handles any potential errors that may occur during the API request. - If an error occurs, it is caught by the
catchblock, 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 theuseEffecthook from the React library, which allows performing side effects in functional components.import { useParams, Link } from "react-router-dom";: This imports theuseParamshook from thereact-router-domlibrary, which allows accessing the parameters from the URL, and theLinkcomponent 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),editBodyandsetEditBody(state variables for the post body during editing), andeditTitleandsetEditTitle(state variables for the post title during editing).
- Getting the post to edit:
const { id } = useParams();: This line uses theuseParamshook to retrieve theidparameter from the URL.const post = posts.find((post) => post.id.toString() === id);: This line searches for the post with the matchingidin thepostsarray. If found, it assigns the post object to thepostvariable.
- Setting initial values for editing:
- The
useEffecthook is used to set the initial values ofeditBodyandeditTitlewhen thepostobject changes. useEffect(() => { ... }, [post, setEditBody, setEditTitle]);: This effect runs whenpost,setEditBody, orsetEditTitlechange.- Inside the effect, if a
postis found, theeditBodyandeditTitlestate variables are set to the respective values from the post object using thesetEditBodyandsetEditTitlefunctions.
- 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
editTitleis truthy, the editing form is rendered, allowing the user to edit the post title and body. When the form is submitted, thehandleEditfunction is called with thepost.idas an argument. - If
editTitleis falsy (post not found), a message is displayed with a link to the home page.
- Export statement:
export default Edit;: This exports theEditcomponent 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 calleduseWindowSizethat serves as the custom hook.const [windowSize, setWindowSize] = useState({ width: undefined, height: undefined }): This line initializes the state variablewindowSizeusing theuseStatehook. The initial state is an object withwidthandheightproperties set toundefined.useEffect(() => { ... }, []): ThisuseEffecthook 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 tocomponentDidMountin class components.const handleResize = () => { ... }: This is the event handler function that is called when the window is resized. It updates thewindowSizestate by setting thewidthandheightproperties to the current inner width and inner height of the window, respectively.handleResize(): This line invokes thehandleResizefunction immediately to set the initial size of the window.window.addEventListener("resize", handleResize): This line adds the event listener to theresizeevent of the window, calling thehandleResizefunction 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 ofuseEffectchanges. It is returned from theuseEffecthook.return windowSize;: Finally, thewindowSizeobject 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
Headercomponent receives two props:titleandwidth. - Inside the
headerelement with the class "Header", it renders anh1element containing thetitleprop value. - It also renders an icon based on the
widthprop value:- If the
widthis less than 768 pixels, it renders theFaMobileAlticon from thereact-icons/fapackage, which represents a mobile device. - If the
widthis between 768 and 992 pixels, it renders theFaTabletAlticon, representing a tablet. - If the
widthis greater than or equal to 992 pixels, it renders theFaLaptopicon, representing a laptop.
- If the
- The icons are imported from the
react-icons/fapackage, 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
dataUrlparameter, which represents the URL from which the data should be fetched. - It initializes state variables using the
useStatehook:datais initialized with an empty array and is used to store the fetched data.fetchErroris initially set tonulland will be updated with any fetch error message.isLoadingis initially set tonulland will be updated to indicate whether the fetch operation is in progress.
- The
useEffecthook is used to handle the data fetching and lifecycle of the component:- It runs whenever the
dataUrldependency changes. - It creates a cancel token source using
axios.CancelToken.source()to handle cancellation of the HTTP request. - The
fetchDatafunction is defined, which performs the actual data fetching usingaxios.getwith the provided URL and cancel token. - Inside the
fetchDatafunction:- It sets the
isLoadingstate totrueto indicate that the fetch operation is in progress. - It tries to make the HTTP request using
axios.getand awaits the response. - If the response is received and the component is still mounted (
isMountedistrue), it updates thedatastate 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
fetchErrorstate with the error message and sets thedatastate to an empty array. - Finally, it sets
isLoadingtofalseafter a 1-second delay, usingsetTimeout.
- It sets the
- The
fetchDatafunction 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 thedataUrlchanges. It cancels any ongoing HTTP request by callingsource.cancel()and updates theisMountedvariable tofalse.
- It runs whenever the
- The hook returns an object containing the
data,fetchError, andisLoadingstates. 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
Homecomponent's JSX:- It creates a
mainelement with the className "Home". - It conditionally renders different elements based on the values of
isLoading,fetchError, and thepostsarray:- If
isLoadingistrue, it renders a<p>element with the className "statusMsg" displaying the text "Loading posts..." to indicate that the posts are currently being fetched. - If
fetchErrorhas 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
isLoadingnorfetchErrorare true, it checks the length of thepostsarray:- If
postsis not empty, it renders aFeedcomponent passing thepostsas a prop. TheFeedcomponent is responsible for rendering the list of posts. - If
postsis 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!
