Fetching hook and examples with container pattern (Data layer - Presenter layer)
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
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({
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]);
Same example but without callbackTip: You can achieve response interception without callbacks,
but callbacks make it more synchronously
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 =)