Skip to content

Commit

Permalink
Resolves #7 by allowing async functions for event handlers, onMount, …
Browse files Browse the repository at this point in the history
…and on.
  • Loading branch information
joshwilsonvu committed Jan 24, 2022
1 parent 2112b38 commit f15c9db
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 10 deletions.
23 changes: 23 additions & 0 deletions docs/reactivity.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,14 @@ function Component() {
createEffect(() => runWithOwner(owner, () => console.log(signal())));
}

const [photos, setPhotos] = createSignal([]);
createEffect(async () => {
const res = await fetch(
"https://jsonplaceholder.typicode.com/photos?_limit=20"
);
setPhotos(await res.json());
});

```

### Valid Examples
Expand Down Expand Up @@ -262,6 +270,21 @@ setImmediate(() => console.log(signal()));
requestAnimationFrame(() => console.log(signal()));
requestIdleCallback(() => console.log(signal()));

const [photos, setPhotos] = createSignal([]);
onMount(async () => {
const res = await fetch(
"https://jsonplaceholder.typicode.com/photos?_limit=20"
);
setPhotos(await res.json());
});

const [a, setA] = createSignal(1);
const [b] = createSignal(2);
on(b, async () => {
await delay(1000);
setA(a() + 1);
});

```
<!-- AUTO-GENERATED-CONTENT:END -->

Expand Down
22 changes: 12 additions & 10 deletions src/rules/reactivity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@ interface TrackedScope {
/**
* The reactive variable should be one of these types:
* - "function": synchronous function or signal variable
* - "event-handler": synchronous or asynchronous function like a timer or
* event handler that isn't really a tracked scope but acts like one
* - "called-function": synchronous or asynchronous function like a timer or
* event handler that isn't really a tracked scope but allows reactivity
* - "expression": some value containing reactivity somewhere
*/
expect: "function" | "event-handler" | "expression";
expect: "function" | "called-function" | "expression";
}

class ScopeStackItem {
Expand Down Expand Up @@ -271,7 +271,7 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
const matchTrackedScope = (trackedScope: TrackedScope, node: T.Node): boolean => {
switch (trackedScope.expect) {
case "function":
case "event-handler":
case "called-function":
return node === trackedScope.node;
case "expression":
return Boolean(
Expand Down Expand Up @@ -638,7 +638,7 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
) => {
const pushTrackedScope = (node: T.Node, expect: TrackedScope["expect"]) => {
currentScope().trackedScopes.push({ node, expect });
if (expect !== "event-handler" && isFunctionNode(node) && node.async) {
if (expect !== "called-function" && isFunctionNode(node) && node.async) {
// From the docs: "[Solid's] approach only tracks synchronously. If you
// have a setTimeout or use an async function in your Effect the code
// that executes async after the fact won't be tracked."
Expand All @@ -662,7 +662,7 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
const expect =
node.parent?.type === "JSXAttribute" &&
sourceCode.getText(node.parent.name).match(/^on[:A-Z]/)
? "function"
? "called-function"
: "expression";
pushTrackedScope(node.expression, expect);
} else if (node.type === "CallExpression" && node.callee.type === "Identifier") {
Expand All @@ -673,7 +673,6 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
"createMemo",
"children",
"createEffect",
"onMount",
"createRenderEffect",
"createDeferred",
"createComputed",
Expand All @@ -686,17 +685,19 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
pushTrackedScope(arg0, "function");
} else if (
[
"onMount",
"setInterval",
"setTimeout",
"setImmediate",
"requestAnimationFrame",
"requestIdleCallback",
].includes(callee.name)
) {
// onMount can be async.
// Timers are NOT tracked scopes. However, they don't need to react
// to updates to reactive variables; it's okay to poll the current
// value. Consider them event-handler tracked scopes for our purposes.
pushTrackedScope(arg0, "event-handler");
pushTrackedScope(arg0, "called-function");
} else if (callee.name === "createMutable" && arg0) {
pushTrackedScope(arg0, "expression");
} else if (callee.name === "on") {
Expand All @@ -712,7 +713,8 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
}
}
if (node.arguments[1]) {
pushTrackedScope(node.arguments[1], "function");
// Since dependencies are known, function can be async
pushTrackedScope(node.arguments[1], "called-function");
}
} else if (callee.name === "runWithOwner") {
// runWithOwner(owner, fn) only creates a tracked scope if `owner =
Expand Down Expand Up @@ -791,7 +793,7 @@ const rule: TSESLint.RuleModule<MessageIds, []> = {
// where event handlers are manually attached to refs, detect these
// scenarios and mark the right hand sides as tracked scopes expecting
// functions.
pushTrackedScope(node.right, "event-handler");
pushTrackedScope(node.right, "called-function");
}
}
};
Expand Down
19 changes: 19 additions & 0 deletions test/rules/reactivity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,15 @@ export const cases = run("reactivity", rule, {
setImmediate(() => console.log(signal()));
requestAnimationFrame(() => console.log(signal()));
requestIdleCallback(() => console.log(signal()));`,
// Async tracking scope exceptions
`const [photos, setPhotos] = createSignal([]);
onMount(async () => {
const res = await fetch("https://jsonplaceholder.typicode.com/photos?_limit=20");
setPhotos(await res.json());
});`,
`const [a, setA] = createSignal(1);
const [b] = createSignal(2);
on(b, async () => { await delay(1000); setA(a() + 1) });`,
],
invalid: [
// Untracked signals
Expand Down Expand Up @@ -323,5 +332,15 @@ export const cases = run("reactivity", rule, {
}`,
errors: [{ messageId: "badUnnamedDerivedSignal", line: 5 }],
},
// Async tracking scopes
{
code: `
const [photos, setPhotos] = createSignal([]);
createEffect(async () => {
const res = await fetch("https://jsonplaceholder.typicode.com/photos?_limit=20");
setPhotos(await res.json());
});`,
errors: [{ messageId: "noAsyncTrackedScope", line: 3 }],
},
],
});

0 comments on commit f15c9db

Please sign in to comment.