Skip to content

Commit edd273b

Browse files
authored
Merge pull request #531 from madaxen86/main
fix: createAsync - catch errors of prev to avoid bubbling error up
2 parents eab3d6c + 8885abf commit edd273b

File tree

6 files changed

+208
-14
lines changed

6 files changed

+208
-14
lines changed

.changeset/silly-hounds-shop.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@solidjs/router": patch
3+
---
4+
5+
fix: createAsync - catch errors of prev to avoid bubbling error up

src/data/createAsync.ts

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
/**
22
* This is mock of the eventual Solid 2.0 primitive. It is not fully featured.
33
*/
4-
import { type Accessor, createResource, sharedConfig, type Setter, untrack } from "solid-js";
4+
import {
5+
type Accessor,
6+
createResource,
7+
sharedConfig,
8+
type Setter,
9+
untrack,
10+
catchError
11+
} from "solid-js";
512
import { createStore, reconcile, type ReconcileOptions, unwrap } from "solid-js/store";
613
import { isServer } from "solid-js/web";
714

@@ -13,7 +20,7 @@ import { isServer } from "solid-js/web";
1320
export type AccessorWithLatest<T> = {
1421
(): T;
1522
latest: T;
16-
}
23+
};
1724

1825
export function createAsync<T>(
1926
fn: (prev: T) => Promise<T>,
@@ -40,19 +47,28 @@ export function createAsync<T>(
4047
}
4148
): AccessorWithLatest<T | undefined> {
4249
let resource: () => T;
43-
let prev = () => !resource || (resource as any).state === "unresolved" ? undefined : (resource as any).latest;
50+
let prev = () =>
51+
!resource || (resource as any).state === "unresolved" ? undefined : (resource as any).latest;
52+
4453
[resource] = createResource(
45-
() => subFetch(fn, untrack(prev)),
54+
() =>
55+
subFetch(
56+
fn,
57+
catchError(
58+
() => untrack(prev),
59+
() => undefined
60+
)
61+
),
4662
v => v,
4763
options as any
4864
);
4965

5066
const resultAccessor: AccessorWithLatest<T> = (() => resource()) as any;
51-
Object.defineProperty(resultAccessor, 'latest', {
67+
Object.defineProperty(resultAccessor, "latest", {
5268
get() {
5369
return (resource as any).latest;
5470
}
55-
})
71+
});
5672

5773
return resultAccessor;
5874
}
@@ -85,9 +101,20 @@ export function createAsyncStore<T>(
85101
} = {}
86102
): AccessorWithLatest<T | undefined> {
87103
let resource: () => T;
88-
let prev = () => !resource || (resource as any).state === "unresolved" ? undefined : unwrap((resource as any).latest);
104+
105+
let prev = () =>
106+
!resource || (resource as any).state === "unresolved"
107+
? undefined
108+
: unwrap((resource as any).latest);
89109
[resource] = createResource(
90-
() => subFetch(fn, untrack(prev)),
110+
() =>
111+
subFetch(
112+
fn,
113+
catchError(
114+
() => untrack(prev),
115+
() => undefined
116+
)
117+
),
91118
v => v,
92119
{
93120
...options,
@@ -96,11 +123,11 @@ export function createAsyncStore<T>(
96123
);
97124

98125
const resultAccessor: AccessorWithLatest<T> = (() => resource()) as any;
99-
Object.defineProperty(resultAccessor, 'latest', {
126+
Object.defineProperty(resultAccessor, "latest", {
100127
get() {
101128
return (resource as any).latest;
102129
}
103-
})
130+
});
104131

105132
return resultAccessor;
106133
}

test/data.spec.tsx

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import {
2+
ErrorBoundary,
3+
ParentProps,
4+
Suspense,
5+
catchError,
6+
createRoot,
7+
createSignal
8+
} from "solid-js";
9+
import { render } from "solid-js/web";
10+
import { createAsync, createAsyncStore } from "../src/data";
11+
import { awaitPromise, waitFor } from "./helpers";
12+
13+
function Parent(props: ParentProps) {
14+
return <ErrorBoundary fallback={<div id="parentError" />}>{props.children}</ErrorBoundary>;
15+
}
16+
17+
async function getText(arg?: string) {
18+
return arg || "fallback";
19+
}
20+
async function getError(arg?: any): Promise<any> {
21+
throw Error("error");
22+
}
23+
24+
describe("createAsync should", () => {
25+
test("return 'fallback'", () => {
26+
createRoot(() => {
27+
const data = createAsync(() => getText());
28+
setTimeout(() => expect(data()).toBe("fallback"), 1);
29+
});
30+
});
31+
test("return 'text'", () => {
32+
createRoot(() => {
33+
const data = createAsync(() => getText("text"));
34+
setTimeout(() => expect(data()).toBe("text"), 1);
35+
});
36+
});
37+
test("initial error to be caught ", () => {
38+
createRoot(() => {
39+
const data = createAsync(() => getError());
40+
setTimeout(() => catchError(data, err => expect(err).toBeInstanceOf(Error)), 1);
41+
});
42+
});
43+
test("catch error after arg change - initial valid", () =>
44+
createRoot(async dispose => {
45+
async function throwWhenError(arg: string): Promise<string> {
46+
if (arg === "error") throw new Error("error");
47+
return arg;
48+
}
49+
50+
const [arg, setArg] = createSignal("");
51+
function Child() {
52+
const data = createAsync(() => throwWhenError(arg()));
53+
54+
return (
55+
<div id="child">
56+
<ErrorBoundary
57+
fallback={(_, reset) => (
58+
<div id="childError">
59+
<button
60+
id="reset"
61+
onClick={() => {
62+
setArg("true");
63+
reset();
64+
}}
65+
/>
66+
</div>
67+
)}
68+
>
69+
<Suspense>
70+
<p id="data">{data()}</p>
71+
<p id="latest">{data.latest}</p>
72+
</Suspense>
73+
</ErrorBoundary>
74+
</div>
75+
);
76+
}
77+
await render(
78+
() => (
79+
<Parent>
80+
<Child />
81+
</Parent>
82+
),
83+
document.body
84+
);
85+
const childErrorElement = () => document.getElementById("childError");
86+
const parentErrorElement = document.getElementById("parentError");
87+
expect(childErrorElement()).toBeNull();
88+
expect(parentErrorElement).toBeNull();
89+
setArg("error");
90+
await awaitPromise();
91+
92+
// after changing the arg the error should still be caught by the Child's ErrorBoundary
93+
expect(childErrorElement()).not.toBeNull();
94+
expect(parentErrorElement).toBeNull();
95+
96+
//reset ErrorBoundary
97+
document.getElementById("reset")?.click();
98+
99+
expect(childErrorElement()).toBeNull();
100+
await awaitPromise();
101+
const dataEl = () => document.getElementById("data");
102+
103+
expect(dataEl()).not.toBeNull();
104+
expect(document.getElementById("data")?.innerHTML).toBe("true");
105+
expect(document.getElementById("latest")?.innerHTML).toBe("true");
106+
107+
document.body.innerHTML = "";
108+
dispose();
109+
}));
110+
test("catch consecutive error after initial error change to be caught after arg change", () =>
111+
createRoot(async cleanup => {
112+
const [arg, setArg] = createSignal("error");
113+
function Child() {
114+
const data = createAsync(() => getError(arg()));
115+
116+
return (
117+
<div id="child">
118+
<ErrorBoundary
119+
fallback={(_, reset) => (
120+
<div id="childError">
121+
<button id="reset" onClick={() => reset()} />
122+
</div>
123+
)}
124+
>
125+
<Suspense>{data()}</Suspense>
126+
</ErrorBoundary>
127+
</div>
128+
);
129+
}
130+
await render(
131+
() => (
132+
<Parent>
133+
<Child />
134+
</Parent>
135+
),
136+
document.body
137+
);
138+
139+
// Child's ErrorBoundary should catch the error
140+
expect(document.getElementById("childError")).not.toBeNull();
141+
expect(document.getElementById("parentError")).toBeNull();
142+
setArg("error_2");
143+
await awaitPromise();
144+
// after changing the arg the error should still be caught by the Child's ErrorBoundary
145+
expect(document.getElementById("childError")).not.toBeNull();
146+
expect(document.getElementById("parentError")).toBeNull();
147+
148+
document.getElementById("reset")?.click();
149+
await awaitPromise();
150+
expect(document.getElementById("childError")).not.toBeNull();
151+
expect(document.getElementById("parentError")).toBeNull();
152+
153+
document.body.innerHTML = "";
154+
cleanup();
155+
}));
156+
});

test/helpers.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,7 @@ export function createAsyncRoot(fn: (resolve: () => void, disposer: () => void)
2323
createRoot(disposer => fn(resolve, disposer));
2424
});
2525
}
26+
27+
export async function awaitPromise() {
28+
return new Promise(resolve => setTimeout(resolve, 100));
29+
}

test/tsconfig.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"extends": "./../tsconfig.json",
3+
"include": ["."]
4+
}

tsconfig.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@
1010
"outDir": "./dist",
1111
"module": "esnext"
1212
},
13-
"include": [
14-
"./src"
15-
],
13+
"include": ["./src"],
1614
"exclude": ["node_modules/"]
17-
}
15+
}

0 commit comments

Comments
 (0)