Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 36 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
"preview": "vite preview"
},
"dependencies": {
"effection": "^3.3.0",
"effection": "^3.4.0",
"react": "^19.1.0",
"react-dom": "^19.1.0"
"react-dom": "^19.1.0",
"react-error-boundary": "^5.0.0"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
Expand Down
9 changes: 9 additions & 0 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
2 changes: 1 addition & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ export const App = enact(function* () {
</div>
</>
);
});
});
82 changes: 48 additions & 34 deletions src/enact.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,56 +6,70 @@ 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<T> {
// NOTE: Disambiguate between undefined ReactNode's and yielding void.
(props: T): Operation<ReactNode | void>;
}

export interface ReactComponent<T> {
(props: T): ReactNode;
}

export function* render(node: ReactNode): Operation<void> {
export function* render(current?: ReactNode): Operation<void> {
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<T>(component: EnactComponent<T>): ReactComponent<T> {
export function enact<T>(component: EnactComponent<T>) {
return (props: T) => {
let [content, setContent] = useState<ReactNode>(null);
const [content, setContent] = useState<ReactNode>();
// Store ref to Future of previous render to block subsequent renders until
// cleanup function has run to completion.
const destroying = useRef<Future<void>>(void 0);
let [err, setErr] = useState<{error: unknown} | void>();

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* () {
try {
// Block subsequent renders until cleanup function has run to completion.
if (destroying.current) {
yield* destroying.current;
}
const val = yield* component(props);
if (React.isValidElement(val)) {
setContent(val);
}
} catch (error) {
setErr({ error });
}
} catch (e) {
let error = e as Error;
setContent(
<>
<h1>Component Crash</h1>
<h3>{error?.message}</h3>
<pre>{error?.stack}</pre>
</>,
);
}
});
return () => { destroy() };
}, []);
})
return () => {
destroying.current = destroy();
destroying.current
// .then(() => { throw new Error('Caught on cleanup') })
.catch((error) => {
console.error(`uncaught error during teardown`, error)
});
};
}, [props]);

// this is the key weird looking part
useEffect(() => {
if (err?.error) {
throw err.error;
}
}, [err]);

return content;
};
Expand Down Expand Up @@ -115,7 +129,7 @@ export function useValue<T>(initial: T): Value<T> {
}

export interface Computed<T> extends Stream<T, never> {
react: ReactComponent<Record<string | symbol, never>>;
react: React.FC<Record<string | symbol, never>>;
}

export function compute<T>(
Expand All @@ -131,7 +145,7 @@ export function compute<T>(

let react = enact<Record<string, never>>(function* () {
for (let value of yield* each(computed)) {
yield* $(String(value));
yield* r(String(value));
yield* each.next();
}
});
Expand All @@ -151,8 +165,8 @@ export function map<A, B, C>(
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) };
},
};
},
Expand Down
17 changes: 9 additions & 8 deletions src/examples/Counter.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { enact, useValue } from "../enact.tsx";
import { enact, r, useValue } from "../enact.tsx";

/**
* ```ts
Expand All @@ -13,12 +13,13 @@ import { enact, useValue } from "../enact.tsx";
}
```
*/
export const Counter = enact(function*() {
let count = useValue(0);
return (
<button type="button" onClick={() => count.set(count.current + 1)}>
export const Counter = enact(function* () {
let count = useValue(0);

return (
<button type="button" onClick={() => count.set(count.current + 1)}>
count is <count.react />
</button>
);
})
);
});

Loading