Last active
September 25, 2025 23:26
-
-
Save subtleGradient/00f326213ea59c4660584fbd078dabf8 to your computer and use it in GitHub Desktop.
Relay Effect component prop composition
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }) | |
| ) | |
| } | |
| } | |
| ) | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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} /> | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 })} /> | |
| </> | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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}`), | |
| }; | |
| }) | |
| }) { } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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