All Projects → Synvox → api

Synvox / api

Licence: MIT license
Simple data loading for React

Programming Languages

typescript
32286 projects

Projects that are alternatives of or similar to api

learn
Vue代理商城项目
Stars: ✭ 40 (+14.29%)
Mutual labels:  axios
vue-music
using Vue to Develop Mobile Project to Simulate “Baidu Music”(高仿百度音乐)
Stars: ✭ 27 (-22.86%)
Mutual labels:  axios
vue-template
🎉 一个集成了 webpack + vue-loader + vuex + axios 的自定义 vue-cli 模板,其中包含 webpack 热更新,linting,测试以及 css 处理器等内容
Stars: ✭ 25 (-28.57%)
Mutual labels:  axios
Vue-NetEaseCloudMusic
vue,vuex ,vue-router,music
Stars: ✭ 95 (+171.43%)
Mutual labels:  axios
mobxSpa
企业级SPA项目,完整开发脚手架
Stars: ✭ 96 (+174.29%)
Mutual labels:  axios
vite-vue3-starter
⭐ A Vite 2.x + Vue 3.x + TypeScript template starter
Stars: ✭ 384 (+997.14%)
Mutual labels:  axios
axios-opentracing
Axios interceptor which traces your requests 👀
Stars: ✭ 15 (-57.14%)
Mutual labels:  axios
axios-elementui-
用axios和elementui做的一个增删改查的小例子
Stars: ✭ 22 (-37.14%)
Mutual labels:  axios
react-rocketshoes
NetShoes Clone with React and Redux
Stars: ✭ 50 (+42.86%)
Mutual labels:  axios
Stime
基于Vue-cli3(Vue+Vue-router)构建的单页单栏Typecho主题,全站Ajax+类Pjax(Vue-router)无刷新,自适应适配移动设备
Stars: ✭ 29 (-17.14%)
Mutual labels:  axios
MovieCards
React App that uses TMDb API to display movie data. Try it out! ->
Stars: ✭ 38 (+8.57%)
Mutual labels:  axios
fptu-app
FUHCM Universal Web App based on Node.js & React
Stars: ✭ 17 (-51.43%)
Mutual labels:  axios
TimeTableManager
Simple react application to create a TimeTable based only on your choice of subjects.
Stars: ✭ 30 (-14.29%)
Mutual labels:  axios
fd-vue-webapp
A Vue.js web application for Freedomotic Open IoT framework
Stars: ✭ 63 (+80%)
Mutual labels:  axios
axios-case-converter
Axios transformer/interceptor that converts snake_case/camelCase
Stars: ✭ 114 (+225.71%)
Mutual labels:  axios
shu-scheduling-helper
A web-based timetabler helping SHUers filter and preselect courses easily. SHU排课助手. 上海大学排课助手.
Stars: ✭ 43 (+22.86%)
Mutual labels:  axios
square-mock
A simple mock-up of the Square Dashboard UI Kit using React, Redux, Webpack, Babel, and Firebase.
Stars: ✭ 21 (-40%)
Mutual labels:  axios
Vue2.0-music
vue2.0全家桶撸的在线音乐播放器1.0
Stars: ✭ 35 (+0%)
Mutual labels:  axios
admin-antd-vue
Vue3.x + Ant Design Admin template (vite/webpack)
Stars: ✭ 111 (+217.14%)
Mutual labels:  axios
element-ui-demo
A element-ui admin base on vue2
Stars: ✭ 18 (-48.57%)
Mutual labels:  axios

@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.

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].