useAxios + React Container pattern + Typescript

Fetching hook and examples with container pattern (Data layer – Presenter layer)

Test project created with Create React App

Part 1: useAxios

Hook settings

useAxios uses axios.create({}) instance. It’s
important to give the opportunity to customize own axios API or
inject custom request – response intercept logic.

Look src/lib/axios.ts to customize axios instance

Axios instance settings

import axios, { AxiosInstance } from "axios";

// Test URL for requesting fake API
const TEST_URL = "https://jsonplaceholder.typicode.com";

// Axios instance with Type AxiosInstance of @types/axios
const axiosInstance: AxiosInstance = axios.create({
  baseURL: TEST_URL,
});

// Example of adding token to request
axiosInstance.interceptors.request.use(
  (request) => {
    const token = localStorage.getItem("token");
    if (token) {
      request.headers = {
        Authorization: `Bearer ${token}`,
      };
    }
    return request;
  },
  (error) => {
    return Promise.reject(error);
  }
);

export default axiosInstance;

useAxios uses instance from src/lib/axios.ts

Using hook

Import useAxios and get tuple with the following settings

Basic hook example:

import { useAxios } from "./useAxios";

// useAxios return tuple with states object, requestFunction, clearState function
const [{ response, isLoading, error }, requestFunction, clearState] = useAxios<
  ResponseGeneric = Record<string, unknown>,
  ErrorGeneric = Record<string, unknown>,
  RequestDataGeneric =  Record<string, unknown>
>();

To make request use requestFunction – request function have next settings: requestFunction(options: AxiosRequestConfig ) => void.

So you can pass axios request config in function.

Option extends with three optional parameters.
responseCallback?, errorCallback?, cancelCallback?

This functions makes to intercept 3 different events without writing
promise code. Just get only data response with <ResponseGeneric>, intercept error with full AxiosResponse<ErrorGeneric> type or intercept request canceling and do some custom logic.

requestFunction small example

requestFunction({
  url: "/posts",
  method: "get",
  responseCallback: (res: ResponseGeneric) => {
    // Make other request or start display dialog or add custom logic etc.
  },
  errorCallback: (err: AxiosResponse<ErrorGeneric>) => {
    // Show error or handle with error handler
  },
  cancelCallback: () => {
    // Do some background work when request canceled
  },
});

Part 2: Auto fetch, get – post params and data

Auto fetch

Most components make automatic request on mount, fetching data and then render them.

useAxios don’t make automatic request by default because there is can be more complex user logic.
But we always can make request with requestFunction and insert any custom logic in this process.

// We expect array of posts from request, in this case error is {}
const [
  { response: posts, isLoading: postsLoading, error: postsError },
  fetchPosts,
] = useAxios<IPost[]>();

// Using useEffect to automaticaly fetch data from '/posts' endpoint
useEffect(() => {
  // Any custom logic, even make pre request and if response is ok make another request and etc.
  fetchPosts(
    {
      url: "/posts",
      method: "get",
    },
    (response) => {
      // Do some other things or skip callbacks...
      // Response automaticaly have type of IPost[],
      // so you can use array methods and IPost interface
    }
  );
  // You can safe add fetchPost to dependencies
}, [fetchPosts]);

Tip: You can achieve response interception without callbacks,
but callbacks make it more synchronously

Same example but without callback

import { useEffect } from "react";

const [
  { response: posts, isLoading: postsLoading, error: postsError },
  fetchPosts,
] = useAxios<IPost[]>();

useEffect(() => {
  fetchPosts({
    url: "/posts",
    method: "get",
  });
}, [fetchPosts]);

// Another useEffect...
useEffect(() => {
  if (posts && !postsLoading) {
    // Do some other things...
    // Response automaticaly have type of IPost[],
    // so you can use array methods and IPost interface
  }
}, [posts, postsLoading]);

Summary

  • In most cases {response, isLoading, error} used to rendering data and ui, handling and showing errors, showing loading state or blocking ui when some request triggered.

  • Callbacks useful when we need to make something after request, on error or when request canceled. For example: we want call some function from parent component, just pass her through props and call when request done.

