@synvox/api
v1.4.1
Published
![Travis (.org)](https://img.shields.io/travis/synvox/api) ![Codecov](https://img.shields.io/codecov/c/github/synvox/api) ![Bundle Size](https://badgen.net/bundlephobia/minzip/@synvox/api) ![License](https://badgen.net/npm/license/@synvox/api) [![Language
Downloads
44
Readme
@synvox/api
Simple HTTP calls in React using Suspense.
npm i @synvox/api axios
CodeSandbox
Features
- Wrapper around
axios
. Pass in anaxios
instance of your choosing - Small interface
useApi
a suspense compatible hook for loading dataapi
a wrapper around axiostouch(...keys: string[])
to refetch queriesdefer<T>(() => T, defaultValue: T): {data: T, loading:boolean}
to defer an HTTP callpreload(() => any): Promise<void>
to preload an HTTP call
- Run any
GET
request through Suspense - Refresh requests without flickering
- De-duplicates
GET
requests to the same url - Caches urls while they're in use and garbage collects them when they are not.
- Can be used in conditions and loops
- Easy integration with websockets and SSE for real-time apps
- Well tested and and written in Typescript
- Tiny
Basic Example
import { createApi } from '@synvox/api';
import axios from 'axios';
const { useApi } = createApi(
axios.create({
baseURL: 'https://your-api.com',
headers: {
'Authorization': 'Bearer your-token-here'
}
})
);
export useApi;
// then elsewhere:
import { useApi } from './api'
function Post({postId}) {
const api = useApi();
const user = api.users.me.get(); // GET https://your-api.com/users/me
const post = api.posts[postId].get(); // GET https://your-api.com/posts/{postId}
const comments = api.comments.get({postId: post.id}); // GET https://your-api.com/comments?post_id={postId}
const authorName = post.authorId === user.id
? 'You'
: api.users[post.authorId].get().name// GET https://your-api.com/users/{post.authorId}
return <>
<h2>{post.title} by {authorName}</h2>
<p>{post.body}</p>
<ul>
{comments.map(comment=><li key={comment.id}>{comment.body}</li>)}
</ul>
</>;
}
The useApi
hook
useApi
returns a Proxy
that builds an axios request when you call it. For example:
import { createApi } from '@synvox/api';
import axios from 'axios';
const { useApi } = createApi(axios);
// in a component:
const api = useApi();
const users = api.users(); // calls GET /users
const notifications = api.notifications.get(); // calls GET /notifications, defaults to `get` when no method is specified.
const userId = 1;
const comments = api.users({ userId: 1 }); // calls GET /users?user_id=1
const projectId = 2;
const project = api.projects[projectId](); // calls GET /projects/2
const userProject = api.users[userId].projects[projectId]({ active: true }); // calls GET /users/1/projects/2?active=true
Calling api
api.path[urlParam](params: object, config?: AxiosConfig) as Type
// | | | |__ axios options like `data` and `headers`
// | | |__ query params (uses query-string under the hood so arrays work)
// | |__ url params
// \__ the url path
useApi
and the laws of hooks
You cannot wrap a hook in a condition or use it in a loop, but the api
object is not a hook, so feel free to use it wherever data is needed.
const api = useApi();
const users = shouldLoadUsers ? api.users() : [];
return (
<>
{users.map(user => (
<div key={user.id}>
{user.name}: {api.stars.count({ userId: user.id })}
</div>
))}
</>
);
Refetching
Call touch
to refetch queries by url fragment(s).
import { createApi } from '@synvox/api';
import axios from 'axios';
const { useApi, touch } = createApi(axios);
// in a component
const api = useApi();
const [commentBody, setCommentBody] = useState('');
async function submit(e) {
e.preventDefault();
// notice you can specify a method when making a call
await api.comments.post(
{},
{
data: {
body: commentBody,
},
}
);
// when used outside a render phase, api returns an AxiosPromise
await touch('comments', 'users');
setCommentBody('');
}
return <form onSubmit={submit}>// Component stuff</form>;
The touch
function will find all the used requests that contain the word(s) given to touch and run those requests again in the background, only updating the components when all the requests are completed. This helps a ton with flickering and race conditions.
Because touch
is not a hook, it can be used outside a component in a websocket handler or a SSE listener to create real-time experiences.
import { touch } from './api';
const sse = new EventSource('/events');
sse.addEventListener('update', e => {
// assume e.data is {touches: ['messages', 'notifications']}
touch(...e.data.touches);
});
Using api
outside a component
When the api object is used outside a component as its rendering, it will return an axios
call to that url.
import { api } from './api';
export async function logout() {
// notice you can specify a method like `post` when making a call
await api.logout.post();
}
Preloading (and avoiding waterfall requests)
Suspense will wait for promises to fulfill before resuming a render which means requests are not loaded parallel. While this is fine for many components, you may want to start the loading of many requests at once. To do this call preload
:
import { preload, useApi } from './api';
function Component() {
const api = useApi();
// use the same way you would in a render phase
preload(() => api.users());
preload(() => api.posts());
// suspend for /users
const users = api.users();
// suspend for /posts, but the promise for posts will have
// already been created in the preload call above.
const posts = api.posts();
return (
<nav>
<a
href="/tasks"
onMouseDown={() => {
// use preload in a handler if you want
preload(() => {
// works with multiple calls
const user = api.users.me();
const tasks = api.tasks({ userId: user.id });
});
}}
>
Tasks
</a>
</nav>
);
}
Deferring Requests (make request, but don't suspend)
If you need to make a request but need to defer until after the first render, then use defer
:
import { defer } from '@synvox/api';
function Component() {
const api = useApi();
const { data: users, loading } = defer(() => api.users(), []);
if (loading) return <Spinner />;
return <UsersList users={users} />;
}
This still subscribes the component to updates from touch
, request de-duplication, and garbage collection.
Binding Links
You can build graph-like structures with useApi
by adding a modifier. Pass in a modifier
to createApi
to build custom link bindings:
// Transforms responses like {'@links': {comments: '/comments?post_id=123' }} into
// an object where data.comments will load /comments?post_id=123
function bindLinks(object: any, loadUrl: (url: string) => unknown) {
if (!object || typeof object !== 'object') return object;
const { '@links': links } = object;
if (!links) return object;
const returned: any = Array.isArray(object) ? [] : {};
for (let [key, value] of Object.entries(object)) {
if (value && typeof value === 'object') {
returned[key] = bindLinks(value, loadUrl);
} else returned[key] = value;
}
if (!links) return returned;
for (let [key, url] of Object.entries(links)) {
if (!object[key]) {
Object.defineProperty(returned, key, {
get() {
return loadUrl(url as string);
},
enumerable: false,
configurable: false,
});
}
}
return returned;
}
const { useApi } = createApi(axios, {
modifier: bindLinks,
});
Defining nested dependencies
Say you call /comments
which returns Comment[]
and want each Comment
to be loaded into the cache individually so calling /comments/:id
doesn't make another request. You can do this by setting a deduplication strategy.
// will update the cache for all all `{"@url": ...} objects
function deduplicationStrategy(item: any): { [key: string]: any } {
if (!item || typeof item !== 'object') return {};
if (Array.isArray(item))
return item
.map(deduplicationStrategy)
.reduce((a, b) => ({ ...a, ...b }), {});
const result: { [key: string]: any } = {};
for (let value of Object.values(item)) {
Object.assign(result, deduplicationStrategy(value));
}
if (item['@url']) {
result[item['@url']] = item;
}
return result;
}
const { useApi, api, touch, reset, preload } = createApi(axios, {
modifier: bindLinks,
deduplicationStrategy: (item: any) => {
const others = deduplicationStrategy(item);
return others;
},
});
Case Transformations
You can optionally specify a case transformation for request bodies, response bodies, and urls.
createApi(axios, {
requestCase: 'snake' | 'camel' | 'constant' | 'pascal' | 'kebab' | 'none',
responseCase: 'snake' | 'camel' | 'constant' | 'pascal' | 'kebab' | 'none',
urlCase: 'snake' | 'camel' | 'constant' | 'pascal' | 'kebab' | 'none',
});
Saving and Restoring
To save the cache call save
:
const { save, restore } = createApi(axios);
localStorage.__cache = JSON.stringify(save());
To restore the cache call restore
:
const { save, restore } = createApi(axios);
restore(window.data__from__SSR);
Retries
Set retryCount
to specify how many times failing GET
requests should be retried. Requests are delayed by 1s
and double for each retry but will not delay longer than 30s
. E.g. 1s, 2s, 4s, 8s, ..., 30s
Retrying only applies to GET
requests called in a render.
createApi(axios, { retryCount: 10 });
Why not just a useEffect
hook or Redux?
Obligatory Notice about Suspense for data loading
The React team has asked that we do not build on react-cache
until it is stable, but that doesn't mean we can't experiment with an implementation of our own Suspense compatible cache until react-cache
is stable.