npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@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

Travis (.org) Codecov Bundle Size License Language grade: JavaScript

Simple HTTP calls in React using Suspense.

npm i @synvox/api axios

CodeSandbox

Edit on CodeSandbox

Features

  • Wrapper around axios. Pass in an axios instance of your choosing
  • Small interface
    • useApi a suspense compatible hook for loading data
    • api a wrapper around axios
    • touch(...keys: string[]) to refetch queries
    • defer<T>(() => T, defaultValue: T): {data: T, loading:boolean} to defer an HTTP call
    • preload(() => 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?

See Comparison

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.