Get – post params and data

Because requestFunction takes AxiosRequestConfig we can pass all parameters as in axios.

For example:

// Add slug to url
fetchPost({
  url: `/posts/${postId}`,
  responseCallback,
  errorCallback,
  cancelCallback,
});

// Add data to params: finally /comments?postId=postId
fetchComments({
  url: "/comments",
  params: { postId },
  responseCallback,
  errorCallback,
  cancelCallback,
});

// Add data to post request
// You can specify <RequestDataGeneric> in useAxios to more safe data injection
fetchComments({
  url: "/comments",
  method: "post",
  data: { postId },
  responseCallback,
  errorCallback,
  cancelCallback,
});

//Or all
fetchComments({
  url: "/comments",
  method: "post",
  params: { postId },
  data: { postId },
  responseCallback,
  errorCallback,
  cancelCallback,
});

Part 3: Data Layer – View Layer

About pattern

This part of MVC pattern (Model-View-Controller) but only with Controller – View. Controller represents Data Layer.

In this case we use this pattern with next idea.

  • Data layer is responsible for preparing, configuring and creating API for requests and passing required data and states to View Layer. Data Layer don’t contain View state. But Data layer can control render of View Layer. For example if we need render View layer only when data was get we can put this logic in Data Layer then we don’t need to check response in View layer.
  • View Layer is responsible for rendering ui and states, invoking requests and actions from Data Layer, validating etc.

useAxios + Data – View layer

We make component that find post by entered id and automatically make request to get comments.

Tip: In real project you can pass id from parent component or create your own usage principle

In examples, we use React Container to prepare Data layer.

Interface

We define interface for post and comment from fake API

export interface IPost {
  userId: number;
  id: number;
  title: string;
  body: string;
}

export interface IComment {
  postId: number;
  id: number;
  name: string;
  email: string;
  body: string;
}

Component Props

We don’t provide requestFunction in View Layer directly. For that purpose we make function that accept postId and three callbacks.

// To make callback recive generic types we import them from useAxios hook
import {
  useAxiosCancelCallback,
  useAxiosErrorCallback,
  useAxiosResponseCallback,
} from "../hooks/useAxios";

// Import data types
import { IPost } from "../interfaces/posts.interface";
import { IComment } from "../interfaces/comment.interface";

// Function to get post by post id for View Layer
export type PostRefreshFunction = (
  postId: number,
  responseCallback?: useAxiosResponseCallback<IPost>,
  errorCallback?: useAxiosErrorCallback<Record<string, unknown>>,
  cancelCallback?: useAxiosCancelCallback
) => void;

// Function to get comments by post id for View Layer
export type CommentsRefreshFunction = (
  postId: number,
  responseCallback?: useAxiosResponseCallback<IComment[]>,
  errorCallback?: useAxiosErrorCallback<Record<string, unknown>>,
  cancelCallback?: useAxiosCancelCallback
) => void;

// We want get in View layer post, comments, two function to request them
// and loading states to block buttons
export interface PostViewProps {
  post?: IPost;
  comments?: IComment[];
  onRefreshPost: PostRefreshFunction;
  onRefreshComments: CommentsRefreshFunction;
  onClearComments: () => void;
  loading: { postLoading: boolean; commentsLoading: boolean };
}

// You can define PostProps if you want recive data from parent component...

Data Layer

In data layer we create two useAxios tuples for post and comments.
For comments, we get function to clear state.

import { useCallback } from "react";
import PostView from "./PostView";
import { useAxios } from "../hooks/useAxios";
import { IPost } from "../interfaces/posts.interface";
import { CommentsRefreshFunction, PostRefreshFunction } from "./Post.props";
import { IComment } from "../interfaces/comment.interface";

