diff --git a/package-lock.json b/package-lock.json
index efebd73..982abc3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,7 +8,7 @@
"name": "enact",
"version": "0.0.0",
"dependencies": {
- "effection": "^3.3.0",
+ "effection": "^3.4.0",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
@@ -1923,9 +1923,9 @@
"license": "MIT"
},
"node_modules/effection": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/effection/-/effection-3.3.0.tgz",
- "integrity": "sha512-dvU4LIP16zF3F9YOaUib8kzc1wLmC5hQBrKJmLl3WEl/B4J2HcDa1dguIxuBw5UYxJC5u01jieNGyyffRhwWCw==",
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/effection/-/effection-3.4.0.tgz",
+ "integrity": "sha512-QqANcLVEBzKoU9CswdWBfk01i23GZCktsNEcnBSSXZgvYc+1AekHpmU/lras/GJALW36Yp7+FdvcMR3GG1CvKg==",
"license": "ISC",
"engines": {
"node": ">= 16"
diff --git a/package.json b/package.json
index b2e6e12..bd0611e 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
- "effection": "^3.3.0",
+ "effection": "^3.4.0",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
diff --git a/src/App.css b/src/App.css
index b9d355d..0325ce2 100644
--- a/src/App.css
+++ b/src/App.css
@@ -37,6 +37,15 @@
padding: 2em;
}
+/* I'm too used to Tailwind :laughing: */
+.relative {position: relative;}
+.absolute {position: absolute;}
+.opacity-50 {opacity: 0.5;}
+.bg-black {background-color: black;}
+.top-0 {top: 0;}
+.left-0 {left: 0;}
+.size-full {height: 100%; width: 100%}
+
.read-the-docs {
color: #888;
}
diff --git a/src/App.tsx b/src/App.tsx
index e037b89..8d01a4f 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -5,9 +5,10 @@ import { enact } from "./enact.tsx";
import { StopWatch } from "./examples/Stopwatch.tsx";
import { Counter } from "./examples/Counter.tsx";
import { Search } from "./examples/Search.tsx";
+import { $ } from './enact.tsx'
export const App = enact(function* () {
- return (
+ yield* $(
<>
>
);
-});
\ No newline at end of file
+});
diff --git a/src/enact.tsx b/src/enact.tsx
index e5d2653..4badc8a 100644
--- a/src/enact.tsx
+++ b/src/enact.tsx
@@ -6,56 +6,60 @@ import {
createSignal,
each,
type Operation,
+ type Future,
resource,
spawn,
Stream,
} from "effection";
import type { ReactNode } from "react";
-import { useEffect, useState } from "react";
+import React, { useEffect, useState, useRef } from "react";
export interface EnactComponent {
+ // NOTE: Disambiguate between undefined ReactNode's and yielding void.
(props: T): Operation;
}
-export interface ReactComponent {
- (props: T): ReactNode;
-}
-
-export function* render(node: ReactNode): Operation {
+export function* render(current?: ReactNode): Operation {
let setContent = yield* RenderContext.expect();
- setContent(node);
+ setContent(current);
}
-export const $ = render;
+export const r = render;
-const RenderContext = createContext<(node: ReactNode) => void>("enact.render");
+const RenderContext =
+ createContext<(_: ReactNode) => void>("enact.render");
-export function enact(component: EnactComponent): ReactComponent {
+export function enact(component: EnactComponent) {
return (props: T) => {
- let [content, setContent] = useState(null);
+ const [content, setContent] = useState();
+ // Store ref to Future of previous render to block subsequent renders until
+ // cleanup function has run to completion.
+ const destroying = useRef>(void 0);
useEffect(() => {
- let [scope, destroy] = createScope();
+ const [scope, destroy] = createScope();
scope.set(RenderContext, setContent);
- scope.run(function* () {
- try {
- let result = yield* component(props);
- if (result) {
- setContent(result);
+ scope
+ .run(function* () {
+ // Block subsequent renders until cleanup function has run to completion.
+ if (destroying.current) {
+ yield* destroying.current;
}
- } catch (e) {
- let error = e as Error;
- setContent(
- <>
- Component Crash
- {error?.message}
- {error?.stack}
- >,
- );
- }
- });
- return () => { destroy() };
- }, []);
+ const val = yield* component(props);
+ if (React.isValidElement(val)) {
+ setContent(val);
+ }
+ })
+ .catch((e) => {
+ throw new Error(e);
+ });
+ return () => {
+ destroying.current = destroy();
+ destroying.current.catch((e) => {
+ console.error("Cleanup failed:", e);
+ });
+ };
+ }, [props]);
return content;
};
@@ -115,7 +119,7 @@ export function useValue(initial: T): Value {
}
export interface Computed extends Stream {
- react: ReactComponent>;
+ react: React.FC>;
}
export function compute(
@@ -131,7 +135,7 @@ export function compute(
let react = enact>(function* () {
for (let value of yield* each(computed)) {
- yield* $(String(value));
+ yield* r(String(value));
yield* each.next();
}
});
@@ -151,8 +155,8 @@ export function map(
let source = yield* stream;
return {
*next() {
- let next = yield* source.next();
- return next.done ? next : ({ done: false, value: fn(next.value) });
+ let next = yield* source.next();
+ return next.done ? next : { done: false, value: fn(next.value) };
},
};
},
diff --git a/src/examples/Counter.tsx b/src/examples/Counter.tsx
index da57bdc..9f8773d 100644
--- a/src/examples/Counter.tsx
+++ b/src/examples/Counter.tsx
@@ -1,4 +1,4 @@
-import { enact, useValue } from "../enact.tsx";
+import { enact, $, useValue } from "../enact.tsx";
/**
* ```ts
@@ -13,12 +13,13 @@ import { enact, useValue } from "../enact.tsx";
}
```
*/
-export const Counter = enact(function*() {
- let count = useValue(0);
-
- return (
- ,
+ );
+});
+
diff --git a/src/examples/Search.tsx b/src/examples/Search.tsx
index eed74e9..714f429 100644
--- a/src/examples/Search.tsx
+++ b/src/examples/Search.tsx
@@ -1,62 +1,49 @@
-import {
- each,
- spawn,
- sleep,
- useAbortSignal,
- call,
- Task,
-} from "effection";
-import { enact, $, useValue, Value } from "../enact.tsx";
-import { ChangeEventHandler } from "react";
+import { useAbortSignal, until, type Operation } from "effection";
+import { enact, r } from "../enact.tsx";
+import React, { ChangeEventHandler } from "react";
-export const Search = enact<{ query: string | undefined }>(function* (props) {
- const query = useValue(props.query);
+export function Search(props: { query?: string }) {
+ const [query, setQuery] = React.useState(props.query);
const onChange: ChangeEventHandler = (event) => {
- query.set(event.target.value);
+ setQuery(event.target.value);
};
return (
-
-
+
+
);
-});
-
-const SearchResults = enact<{ query: Value }>(function* (props) {
- let lastTask: Task | undefined;
-
- for (const q of yield* each(props.query)) {
- if (!q?.length) {
- yield* $(Enter a keyword to search for packages on NPM.
); // Renders an "Initial State"
- yield* each.next();
- continue; // skip everything else below.
- }
-
- if (lastTask) {
- yield* lastTask.halt();
- }
-
- lastTask = yield* spawn(function* () {
- yield* $(Loading results for {q}...
);
- // Attempting to add debouncing for when things go out of scope below but didn't seem to work :thinking:
-
- yield* sleep(300);
+}
- try {
- let { results } = yield* npmSearch(q);
- yield* $();
- } catch (error) {
- yield* $();
- }
- });
+let results: Results | undefined;
- yield* each.next();
+const SearchResults = enact<{ query?: string }>(function* ({ query }) {
+ if (!query?.length) {
+ results = undefined;
+ return Enter a keyword to search for packages on NPM.
; // Renders an "Initial State"
+ }
+ if (results) {
+ yield* r(
+ ,
+ );
+ } else {
+ yield* r(Loading results for {query}...
);
+ }
+ try {
+ const stuff = yield* npmSearch(query);
+ results = stuff;
+ return ;
+ } catch (error) {
+ return ;
}
});
-function SearchResultsList({ results }: { results: unknown[] }) {
+function SearchResultsList({ results }: Results) {
return results.length === 0 ? (
No results
) : (
@@ -78,19 +65,30 @@ function SearchResultsList({ results }: { results: unknown[] }) {
);
}
-function* npmSearch(query: string) {
+type Results = {
+ results: Array<{
+ package: {
+ name: string;
+ version: string;
+ description: string;
+ links: { npm: string };
+ };
+ }>;
+};
+
+function* npmSearch(query: string): Operation {
const signal = yield* useAbortSignal();
/* npms.io search API is used in this example. Good stuff.*/
const url = `https://api.npms.io/v2/search?from=0&size=25&q=${query}`;
- let response = yield* call(() => fetch(url, { signal }));
+ let response = yield* until(fetch(url, { signal }));
if (response.ok) {
- return yield* call(() => response.json());
+ return yield* until(response.json());
}
/* If API returns some weird stuff and not 2xx, convert it to error and show
on the screen. */
- throw new Error(yield* call(() => response.text()));
+ throw new Error(yield* until(response.text()));
}
function ErrorMessage({ error }: { error: Error }) {