remix-response
v1.0.2
Published
Semantic response helpers for your Remix app.
Downloads
3
Readme
remix-response
Semantic response helpers for your Remix app.
remix-response
provides response helpers that wait on all promises to
resolve before serializing the response.
Basic Usage
yarn add remix-response
import type { LoaderArgs } from "@remix-run/node";
import { ok } from 'remix-response';
const wait = (delay: number) => new Promise((r) => setTimeout(r, delay));
const fetchListings = (search: string) => wait(600).then(() => []);
const fetchRecommendations = (user: unknown) => wait(300).then(() => []);
export const loader = async ({ request, context }: LoaderArgs) => {
const listings = fetchListings(request.url);
const recommendations = fetchRecommendations(context.user);
return ok({
listings, // Promise<[]>
recommendations, // Promise<[]>
});
};
export default function MyRouteComponent() {
const data = useLoaderData<typeof loader>(); // { listings: [], recommendations: [] }
// ...
}
Don't go chasin' waterfalls
The simplest way fetch data in a remix loader is to use an async function and unwrap every promise with await.
import type { LoaderArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
const wait = (delay: number) => new Promise((r) => setTimeout(r, delay));
const fetchListings = (search: string) => wait(600).then(() => []);
const fetchRecommendations = (user: unknown) => wait(300).then(() => []);
export const loader = async ({ request, context }: LoaderArgs) => {
const listings = await fetchListings(request.url);
const recommendations = await fetchRecommendations(context.user);
return json({
listings,
recommendations,
});
};
However, if we need to fetch data from multiple independent sources
this can slow down the loader response since fetchRecommendations
doesn't start until after the fetchListings
request has been
completed. A better approach would be to delay waiting until all the
fetchs have been initiated.
export const loader = async ({ request, context }: LoaderArgs) => {
- const listings = await fetchListings(request.url);
+ const listings = fetchListings(request.url);
- const recommendations = await fetchRecommendations(context.user);
+ const recommendations = fetchRecommendations(context.user);
return json({
- listings,
+ listings: await listings,
- recommendations,
+ recommendations: await recommendations,
});
};
This change improves the time it takes to run the loader function because now all the fetches are run in parallel and we only need to wait for the longest fetch to complete.
remix-response
can simplifiy things a bit further by automatically
awaiting any promises provided to the top level object before
serializing the response.
This is similar to the behavior of Promise.all
but it preserves the
object shape and keys similar to RSVP.hash
or bluebird's
Promise.props
.
- import { json } from "@remix-run/node";
+ import { ok } from 'remix-response';
export const loader = async ({ request, context }: LoaderArgs) => {
const listings = fetchListings(request.url);
const recommendations = fetchRecommendations(context.user);
- return json({
+ return ok({
- listings: await listings,
+ listings,
- recommendations: await recommendations,
+ recommendations,
});
};
Errors
When returning a response, if any of the promises reject the response
will have a 500 status code. The data object will contain all of the
properites with an object similar to Promise.allSettled
indicating
if the promises are fulfilled or rejected and the value
/reason
. This
object can be used in your ErrorBoundary
component to render the
appropriate error message.
import type { LoaderArgs } from "@remix-run/node";
import { ok } from 'remix-response';
const wait = (delay: number) => new Promise((r) => setTimeout(r, delay));
const fetchListings = (search: string) => wait(600).then(() => []);
const fetchRecommendations = (user: unknown) => wait(300).then(() => []);
export const loader = async ({ request, context }: LoaderArgs) => {
const listings = fetchListings(request.url);
const recommendations = fetchRecommendations(context.user);
return ok({
listings, // Promise<[]>
recommendations, // Promise<[]>
ohNo: Promise.reject('oops!'),
});
};
export function ErrorBoundary() {
const error = useRouteError();
// {
// status: 500,
// statusText: 'Server Error',
// data: {
// listings: { status: 'fulfilled', value: [] },
// recommendations: { status: 'fulfilled', value: [] },
// ohNo: { status: 'rejected', reason: 'oops' },
// }
// }
return (
<div>
<h1>
{error.status} {error.statusText}
</h1>
<pre>{JSON.stringify(error.data, null, 2)}</pre>
</div>
);
}
If a response is thrown in the loader this indicates an error. Thrown responses will always keep their original status even if a promise rejects. Unlike a returned response, thown responses always use a settled object format with the status and value/reason. This is to ensure the shape will always be consistent in the ErrorBoundary component.
import type { LoaderArgs } from "@remix-run/node";
import { notFound } from 'remix-response';
const wait = (delay: number) => new Promise((r) => setTimeout(r, delay));
const fetchListings = (search: string) => wait(600).then(() => []);
const fetchRecommendations = (user: unknown) => wait(300).then(() => []);
export const loader = async ({ request, context }: LoaderArgs) => {
const listings = fetchListings(request.url);
const recommendations = fetchRecommendations(context.user);
throw notFound({
listings, // Promise<[]>
recommendations, // Promise<[]>
});
};
export function ErrorBoundary() {
const error = useRouteError();
// {
// status: 404,
// statusText: 'Not Found',
// data: {
// listings: { status: 'fulfilled', value: [] },
// recommendations: { status: 'fulfilled', value: [] },
// }
// }
return null;
}
API
Members
import { created } from 'remix-response';
export const action = async () => {
return created({
status: 'new',
id: Promise.resolve(1),
});
};
import { created } from 'remix-response';
export const action = async () => {
return noContent();
};
import { resetContent } from 'remix-response';
export const loader = async () => {
return resetContent({
form: {},
id: Promise.resolve(1),
});
};
import { partialContent } from 'remix-response';
export const loader = async () => {
return partialContent({
title: 'RFC 2616 - Hypertext Transfer Protocol -- HTTP/1.1',
id: Promise.resolve(2616),
});
};
import { movedPermanently } from 'remix-response';
export const loader = async () => {
return movedPermanently('https://www.example.com/');
};
import { found } from 'remix-response';
export const action = async () => {
return found('https://www.example.com/');
};
import { seeOther } from 'remix-response';
export const action = async () => {
return seeOther('https://www.example.com/');
};
import { notModified } from 'remix-response';
export const loader = async ({ request }: LoaderArgs) => {
if (request.headers.get('If-Modified-Since') === 'Wed, 21 Oct 2015 07:28:00 GMT') {
return notModified(request.url);
}
};
import { temporaryRedirect } from 'remix-response';
export const action = async () => {
return temporaryRedirect('https://www.example.com/');
};
import { permanentRedirect } from 'remix-response';
export const action = async () => {
return permanentRedirect('https://www.example.com/');
};
import type { ActionArgs } from "@remix-run/node";
import { badRequest } from 'remix-response';
export async function action({ request }: ActionArgs) {
return badRequest({
form: request.formData(),
errors: Promise.resolve({name: 'missing'}),
});
};
import type { ActionArgs } from "@remix-run/node";
import { unauthorized } from 'remix-response';
export async function action({ request }: ActionArgs) {
return unauthorized({
form: request.formData(),
errors: Promise.resolve({user: 'missing'}),
});
};
import type { ActionArgs } from "@remix-run/node";
import { forbidden } from 'remix-response';
export async function action({ request }: ActionArgs) {
return forbidden({
form: request.formData(),
errors: Promise.resolve({user: 'missing'}),
});
};
import { notFound } from 'remix-response';
export async function loader() {
return notFound({
recommendations: []
fromTheBlog: Promise.resolve([]),
});
};
import { methodNotAllowed } from 'remix-response';
export async function action() {
return methodNotAllowed({
allowedMethods: Promise.resolve(['GET', 'POST']),
});
};
import { notAcceptable } from 'remix-response';
export async function action() {
return notAcceptable({
allowedLanguage: Promise.resolve(['US_en', 'US_es']),
});
};
import { conflict } from 'remix-response';
export async function action() {
return conflict({
error: Promise.resolve({ id: 'duplicate id' }),
});
};
import { gone } from 'remix-response';
export async function action() {
return gone({
error: Promise.resolve('resource deleted'),
});
};
import { preconditionFailed } from 'remix-response';
export async function action() {
return preconditionFailed({
modifiedSince: Promise.resolve(Date.now()),
});
};
import { expectationFailed } from 'remix-response';
export async function action() {
return expectationFailed({
error: Promise.resolve('Content-Length is too large.'),
});
};
import { teapot } from 'remix-response';
export async function action() {
return teapot({
error: Promise.resolve('🚫☕'),
});
};
import { preconditionFailed } from 'remix-response';
export async function action() {
return preconditionFailed({
error: Promise.resolve('Missing If-Match header.'),
});
};
import { tooManyRequests } from 'remix-response';
export async function action() {
return tooManyRequests({
retryIn: Promise.resolve(5 * 60 * 1000),
});
};
import { serverError } from 'remix-response';
export async function loader() {
throw serverError({
error: Promise.resolve('Unable to load resouce.'),
});
};
import { notImplemented } from 'remix-response';
export async function loader() {
throw notImplemented({
error: Promise.resolve('Unable to load resouce.'),
}, {
headers: { 'Retry-After': 300 }
});
};
import { serviceUnavailable } from 'remix-response';
export async function loader() {
throw serviceUnavailable({
error: Promise.resolve('Unable to load resouce.'),
}, {
headers: { 'Retry-After': 300 }
});
};
Constants
import { ok } from 'remix-response';
export const loader = async () => {
return ok({
hello: 'world',
promise: Promise.resolve('result'),
});
};
ok
import { created } from 'remix-response';
export const action = async () => {
return created({
status: 'new',
id: Promise.resolve(1),
});
};
Kind: global variable
| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |
created
import { created } from 'remix-response';
export const action = async () => {
return noContent();
};
Kind: global variable
| Param | Description | | --- | --- | | init? | An optional RequestInit configuration object. |
noContent
import { resetContent } from 'remix-response';
export const loader = async () => {
return resetContent({
form: {},
id: Promise.resolve(1),
});
};
Kind: global variable
| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |
resetContent
import { partialContent } from 'remix-response';
export const loader = async () => {
return partialContent({
title: 'RFC 2616 - Hypertext Transfer Protocol -- HTTP/1.1',
id: Promise.resolve(2616),
});
};
Kind: global variable
| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |
partialContent
import { movedPermanently } from 'remix-response';
export const loader = async () => {
return movedPermanently('https://www.example.com/');
};
Kind: global variable
| Param | Description | | --- | --- | | url | A url to redirect the request to |
movedPermanently
import { found } from 'remix-response';
export const action = async () => {
return found('https://www.example.com/');
};
Kind: global variable
| Param | Description | | --- | --- | | url | A url to redirect the request to |
found
import { seeOther } from 'remix-response';
export const action = async () => {
return seeOther('https://www.example.com/');
};
Kind: global variable
| Param | Description | | --- | --- | | url | A url to redirect the request to |
seeOther
import { notModified } from 'remix-response';
export const loader = async ({ request }: LoaderArgs) => {
if (request.headers.get('If-Modified-Since') === 'Wed, 21 Oct 2015 07:28:00 GMT') {
return notModified(request.url);
}
};
Kind: global variable
| Param | Description | | --- | --- | | url | A url to redirect the request to |
notModified
import { temporaryRedirect } from 'remix-response';
export const action = async () => {
return temporaryRedirect('https://www.example.com/');
};
Kind: global variable
| Param | Description | | --- | --- | | url | A url to redirect the request to |
temporaryRedirect
import { permanentRedirect } from 'remix-response';
export const action = async () => {
return permanentRedirect('https://www.example.com/');
};
Kind: global variable
| Param | Description | | --- | --- | | url | A url to redirect the request to |
permanentRedirect
import type { ActionArgs } from "@remix-run/node";
import { badRequest } from 'remix-response';
export async function action({ request }: ActionArgs) {
return badRequest({
form: request.formData(),
errors: Promise.resolve({name: 'missing'}),
});
};
Kind: global variable
| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |
badRequest
import type { ActionArgs } from "@remix-run/node";
import { unauthorized } from 'remix-response';
export async function action({ request }: ActionArgs) {
return unauthorized({
form: request.formData(),
errors: Promise.resolve({user: 'missing'}),
});
};
Kind: global variable
| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |
unauthorized
import type { ActionArgs } from "@remix-run/node";
import { forbidden } from 'remix-response';
export async function action({ request }: ActionArgs) {
return forbidden({
form: request.formData(),
errors: Promise.resolve({user: 'missing'}),
});
};
Kind: global variable
| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |
forbidden
import { notFound } from 'remix-response';
export async function loader() {
return notFound({
recommendations: []
fromTheBlog: Promise.resolve([]),
});
};
Kind: global variable
| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |
notFound
import { methodNotAllowed } from 'remix-response';
export async function action() {
return methodNotAllowed({
allowedMethods: Promise.resolve(['GET', 'POST']),
});
};
Kind: global variable
| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |
methodNotAllowed
import { notAcceptable } from 'remix-response';
export async function action() {
return notAcceptable({
allowedLanguage: Promise.resolve(['US_en', 'US_es']),
});
};
Kind: global variable
| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |
notAcceptable
import { conflict } from 'remix-response';
export async function action() {
return conflict({
error: Promise.resolve({ id: 'duplicate id' }),
});
};
Kind: global variable
| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |
conflict
import { gone } from 'remix-response';
export async function action() {
return gone({
error: Promise.resolve('resource deleted'),
});
};
Kind: global variable
| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |
gone
import { preconditionFailed } from 'remix-response';
export async function action() {
return preconditionFailed({
modifiedSince: Promise.resolve(Date.now()),
});
};
Kind: global variable
| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |
preconditionFailed
import { expectationFailed } from 'remix-response';
export async function action() {
return expectationFailed({
error: Promise.resolve('Content-Length is too large.'),
});
};
Kind: global variable
| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |
expectationFailed
import { teapot } from 'remix-response';
export async function action() {
return teapot({
error: Promise.resolve('🚫☕'),
});
};
Kind: global variable
| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |
teapot
import { preconditionFailed } from 'remix-response';
export async function action() {
return preconditionFailed({
error: Promise.resolve('Missing If-Match header.'),
});
};
Kind: global variable
| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |
preconditionRequired
import { tooManyRequests } from 'remix-response';
export async function action() {
return tooManyRequests({
retryIn: Promise.resolve(5 * 60 * 1000),
});
};
Kind: global variable
| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |
tooManyRequests
import { serverError } from 'remix-response';
export async function loader() {
throw serverError({
error: Promise.resolve('Unable to load resouce.'),
});
};
Kind: global variable
| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |
serverError
import { notImplemented } from 'remix-response';
export async function loader() {
throw notImplemented({
error: Promise.resolve('Unable to load resouce.'),
}, {
headers: { 'Retry-After': 300 }
});
};
Kind: global variable
| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |
notImplemented
import { serviceUnavailable } from 'remix-response';
export async function loader() {
throw serviceUnavailable({
error: Promise.resolve('Unable to load resouce.'),
}, {
headers: { 'Retry-After': 300 }
});
};
Kind: global variable
| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |
ok
import { ok } from 'remix-response';
export const loader = async () => {
return ok({
hello: 'world',
promise: Promise.resolve('result'),
});
};
Kind: global constant
| Param | Description | | --- | --- | | data | A JavaScript object that will be serialized as JSON. | | init? | An optional RequestInit configuration object. |