const Post = (): JSX.Element => {
  // Get states and function from useAxios hook
  const [
    { response: post, isLoading: postLoading, error: postError },
    fetchPost,
  ] = useAxios<IPost, Record<string, unknown>>();

  // Prepare function to get post by id
  const postRefresh: PostRefreshFunction = (
    postId,
    responseCallback,
    errorCallback,
    cancelCallback
  ) => {
    fetchPost({
      url: `/posts/${postId}`,
      responseCallback,
      errorCallback,
      cancelCallback,
    });
  };

  // Get states, request function and clear function for comments
  const [
    { response: comments, isLoading: commentsLoading },
    fetchComments,
    clearComments,
  ] = useAxios<IComment[], Record<string, unknown>>();

  // Prepare function to get comments by post id - this function will start in View Layer useEffect
  // To prevent call loop we add this function to useCallback
  const commentsRefresh: CommentsRefreshFunction = useCallback(
    (postId, responseCallback, errorCallback, cancelCallback) => {
      fetchComments({
        url: "/comments",
        params: { postId: postId },
        responseCallback,
        errorCallback,
        cancelCallback,
      });
    },
    [fetchComments]
  );

  return (
    <>
      {/*Show dialog with loading or etc.*/}
      {postLoading && <p>postLoading...</p>}
      {/*Handle error or add cstom handlers*/}
      {postError && <p>There is no post with that id</p>}
      {/*Pass props by PostViewProps*/}
      <PostView
        post={post}
        comments={comments}
        onRefreshPost={postRefresh}
        onRefreshComments={commentsRefresh}
        onClearComments={clearComments}
        loading={{ postLoading, commentsLoading }}
      />
    </>
  );
};

export default Post;

View Layer

In view layer we use states and functions from Data layer

import { useEffect, useState } from "react";
import { PostViewProps } from "./Post.props";
import { IPost } from "../interfaces/posts.interface";
import { IComment } from "../interfaces/comment.interface";

const PostView = ({
  post,
  comments,
  onRefreshComments,
  onClearComments,
  onRefreshPost,
  loading,
}: PostViewProps): JSX.Element => {
  // State for request - you can use state managment, Form library and etc.
  const [id, setId] = useState<string>("");

  // We define function that takes id and call onRefreshPost function from Data Layer
  // When post fetched we can display this by using callback that pass request with IPost type
  const handleGetPost = (): void => {
    const paramId = parseInt(id);
    if (id && !isNaN(paramId)) {
      onRefreshPost(
        paramId,
        (p) => {
          window.alert(
            `We get post from callback with id:${p.id} userId:${p.userId}`
          );
        },
        () => {
          // When we put wron id we get error. We clear comments state to not show comments if the correct id was entered before
          onClearComments();
        }
      );
    } else {
      window.alert("Enter id number");
    }
  };

  // Catch in View layer when we get post and make request to automatically get comments
  // Its need to use useCallback in Data Layer to prevent loop
  // This part can be made by using callback but somethimes there is more complex logic
  useEffect(() => {
    if (post) {
      onRefreshComments(post.id);
    }
  }, [post, onRefreshComments]);

  // Short function to render post
  const renderPost = (p: IPost): JSX.Element => {
    return (
      <div>
        <p>
          <span>Post ID</span> {p.id}
        </p>
        <p>
          <span>{p.title}</span>
        </p>
        <p>{p.body}</p>
      </div>
    );
  };

  // Short function to render comment
  const renderComment = (c: IComment): JSX.Element => {
    return (
      <div key={c.id}>
        <span>{c.email}</span>
        <p>{c.body}</p>
        <hr />
      </div>
    );
  };

  return (
    <div>
      <h3>Get post by ID</h3>
      <input onChange={(event): void => setId(event.target.value)} />
      <div>
        {/*Call function and block button on requesting*/}
        <button onClick={handleGetPost} disabled={loading.postLoading}>
          Get Post
        </button>
      </div>
      {/*When we get post render them*/}
      {post && (
        <>
          <span>We display post from useAxios datalayer post state</span>
          {renderPost(post)}
        </>
      )}
      <p>
        <strong>Comments</strong>
      </p>
      {comments && comments.map((c) => renderComment(c))}
      <hr />
    </div>
  );
};

export default PostView;

#Finally

If you have any suggestion or ideas make pull request.

Thanks for reading.
This is my first public repository don’t beat me =)

GitHub

View Github