Appearance
Axios vs Alette Signal
Axios is a promise-based HTTP client for Node.js and the browser.
Alette Signal is a Front-End data fetching library, designed to be used in any environment except Node.js:
- Alette Signal can be used with UI frameworks like React.
- Alette Signal can be used in browsers and browser-based environments (WebWorkers or Service Workers).
Server interaction
Alette Signal uses XMLHttpRequest for server interaction, just like Axios.
Api instance
Axios:
ts
// src/api/base.ts
import axios from 'axios';
export const instance = axios.create({
baseURL: 'https://some-domain.com/api/',
timeout: 1000,
headers: {'X-Custom-Header': 'foobar'}
});
Alette Signal:
ts
// src/api/client.ts
import {
client,
activatePlugins,
coreApiPlugin,
setOrigin,
} from '@alette/signal';
export const core = coreApiPlugin();
export const api = client(
setOrigin('https://some-domain.com/api/'),
activatePlugins(core.plugin),
// Timeouts are work in progress at the moment
);
export const commonHeaders = {'X-Custom-Header': 'foobar'};
export const {
query: baseQuery,
mutation: baseMutation,
custom: baseCustom,
token,
cookie
} = core.use();
ts
// src/api/base.ts
import {
baseQuery,
baseMutation,
baseCustom,
commonHeaders
} from 'api/client.ts';
import { headers } from '@alette/signal';
export const query = baseQuery(headers(commonHeaders)).toFactory();
export const mutation = baseMutation(headers(commonHeaders)).toFactory();
export const custom = baseCustom(headers(commonHeaders)).toFactory();
Performing a GET request
Axios:
ts
import { instance } from './api/base.ts';
instance.get({
url: '/users',
method: 'get'
});
Alette Signal:
ts
import { query } from './api/base.ts';
import { path } from '@alette/signal';
const getUsers = query(
path('/users')
// method: "get" is set automatically for queries
);
await getPosts.execute();
// or inline
await query(path('/users')).execute();
Instance methods
Axios:
ts
axios.get(/*...*/)
axios.delete(/*...*/)
axios.head(/*...*/)
axios.options(/*...*/)
axios.post(/*...*/)
axios.put(/*...*/)
axios.patch(/*...*/)
Alette Signal:
ts
import { query, mutation, custom } from './api/base.ts';
import { path, deletes, patches, puts, posts, method } from '@alette/signal';
// GET is set automatically for queries
query(/*...*/);
// DELETE
mutation(deletes());
custom(method('HEAD'))
custom(method('OPTIONS'))
// POST is set for mutations by default
mutation(/*...*/);
// PUT
mutation(puts());
// PATCH
mutation(patches());
Handling authentication errors
Axios:
ts
instance.interceptors.response.use(undefined, async (error) => {
if (error.response?.status === 401) {
await refreshToken();
return instance(error.config); // Retry original request
}
throw error;
});
Alette Signal (extending base request blueprints):
ts
// src/api/auth.ts
import { baseMutation, token, commonHeaders } from './api/client.ts';
import { path, headers } from '@alette/signal';
// We are going to use baseMutation() here
// to avoid circular references with "src/api/base.ts"
const refreshToken = baseMutation(
path('/token'),
headers(commonHeaders)
);
export const jwt = token()
.from(async () => {
const { accessToken } = await refreshToken.execute();
return accessToken;
})
.build();
ts
// src/api/base.ts
import { baseQuery, baseMutation, baseCustom } from 'api/client.ts';
import { jwt } from 'api/tokens.ts';
import { bearer } from '@alette/signal';
/*
* 1. 401 status code are retried automatically
* by "query", "mutation" and "custom"
* 2. The bearer() middleware automatically
* invalidates token or cookie when the 401 error is
* encountered.
* 3. The bearer() middleware makes sure only 1
* token request for the same token refresh
* is dispatched at all times - you don't have to use something
* like Mutex - it's built in.
* */
export const query = baseQuery(
headers(commonHeaders),
bearer(jwt)
).toFactory();
export const mutation = baseMutation(
headers(commonHeaders),
bearer(jwt)
).toFactory();
export const custom = baseCustom(
headers(commonHeaders),
bearer(jwt)
).toFactory();
INFO
To learn more about tokens, see Alette Signal token holder guide.
File upload
Axios:
ts
import { instance } from './api/base.ts';
const form = new FormData();
form.append('my_field', 'my value');
form.append('my_buffer', new Blob([1,2,3]));
form.append('my_file', fileInput.files[0]);
await instance.post('/upload', form);
Alette Signal:
ts
import { mutation } from './api/base.ts';
import { path, body } from '@alette/signal';
const form = new FormData();
form.append('my_field', 'my value');
form.append('my_buffer', new Blob([1,2,3]));
form.append('my_file', fileInput.files[0]);
await mutation(path('/upload'), body(form)).execute();
INFO
To learn more about body upload, see Alette Signal body uploading guide.
Retrying requests
Axios does not have a built-in way of retrying requests - you need to install axios-retry or implement the retrying logic yourself.
ts
import axiosRetry from 'axios-retry';
import axios from 'axios';
import { instance } from './api/base.ts';
axiosRetry(instance, { retries: 3 });
// The first request fails and the second returns 'ok'
instance.get('/test')
.then(result => {
result.data; // 'ok'
});
// Exponential back-off retry delay between requests
axiosRetry(instance, { retryDelay: axiosRetry.exponentialDelay });
// Liner retry delay between requests
axiosRetry(instance, { retryDelay: axiosRetry.linearDelay() });
// Custom retry delay
axiosRetry(instance, { retryDelay: (retryCount) => {
return retryCount * 1000;
}});
// Works with custom axios instances
const client = axios.create({ baseURL: 'http://example.com' });
axiosRetry(client, { retries: 3 });
client.get('/test') // The first request fails and the second returns 'ok'
.then(result => {
result.data; // 'ok'
});
// Allows request-specific configuration
client
.get('/test', {
'axios-retry': {
retries: 0
}
})
.catch(error => { // The first request fails
error !== undefined
});
Alette Signal:
ts
import { query } from './api/base.ts';
import { path, retry } from '@alette/signal';
await query(
path('/test'),
retry({
times: 4,
backoff: [1000, 5000, 10000, 15000]
})
).execute();
ts
import { query } from './api/base.ts';
import { path, retryWhen, wait } from '@alette/signal';
await query(
path('/test'),
retryWhen(async ({ attempt }) => {
await wait(attempt * 1000);
return true;
})
).execute();
INFO
To learn more about request retrying, see Alette Signal request retrying guide.
Progress tracking
Axios:
ts
import { instance } from './api/base.ts';
const formData = new FormData();
formData.append("file", yourFile);
instance.post("/upload", formData, {
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
console.log(`Upload progress: ${percentCompleted}%`);
},
});
Alette Signal:
ts
import { mutation } from './api/base.ts';
import { path, body, tapUploadProgress } from '@alette/signal';
const formData = new FormData();
formData.append("file", yourFile);
await mutation(
path('/upload'),
body(formData),
tapUploadProgress(({ progress }) => {
console.log(`Upload progress: ${progress}%`);
})
).execute();
Interceptors
Axios:
ts
// src/api/base.ts
// ...
instance.interceptors.request.use(function (config) {
// Do something before request is sent
return config;
}, function (error) {
// Do something with request error
return Promise.reject(error);
},
{ synchronous: true, runWhen: () => { /* This function returns true */ }}
);
instance.interceptors.response.use(function onFulfilled(response) {
// Do something with response data
return response;
}, function onRejected(error) {
// Do something with response error
return Promise.reject(error);
});
Alette Signal (extending base request blueprints):
ts
// src/api/base.ts
import { baseQuery, baseMutation, baseCustom } from 'api/client.ts';
import {
mapError,
map,
tapError,
tapError,
tapTrigger
} from '@alette/signal';
/*
* 1. You can set unique "interceptors"
* for each request blueprint "branch" individually - when
* you do query(...) in another file, all interceptors
* bound to "base" query will be transferred
* to your newly defined query.
* 2. This is all reflected in TypeScript types.
* */
export const query = baseQuery(
headers(commonHeaders),
tapTrigger(() => {
// Do something before request is sent
}),
tapError(error => {
// Do something with response error (readonly)
}),
mapError(error => {
// Do something with request error
}),
map(response => {
// Do something with response data
})
).toFactory();
export const mutation = baseMutation(
headers(commonHeaders),
tapTrigger(() => {
// Do something before request is sent
}),
tapError(error => {
// Do something with response error (readonly)
}),
mapError(error => {
// Do something with request error
}),
map(response => {
// Do something with response data
})
).toFactory();
export const custom = baseCustom(
headers(commonHeaders),
tapTrigger(() => {
// Do something before request is sent
}),
tapError(error => {
// Do something with response error (readonly)
}),
mapError(error => {
// Do something with request error
}),
map(response => {
// Do something with response data
})
).toFactory();
ts
// src/api/foo.ts
import { query } from './api/base.ts';
import { path } from '@alette/signal';
export const getFoo = query(
path('/foo')
);
// Will be executed with interceptors
await getFoo.execute();
Handling errors
Axios:
ts
import { instance } from 'api/base.ts';
instance.get('/user/12345')
.catch(function (error) {
if (error.response) {
console.log(error.response.data);
console.log(error.response.status);
console.log(error.response.headers);
} else if (error.request) {
console.log(error.request);
} else {
console.log('Error', error.message);
}
console.log(error.config);
});
Alette Signal:
ts
import { query } from './api/base.ts';
import { path } from '@alette/signal';
try {
await query(path('/user/12345')).execute();
} catch (e) {
console.log(error.getReason());
console.log(error.getStatus());
console.log(error.getHeaders());
console.log(error.getServerResponse());
}
Cancelling requests
Axios:
ts
const controller = new AbortController();
axios.get('/foo/bar', {
signal: controller.signal
}).then(function(response) {
//...
});
// cancel the request
controller.abort()
Alette Signal:
ts
import { query } from './api/base.ts';
import { path, abortedBy } from '@alette/signal';
const controller = new AbortController();
/*
* ".spawn()" runs the request
* in the background, without returning
* a Promise back.
* */
query(
path('/foo/bar'),
abortedBy(controller)
).spawn();
// Cancel the request
controller.abort()
UI framework integration
Axios:
tsx
// React component
import React, { useEffect, useState } from 'react';
import { instance } from 'api/base.ts';
const PostSelect = () => {
const [response, setResponse] = useState(null);
useEffect(() => {
/*
* 1. Untyped + you have to track loading
* states and everything else yourself.
* 2. Have to execute the request on mount
* yourself, etc., etc.
* */
instance.get('/foo').then((response) => {
setResponse(response)
})
}, [])
return <div>{ /*...*/ }</div>
};
Alette Signal:
ts
// src/api/foo.ts
import { path } from '@alette/signal';
import { query } from './api/base.ts';
export const getFoo = query(
path('/foo')
);
tsx
// React component
import React, { useEffect, useState } from 'react';
import { useApi } from '@alette/signal-react';
import { getFoo } from '../api/foo.ts'
const PostSelect = () => {
/*
* 1. query() is executed on component mount by default.
* 2. Everything is tracked for you, and
* you can use exposed props to update PostSelect UI.
* */
const {
isUninitialized,
isLoading,
isSuccess,
isError,
data,
error,
settings,
execute,
cancel,
} = useApi(getFoo);
return (
<>
{isLoading && <div>Loading...</div>}
<div>{ /*...*/ }</div>
</>
)
};