Skip to content

Instantly share code, notes, and snippets.

@subtleGradient
Last active September 25, 2025 23:26
Show Gist options
  • Select an option

  • Save subtleGradient/00f326213ea59c4660584fbd078dabf8 to your computer and use it in GitHub Desktop.

Select an option

Save subtleGradient/00f326213ea59c4660584fbd078dabf8 to your computer and use it in GitHub Desktop.
Relay Effect component prop composition
import { Atom } from "@effect-atom/atom"
import * as AtomReact from "@effect-atom/atom-react"
import { type JSX, jsx } from "@effect-native/patterns/jsx-runtime"
import type * as Context from "effect/Context"
import { dual } from "effect/Function"
import * as Layer from "effect/Layer"
import type * as Types from "effect/Types"
export namespace AtomicComponent {
export interface FC<Props, E = never, R = never> {
(props: Props): JSX.Element
}
type Props<Key, Self> =
& Context.Tag<Self, Self>
& { key: Key }
export const provideProps = dual<
<I extends string, Provided, AllProps, E = never, R = never>(
Props: Props<I, Provided>,
layer: Types.NoInfer<Layer.Layer<Provided, E>>
) => (Component: FC<AllProps, E, R>) => FC<Omit<AllProps, keyof Provided>, E>,
<I extends string, Provided, AllProps, E = never, R = never>(
Component: FC<AllProps, E, R>,
Props: Props<I, Provided>,
layer: Types.NoInfer<Layer.Layer<Provided, E>>
) => FC<Omit<AllProps, keyof Provided>, E>
>(
3,
<I extends string, Provided, AllProps, E = never, R = never>(
Component: FC<AllProps, E, R>,
Props: Props<I, Provided>,
layer: Types.NoInfer<Layer.Layer<Provided, E>>
) => {
type P2 = Omit<AllProps, keyof Provided>
type Out = ReturnType<typeof Component>
const runtimeAtom = Atom.runtime(Layer.mergeAll(layer))
const PropsAtom = runtimeAtom.atom(Props)
return function Loader(props: P2): Out {
const atomicPropsResult = AtomReact.useAtomSuspense(PropsAtom)
return (
jsx(Component, { ...props, ...atomicPropsResult.value })
)
}
}
)
}
import * as SqlClient from "@effect/sql/SqlClient";
import { pipe } from "effect";
import * as Layer from "effect/Layer";
import { AtomicComponent } from "../AtomicComponent.js";
import { TaskListView, TaskListViewProps } from "./TaskList.jsx";
import { TaskModel } from "./TaskModel.jsx";
import * as Reactivity from "@effect/experimental/Reactivity";
const sqlClientLayer = Layer.succeed(SqlClient.SqlClient, null as any as SqlClient.SqlClient);
const layers = Layer.mergeAll(
TaskListViewProps.Default.pipe(Layer.provide(TaskModel.Default.pipe(Layer.provide(sqlClientLayer), Layer.provide(Reactivity.layer)))),
);
const MainComponent = pipe(
TaskListView,
AtomicComponent.provideProps(TaskListViewProps, layers)
);
export default function App() {
return <MainComponent abc={123} />
}
import { Effect } from "effect";
import { Atom } from "@effect-atom/atom";
import { useAtomSet, useAtomSuspense } from "@effect-atom/atom-react";
import { TaskModel } from "./TaskModel.jsx";
import { startTransition } from "react";
import { TaskView, TaskViewProps } from "./TaskView.jsx";
export class TaskListViewProps extends Effect.Service<TaskListViewProps>()("TaskListViewProps", {
effect: Effect.gen(function* () {
const tasksAtom = Atom.make(yield* TaskModel.allTasks);
const selectedTaskAtom = Atom.make({ id: 1, name: "New Task", status: "pending" })
return { tasksAtom, selectedTaskAtom };
})
}) { }
export function TaskListView(props: TaskListViewProps & { abc?: number; }) {
const tasksResult = useAtomSuspense(props.tasksAtom);
const setSelectedTask = useAtomSet(props.selectedTaskAtom);
return <>
<ol>
{tasksResult.value?.map((task) =>
<li key={task.id}>
<button onClick={() => startTransition(() => setSelectedTask(task))}>Select</button>
<TaskView {...TaskViewProps.makeStatic(task)} />
</li>
)}
</ol>
<TaskView {...TaskViewProps.make({ taskAtom: props.selectedTaskAtom })} />
</>
}
import { Reactivity } from "@effect/experimental/Reactivity";
import { SqlClient } from "@effect/sql/SqlClient";
import { Effect } from "effect";
export type TaskRow = {
id: number;
name: string;
status: string;
};
export class TaskModel extends Effect.Service<TaskModel>()("TaskModel", {
accessors: true,
scoped: Effect.gen(function* () {
const sql = yield* SqlClient;
const re = yield* Reactivity;
yield* sql`CREATE TABLE IF NOT EXISTS tasks (id INTEGER PRIMARY KEY, name TEXT, status TEXT)`;
return {
allTasks: sql.reactive(["tasks"], sql<TaskRow> `SELECT id, name, status FROM tasks`),
getTaskById: (id: TaskRow['id']) => sql.reactive([{ tasks: [id] }], sql<TaskRow>`SELECT id, name, status FROM tasks WHERE id = ${id}`.pipe(Effect.map(rows => rows[0] ?? null))),
addTask: (task: TaskRow) => re.mutation({ tasks: [task.id] }, sql`INSERT INTO tasks (id, name, status) VALUES (${task.id}, ${task.name}, ${task.status})`),
updateTask: (task: TaskRow) => re.mutation([{ tasks: [task.id] }], sql`UPDATE tasks SET name = ${task.name}, status = ${task.status} WHERE id = ${task.id}`),
deleteTask: (id: TaskRow['id']) => re.mutation([{ tasks: [id] }], sql`DELETE FROM tasks WHERE id = ${id}`),
};
})
}) { }
import { useAtomValue } from "@effect-atom/atom-react";
import { Atom } from "@effect-atom/atom";
import { Result } from "@effect-atom/atom";
import { Effect } from "effect";
import * as Option from "effect/Option";
import { TaskModel, TaskRow } from "./TaskModel.jsx";
export class TaskViewProps extends Effect.Service<TaskViewProps>()("TaskViewProps", {
effect: Effect.gen(function* () {
const taskAtom0 = Atom.make(yield* TaskModel.getTaskById(1));
const taskAtom = Atom.make(get => get(taskAtom0).pipe(Result.value, Option.getOrThrow));
return { taskAtom };
})
}) {
static makeStatic = (task: TaskRow) => TaskViewProps.make({
taskAtom: Atom.make(task)
});
}
export function TaskView({ taskAtom }: TaskViewProps) {
const task = useAtomValue(taskAtom);
return <div>{task.name} {task.status}</div>;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment