Skip to content

Commit f15c9db

Browse files
committed
Resolves #7 by allowing async functions for event handlers, onMount, and on.
1 parent 2112b38 commit f15c9db

File tree

3 files changed

+54
-10
lines changed

3 files changed

+54
-10
lines changed

docs/reactivity.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,14 @@ function Component() {
135135
createEffect(() => runWithOwner(owner, () => console.log(signal())));
136136
}
137137

138+
const [photos, setPhotos] = createSignal([]);
139+
createEffect(async () => {
140+
const res = await fetch(
141+
"https://jsonplaceholder.typicode.com/photos?_limit=20"
142+
);
143+
setPhotos(await res.json());
144+
});
145+
138146
```
139147

140148
### Valid Examples
@@ -262,6 +270,21 @@ setImmediate(() => console.log(signal()));
262270
requestAnimationFrame(() => console.log(signal()));
263271
requestIdleCallback(() => console.log(signal()));
264272

273+
const [photos, setPhotos] = createSignal([]);
274+
onMount(async () => {
275+
const res = await fetch(
276+
"https://jsonplaceholder.typicode.com/photos?_limit=20"
277+
);
278+
setPhotos(await res.json());
279+
});
280+
281+
const [a, setA] = createSignal(1);
282+
const [b] = createSignal(2);
283+
on(b, async () => {
284+
await delay(1000);
285+
setA(a() + 1);
286+
});
287+
265288
```
266289
<!-- AUTO-GENERATED-CONTENT:END -->
267290

src/rules/reactivity.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,11 @@ interface TrackedScope {
4848
/**
4949
* The reactive variable should be one of these types:
5050
* - "function": synchronous function or signal variable
51-
* - "event-handler": synchronous or asynchronous function like a timer or
52-
* event handler that isn't really a tracked scope but acts like one
51+
* - "called-function": synchronous or asynchronous function like a timer or
52+
* event handler that isn't really a tracked scope but allows reactivity
5353
* - "expression": some value containing reactivity somewhere
5454
*/
55-
expect: "function" | "event-handler" | "expression";
55+
expect: "function" | "called-function" | "expression";
5656
}
5757

5858
class ScopeStackItem {
@@ -271,7 +271,7 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
271271
const matchTrackedScope = (trackedScope: TrackedScope, node: T.Node): boolean => {
272272
switch (trackedScope.expect) {
273273
case "function":
274-
case "event-handler":
274+
case "called-function":
275275
return node === trackedScope.node;
276276
case "expression":
277277
return Boolean(
@@ -638,7 +638,7 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
638638
) => {
639639
const pushTrackedScope = (node: T.Node, expect: TrackedScope["expect"]) => {
640640
currentScope().trackedScopes.push({ node, expect });
641-
if (expect !== "event-handler" && isFunctionNode(node) && node.async) {
641+
if (expect !== "called-function" && isFunctionNode(node) && node.async) {
642642
// From the docs: "[Solid's] approach only tracks synchronously. If you
643643
// have a setTimeout or use an async function in your Effect the code
644644
// that executes async after the fact won't be tracked."
@@ -662,7 +662,7 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
662662
const expect =
663663
node.parent?.type === "JSXAttribute" &&
664664
sourceCode.getText(node.parent.name).match(/^on[:A-Z]/)
665-
? "function"
665+
? "called-function"
666666
: "expression";
667667
pushTrackedScope(node.expression, expect);
668668
} else if (node.type === "CallExpression" && node.callee.type === "Identifier") {
@@ -673,7 +673,6 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
673673
"createMemo",
674674
"children",
675675
"createEffect",
676-
"onMount",
677676
"createRenderEffect",
678677
"createDeferred",
679678
"createComputed",
@@ -686,17 +685,19 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
686685
pushTrackedScope(arg0, "function");
687686
} else if (
688687
[
688+
"onMount",
689689
"setInterval",
690690
"setTimeout",
691691
"setImmediate",
692692
"requestAnimationFrame",
693693
"requestIdleCallback",
694694
].includes(callee.name)
695695
) {
696+
// onMount can be async.
696697
// Timers are NOT tracked scopes. However, they don't need to react
697698
// to updates to reactive variables; it's okay to poll the current
698699
// value. Consider them event-handler tracked scopes for our purposes.
699-
pushTrackedScope(arg0, "event-handler");
700+
pushTrackedScope(arg0, "called-function");
700701
} else if (callee.name === "createMutable" && arg0) {
701702
pushTrackedScope(arg0, "expression");
702703
} else if (callee.name === "on") {
@@ -712,7 +713,8 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
712713
}
713714
}
714715
if (node.arguments[1]) {
715-
pushTrackedScope(node.arguments[1], "function");
716+
// Since dependencies are known, function can be async
717+
pushTrackedScope(node.arguments[1], "called-function");
716718
}
717719
} else if (callee.name === "runWithOwner") {
718720
// runWithOwner(owner, fn) only creates a tracked scope if `owner =
@@ -791,7 +793,7 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
791793
// where event handlers are manually attached to refs, detect these
792794
// scenarios and mark the right hand sides as tracked scopes expecting
793795
// functions.
794-
pushTrackedScope(node.right, "event-handler");
796+
pushTrackedScope(node.right, "called-function");
795797
}
796798
}
797799
};

test/rules/reactivity.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,15 @@ export const cases = run("reactivity", rule, {
101101
setImmediate(() => console.log(signal()));
102102
requestAnimationFrame(() => console.log(signal()));
103103
requestIdleCallback(() => console.log(signal()));`,
104+
// Async tracking scope exceptions
105+
`const [photos, setPhotos] = createSignal([]);
106+
onMount(async () => {
107+
const res = await fetch("https://jsonplaceholder.typicode.com/photos?_limit=20");
108+
setPhotos(await res.json());
109+
});`,
110+
`const [a, setA] = createSignal(1);
111+
const [b] = createSignal(2);
112+
on(b, async () => { await delay(1000); setA(a() + 1) });`,
104113
],
105114
invalid: [
106115
// Untracked signals
@@ -323,5 +332,15 @@ export const cases = run("reactivity", rule, {
323332
}`,
324333
errors: [{ messageId: "badUnnamedDerivedSignal", line: 5 }],
325334
},
335+
// Async tracking scopes
336+
{
337+
code: `
338+
const [photos, setPhotos] = createSignal([]);
339+
createEffect(async () => {
340+
const res = await fetch("https://jsonplaceholder.typicode.com/photos?_limit=20");
341+
setPhotos(await res.json());
342+
});`,
343+
errors: [{ messageId: "noAsyncTrackedScope", line: 3 }],
344+
},
326345
],
327346
});

0 commit comments

Comments
 (0)