Skip to content

Commit df33160

Browse files
authored
Add support for unstable_data for single fetch usage (#11836)
1 parent 01d0f41 commit df33160

File tree

5 files changed

+80
-6
lines changed

5 files changed

+80
-6
lines changed

.changeset/fifty-rockets-hide.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@remix-run/router": minor
3+
---
4+
5+
Add a new `unstable_data()` API for usage with Remix Single Fetch
6+
7+
- This API is not intended for direct usage in React Router SPA applications
8+
- It is primarily intended for usage with `createStaticHandler.query()` to allow loaders/actions to return arbitrary data + `status`/`headers` without forcing the serialization of data into a `Response` instance
9+
- This allows for more advanced serialization tactics via `unstable_dataStrategy` such as serializing via `turbo-stream` in Remix Single Fetch
10+
- ⚠️ This removes the `status` field from `HandlerResult`
11+
- If you need to return a specific `status` from `unstable_dataStrategy` you should instead do so via `unstable_data()`

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@
105105
},
106106
"filesize": {
107107
"packages/router/dist/router.umd.min.js": {
108-
"none": "56.4 kB"
108+
"none": "57.1 kB"
109109
},
110110
"packages/react-router/dist/react-router.production.min.js": {
111111
"none": "14.9 kB"

packages/router/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export type {
3737

3838
export {
3939
AbortedDeferredError,
40+
data as unstable_data,
4041
defer,
4142
generatePath,
4243
getToPathname,

packages/router/router.ts

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import type {
3636
V7_FormMethod,
3737
V7_MutationFormMethod,
3838
AgnosticPatchRoutesOnMissFunction,
39+
DataWithResponseInit,
3940
} from "./utils";
4041
import {
4142
ErrorResponseImpl,
@@ -4906,7 +4907,7 @@ async function callLoaderOrAction(
49064907
async function convertHandlerResultToDataResult(
49074908
handlerResult: HandlerResult
49084909
): Promise<DataResult> {
4909-
let { result, type, status } = handlerResult;
4910+
let { result, type } = handlerResult;
49104911

49114912
if (isResponse(result)) {
49124913
let data: any;
@@ -4946,10 +4947,26 @@ async function convertHandlerResultToDataResult(
49464947
}
49474948

49484949
if (type === ResultType.error) {
4950+
if (isDataWithResponseInit(result)) {
4951+
if (result.data instanceof Error) {
4952+
return {
4953+
type: ResultType.error,
4954+
error: result.data,
4955+
statusCode: result.init?.status,
4956+
};
4957+
}
4958+
4959+
// Convert thrown unstable_data() to ErrorResponse instances
4960+
result = new ErrorResponseImpl(
4961+
result.init?.status || 500,
4962+
undefined,
4963+
result.data
4964+
);
4965+
}
49494966
return {
49504967
type: ResultType.error,
49514968
error: result,
4952-
statusCode: isRouteErrorResponse(result) ? result.status : status,
4969+
statusCode: isRouteErrorResponse(result) ? result.status : undefined,
49534970
};
49544971
}
49554972

@@ -4962,7 +4979,18 @@ async function convertHandlerResultToDataResult(
49624979
};
49634980
}
49644981

4965-
return { type: ResultType.data, data: result, statusCode: status };
4982+
if (isDataWithResponseInit(result)) {
4983+
return {
4984+
type: ResultType.data,
4985+
data: result.data,
4986+
statusCode: result.init?.status,
4987+
headers: result.init?.headers
4988+
? new Headers(result.init.headers)
4989+
: undefined,
4990+
};
4991+
}
4992+
4993+
return { type: ResultType.data, data: result };
49664994
}
49674995

49684996
// Support relative routing in internal redirects
@@ -5476,6 +5504,19 @@ function isRedirectResult(result?: DataResult): result is RedirectResult {
54765504
return (result && result.type) === ResultType.redirect;
54775505
}
54785506

5507+
export function isDataWithResponseInit(
5508+
value: any
5509+
): value is DataWithResponseInit<unknown> {
5510+
return (
5511+
typeof value === "object" &&
5512+
value != null &&
5513+
"type" in value &&
5514+
"data" in value &&
5515+
"init" in value &&
5516+
value.type === "DataWithResponseInit"
5517+
);
5518+
}
5519+
54795520
export function isDeferredData(value: any): value is DeferredData {
54805521
let deferred: DeferredData = value;
54815522
return (

packages/router/utils.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,7 @@ export type DataResult =
6868
*/
6969
export interface HandlerResult {
7070
type: "data" | "error";
71-
result: unknown; // data, Error, Response, DeferredData
72-
status?: number;
71+
result: unknown; // data, Error, Response, DeferredData, DataWithResponseInit
7372
}
7473

7574
type LowerCaseFormMethod = "get" | "post" | "put" | "patch" | "delete";
@@ -1375,6 +1374,28 @@ export const json: JsonFunction = (data, init = {}) => {
13751374
});
13761375
};
13771376

1377+
export class DataWithResponseInit<D> {
1378+
type: string = "DataWithResponseInit";
1379+
data: D;
1380+
init: ResponseInit | null;
1381+
1382+
constructor(data: D, init?: ResponseInit) {
1383+
this.data = data;
1384+
this.init = init || null;
1385+
}
1386+
}
1387+
1388+
/**
1389+
* Create "responses" that contain `status`/`headers` without forcing
1390+
* serialization into an actual `Response` - used by Remix single fetch
1391+
*/
1392+
export function data<D>(data: D, init?: number | ResponseInit) {
1393+
return new DataWithResponseInit(
1394+
data,
1395+
typeof init === "number" ? { status: init } : init
1396+
);
1397+
}
1398+
13781399
export interface TrackedPromise extends Promise<any> {
13791400
_tracked?: boolean;
13801401
_data?: any;

0 commit comments

Comments
 (0)