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:

  1. Importing the API module:
    • import api from "./api/posts";: This line imports the api module from the "./api/posts" file. It suggests that the file contains functions to interact with an API, specifically related to fetching posts.
  2. Declaring state variables:
    • const [posts, setPosts] = useState([]);: This line declares a state variable named posts using the useState hook. The initial value of posts is an empty array [], and setPosts is a function that can be used to update the value of posts.
  3. 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 named fetchPosts, 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 from response.data and set as the new value for the posts state variable using setPosts(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.
  4. Invoking the fetchPosts function:
    • fetchPosts();: This line invokes the fetchPosts function defined inside the useEffect 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:

  1. Function declaration:
    • const handleSubmit = async (e) => { ... }: This line declares an asynchronous function named handleSubmit. It takes an event object e as its parameter, which represents the form submission event.
  2. Preventing default form behavior:
    • e.preventDefault();: This line prevents the default behavior of the form submission event, which typically refreshes the page.
  3. Generating a new post ID:
    • const id = posts.length ? posts[posts.length - 1] + 1 : 1;: This line generates a new post ID. If the posts 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.
  4. Formatting the current datetime:
    • const datetime = format(new Date(), "MMMM dd, yyyy pp");: This line uses the format function from the date-fns library to format the current date and time. It generates a string representation of the date in the format "Month day, year time".
  5. Creating a new post object:
    • const newPost = { id, title: postTitle, datetime, body: postBody };: This line creates a new post object using the id, postTitle, datetime, and postBody variables. It represents the data of the new post to be submitted.
  6. 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 the api module. It includes the newPost object as the request payload. The response object is stored in the response variable.
  7. Updating the posts state:
    • const allPosts = [...posts, response.data];: This line creates a new array allPosts by spreading the existing posts array and adding the new post response.data received from the API response.
    • setPosts(allPosts);: This line updates the posts state variable with the new array of posts, triggering a re-render of the component.
  8. Clearing form input values:
    • setPostTitle(""); and setPostBody("");: These lines reset the postTitle and postBody state variables to empty strings, clearing the form input fields.
  9. Navigating to the home page:
    • navigate("/");: This line uses the navigate function from the react-router-dom library to navigate to the home page ("/") after the form submission.
  10. 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 using console.log().

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:

  1. Function declaration:
    • const handleDelete = async (id) => { ... }: This line declares an asynchronous function named handleDelete. It takes an id parameter that represents the ID of the post to be deleted.
  2. 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. The api module is used to perform the request. The await keyword is used to wait for the response before proceeding.
  3. Updating the posts state:
    • const postsList = posts.filter((post) => post.id !== id);: This line creates a new array postsList by filtering the existing posts array. It excludes the post with the specified id, effectively removing it from the list.
    • setPosts(postsList);: This line updates the posts state variable with the new array of posts, triggering a re-render of the component.
  4. Navigating to the home page:
    • navigate("/");: This line uses the navigate function from the react-router-dom library to navigate to the home page ("/") after the post deletion.
  5. 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 using console.log().

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:

  1. State variables:
    • const [editTitle, setEditTitle] = useState("");: This line initializes the editTitle state variable with an empty string as its initial value. The setEditTitle function is used to update the value of editTitle.
    • const [editBody, setEditBody] = useState("");: This line initializes the editBody state variable with an empty string as its initial value. The setEditBody function is used to update the value of editBody.
  2. Function declaration:
    • const handleEdit = async (id) => { ... }: This line declares an asynchronous function named handleEdit. It takes an id parameter that represents the ID of the post to be edited.
  3. Creating an updated post object:
    • const datetime = format(new Date(), "MMMM dd, yyyy pp");: This line creates a formatted date and time string using the format function from the date-fns library. It represents the current date and time.
    • const updatedPost = { id, title: editTitle, datetime, body: editBody };: This line creates an updatedPost object that contains the updated title, body, and datetime values. The editTitle and editBody state variables hold the updated values.
  4. 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 the updatedPost object as the request payload. The api module is used to perform the request. The await keyword is used to wait for the response before proceeding.
  5. Updating the posts state:
    • setPosts(...);: This line updates the posts state variable. It maps over the existing posts array and replaces the post with the matching id 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.
  6. Clearing the input fields and navigating to the home page:
    • setEditTitle(""); and setEditBody("");: These lines reset the editTitle and editBody state variables to empty strings, clearing the input fields.
    • navigate("/");: This line uses the navigate function from the react-router-dom library to navigate to the home page ("/") after the post is edited.
  7. 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 using console.log().

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:

  1. Import statements:
    • import { useEffect } from "react";: This imports the useEffect hook from the React library, which allows performing side effects in functional components.
    • import { useParams, Link } from "react-router-dom";: This imports the useParams hook from the react-router-dom library, which allows accessing the parameters from the URL, and the Link component for navigation.
  2. Function declaration:
    • const Edit = ({ posts, handleEdit, editBody, setEditBody, editTitle, setEditTitle }) => { ... }: This declares a functional component named Edit. It takes several props: posts (the list of posts), handleEdit (a function to handle the post editing), editBody and setEditBody (state variables for the post body during editing), and editTitle and setEditTitle (state variables for the post title during editing).
  3. Getting the post to edit:
    • const { id } = useParams();: This line uses the useParams hook to retrieve the id parameter from the URL.
    • const post = posts.find((post) => post.id.toString() === id);: This line searches for the post with the matching id in the posts array. If found, it assigns the post object to the post variable.
  4. Setting initial values for editing:
    • The useEffect hook is used to set the initial values of editBody and editTitle when the post object changes.
    • useEffect(() => { ... }, [post, setEditBody, setEditTitle]);: This effect runs when post, setEditBody, or setEditTitle change.
    • Inside the effect, if a post is found, the editBody and editTitle state variables are set to the respective values from the post object using the setEditBody and setEditTitle functions.
  5. 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, the handleEdit function is called with the post.id as an argument.
    • If editTitle is falsy (post not found), a message is displayed with a link to the home page.
  6. Export statement:
    • export default Edit;: This exports the Edit 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 called useWindowSize that serves as the custom hook.
  • const [windowSize, setWindowSize] = useState({ width: undefined, height: undefined }): This line initializes the state variable windowSize using the useState hook. The initial state is an object with width and height properties set to undefined.
  • useEffect(() => { ... }, []): This useEffect 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 to componentDidMount in class components.
  • const handleResize = () => { ... }: This is the event handler function that is called when the window is resized. It updates the windowSize state by setting the width and height properties to the current inner width and inner height of the window, respectively.
  • handleResize(): This line invokes the handleResize function immediately to set the initial size of the window.
  • window.addEventListener("resize", handleResize): This line adds the event listener to the resize event of the window, calling the handleResize 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 of useEffect changes. It is returned from the useEffect hook.
  • return windowSize;: Finally, the windowSize 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 and width.
  • Inside the header element with the class "Header", it renders an h1 element containing the title prop value.
  • It also renders an icon based on the width prop value:
    • If the width is less than 768 pixels, it renders the FaMobileAlt icon from the react-icons/fa package, which represents a mobile device.
    • If the width is between 768 and 992 pixels, it renders the FaTabletAlt icon, representing a tablet.
    • If the width is greater than or equal to 992 pixels, it renders the FaLaptop icon, representing a laptop.
  • 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 to null and will be updated with any fetch error message.
    • isLoading is initially set to null 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 using axios.get with the provided URL and cancel token.
    • Inside the fetchData function:
      • It sets the isLoading state to true 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 is true), it updates the data 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 the data state to an empty array.
      • Finally, it sets isLoading to false after a 1-second delay, using setTimeout.
    • The fetchData function is called immediately when the component mounts, using the provided dataUrl.
    • A cleanup function is defined within the hook's useEffect. This function will be called when the component unmounts or when the dataUrl changes. It cancels any ongoing HTTP request by calling source.cancel() and updates the isMounted variable to false.
  • The hook returns an object containing the data, fetchError, and isLoading 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 the posts array:
      • If isLoading is true, 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 nor fetchError are true, it checks the length of the posts array:
        • If posts is not empty, it renders a Feed component passing the posts as a prop. The Feed 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.

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!

Popular Posts

Perform CRUD (Create, Read, Update, Delete) Operations using PHP, MySQL and MAMP : Part 4. My First Angular-15 Ionic-7 App

Visualize Your Data: Showcasing Interactive Charts for Numerical Data using Charts JS Library (Part 23) in Your Angular-15 Ionic-7 App

How to Build a Unit Converter for Your Baking Needs with HTML, CSS & Vanilla JavaScript