In this brief article, I will show you an elegant way (for me) to consume REST API.
Context
When we consume REST API endpoints, it is often the same.
REST API are strict on HTTP verbs and naming of endpoints.
It is often a GET request to fetch array of items and POST / DELETE / PATCH to respectively create / delete / edit an item.
This article only deals with the following HTTP verbs: GET / POST / DELETE / PATCH. You can easly adapt the code for your case.
What do we want
The goal of this article is to create a hook to easly consume REST API.
We want something like this (example of Quotes
REST API management):
const { items, add, remove, edit } = useQuotes();
remove
and notdelete
because this is a reserved keywork in JavaScript
This hook handles the REST API calls AND updates local data!
The fetch is automatically done.
To be able to do it, we use createResource API and Context API.
To facilate the creation of this hook, we create a "Hook contructor" to easily create many types of endpoints.
How is our "Hook constructor" used?
Simply:
useCitations.tsx
import { createRESTApiHook } from "./createRESTApiHook";
export type Quote = {
id: string;
text: string;
title: string;
author?: string;
tags: string[];
numberOfVotes: number;
};
const { Provider, useRESTApi } = createRESTApiHook<Quote>();
export {
Provider as QuotesProvider,
useRESTApi as useQuotes
};
Usage of useQuotes
:
const { items, add, remove, edit, refetch } = useQuotes();
Because we use Context:
App.tsx
<QuotesProvider baseURL="http://localhost:8080/quotes">
// Components using API
</QuotesProvider>
And that's all!
How is our "Hook contructor" created?
Simply:
createRESTApiHook.tsx
import {
createContext,
createResource,
Resource,
JSXElement,
useContext,
} from "solid-js";
interface ProviderProps {
baseURL: string;
children: JSXElement;
}
export function createRESTApiHook<T>() {
interface ContextValue {
items: Resource<T[]>;
add: (item: Omit<T, "id">) => Promise<boolean>;
edit: (itemId: string, item: Partial<T>) => Promise<boolean>;
remove: (itemId: string) => Promise<boolean>;
refetch: () => void;
}
const context = createContext<ContextValue>();
function Provider(props: ProviderProps) {
const [items, { mutate, refetch }] =
createResource<T[]>(fetchItems);
async function fetchItems() {
const res = await fetch(props.baseURL);
return res.json();
}
async function add(item: Omit<T, "id">) {
try {
const res = await fetch(props.baseURL, {
method: "POST",
body: JSON.stringify(item),
headers: {
"Content-Type": "application/json",
},
});
const addedItem = await res.json();
mutate((prev) => (prev ? [...prev, addedItem] : [addedItem]));
return true;
} catch (err) {
console.error(err);
return false;
}
}
async function edit(itemId: string, item: Partial<T>) {
try {
const res = await fetch(`${props.baseURL}/${itemId}`, {
method: "PATCH",
body: JSON.stringify(item),
headers: {
"Content-Type": "application/json",
},
});
const updatedItem = await res.json();
mutate((prev) =>
prev?.map((elt: any) => (elt.id === itemId ? updatedItem : elt))
);
return true;
} catch (err) {
console.error(err);
return false;
}
}
async function remove(itemId: string) {
try {
await fetch(`${props.baseURL}/${itemId}`, {
method: "DELETE",
});
mutate((prev) => prev?.filter((elt: any) => elt.id !== itemId));
return true;
} catch (err) {
console.error(err);
return false;
}
}
const value: ContextValue = {
items,
add,
edit,
remove,
refetch,
};
return <context.Provider value={value}>{props.children}</context.Provider>;
}
function useRESTApi() {
const ctx = useContext(context);
if (!ctx) {
throw new Error("useRESTApi must be used within a RestAPIProvider");
}
return ctx;
}
return {
Provider,
useRESTApi
}
}
Conclusion
You can already put this "Hook contructor" in your application.
Of course, this function can be improved or adapted.
For example, if the ID field of your items is not id
, you can put something else or be adaptable via a props.
Thanks for reading!