0.1.4 (#5)
All checks were successful
Publish / publish (push) Successful in 21s
Lint / lint (push) Successful in 12s

Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Reviewed-on: #5
This commit was merged in pull request #5.
This commit is contained in:
2025-10-02 18:18:23 +02:00
parent 831a808568
commit 9a3c91b50b
66 changed files with 1157 additions and 634 deletions

View File

@@ -0,0 +1,8 @@
{
"$schema": "https://biomejs.dev/schemas/latest/schema.json",
"root": false,
"extends": "//",
"files": {
"includes": ["./src/**", "!src/routeTree.gen.ts"]
}
}

View File

@@ -1,28 +0,0 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)

View File

@@ -5,43 +5,40 @@
"private": true,
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint:tsc": "tsc --noEmit",
"lint:eslint": "eslint .",
"preview": "vite preview"
"lint:biome": "biome lint",
"preview": "vite preview",
"clean:cache": "rm -rf .turbo node_modules/.tmp node_modules/.vite* .tanstack",
"clean:dist": "rm -rf dist",
"clean:modules": "rm -rf node_modules"
},
"devDependencies": {
"@effect/language-service": "^0.35.2",
"@eslint/js": "^9.34.0",
"@tanstack/react-router": "^1.131.27",
"@tanstack/react-router-devtools": "^1.131.27",
"@tanstack/router-plugin": "^1.131.27",
"@types/react": "^19.1.11",
"@types/react-dom": "^19.1.7",
"@vitejs/plugin-react": "^5.0.1",
"eslint": "^9.34.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"typescript-eslint": "^8.40.0",
"vite": "^7.1.3"
"@tanstack/react-router": "^1.132.31",
"@tanstack/react-router-devtools": "^1.132.31",
"@tanstack/router-plugin": "^1.132.31",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@vitejs/plugin-react": "^5.0.4",
"globals": "^16.4.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"type-fest": "^5.0.1",
"vite": "^7.1.8"
},
"dependencies": {
"@effect/platform": "^0.90.6",
"@effect/platform-browser": "^0.70.0",
"@effect/platform": "^0.92.1",
"@effect/platform-browser": "^0.72.0",
"@radix-ui/themes": "^3.2.1",
"@typed/async-data": "^0.13.1",
"@typed/id": "^0.17.2",
"@typed/lazy-ref": "^0.3.3",
"effect": "^3.17.9",
"effect": "^3.18.1",
"effect-fc": "workspace:*",
"react-icons": "^5.5.0"
},
"overrides": {
"@types/react": "^19.1.11",
"effect": "^3.17.9",
"react": "^19.1.1"
"@types/react": "^19.2.0",
"effect": "^3.18.1",
"react": "^19.2.0"
}
}

View File

@@ -0,0 +1,77 @@
import { Callout, Flex, Spinner, Switch, TextField } from "@radix-ui/themes"
import { Array, Option } from "effect"
import { Component, Form, Hooks } from "effect-fc"
interface Props
extends TextField.RootProps, Form.useInput.Options {
readonly optional?: false
readonly field: Form.FormField<any, string>
}
interface OptionalProps
extends Omit<TextField.RootProps, "defaultValue">, Form.useOptionalInput.Options<string> {
readonly optional: true
readonly field: Form.FormField<any, Option.Option<string>>
}
export type TextFieldFormInputProps = Props | OptionalProps
export class TextFieldFormInput extends Component.makeUntraced("TextFieldFormInput")(
function*(props: TextFieldFormInputProps) {
const input: (
| { readonly optional: true } & Form.useOptionalInput.Result<string>
| { readonly optional: false } & Form.useInput.Result<string>
) = props.optional
// biome-ignore lint/correctness/useHookAtTopLevel: "optional" reactivity not supported
? { optional: true, ...yield* Form.useOptionalInput(props.field, props) }
// biome-ignore lint/correctness/useHookAtTopLevel: "optional" reactivity not supported
: { optional: false, ...yield* Form.useInput(props.field, props) }
const [issues, isValidating, isSubmitting] = yield* Hooks.useSubscribables(
props.field.issuesSubscribable,
props.field.isValidatingSubscribable,
props.field.isSubmittingSubscribable,
)
return (
<Flex direction="column" gap="1">
<TextField.Root
value={input.value}
onChange={e => input.setValue(e.target.value)}
disabled={(input.optional && !input.enabled) || isSubmitting}
{...props}
>
{input.optional &&
<TextField.Slot side="left">
<Switch
size="1"
checked={input.enabled}
onCheckedChange={input.setEnabled}
/>
</TextField.Slot>
}
{isValidating &&
<TextField.Slot side="right">
<Spinner />
</TextField.Slot>
}
{props.children}
</TextField.Root>
{Option.match(Array.head(issues), {
onSome: issue => (
<Callout.Root>
<Callout.Text>{issue.message}</Callout.Text>
</Callout.Root>
),
onNone: () => <></>,
})}
</Flex>
)
}
) {}

View File

@@ -1,7 +1,8 @@
import { Callout, Flex, TextArea, TextAreaProps } from "@radix-ui/themes"
import { Array, Equivalence, Option, ParseResult, Schema, Struct } from "effect"
/** biome-ignore-all lint/correctness/useHookAtTopLevel: effect-fc HOC */
import { Callout, Flex, TextArea, type TextAreaProps } from "@radix-ui/themes"
import { Array, type Equivalence, Option, ParseResult, type Schema, Struct } from "effect"
import { Component } from "effect-fc"
import { useInput } from "effect-fc/hooks"
import { useInput } from "effect-fc/Hooks"
import * as React from "react"
@@ -15,7 +16,7 @@ export const TextAreaInput = <A, R>(options: {
React.JSX.Element,
ParseResult.ParseError,
R
> => Component.makeUntraced(function* TextFieldInput(props) {
> => Component.makeUntraced("TextFieldInput")(function*(props) {
const input = yield* useInput({ ...options, ...props })
const issue = React.useMemo(() => input.error.pipe(
Option.map(ParseResult.ArrayFormatter.formatErrorSync),

View File

@@ -1,7 +1,8 @@
/** biome-ignore-all lint/correctness/useHookAtTopLevel: effect-fc HOC */
import { Callout, Checkbox, Flex, TextField } from "@radix-ui/themes"
import { Array, Equivalence, Option, ParseResult, Schema, Struct } from "effect"
import { Component, Memo } from "effect-fc"
import { useInput, useOptionalInput } from "effect-fc/hooks"
import { Array, type Equivalence, Option, ParseResult, type Schema, Struct } from "effect"
import { Component } from "effect-fc"
import { useInput, useOptionalInput } from "effect-fc/Hooks"
import * as React from "react"
@@ -18,7 +19,7 @@ export const TextFieldInput = <A, R, O extends boolean = false>(options: {
readonly optional?: O
readonly schema: Schema.Schema<A, string, R>
readonly equivalence?: Equivalence.Equivalence<A>
}) => Component.makeUntraced(function* TextFieldInput(props: O extends true
}) => Component.makeUntraced("TextFieldInput")(function*(props: O extends true
? TextFieldOptionalInputProps<A, R>
: TextFieldInputProps<A, R>
) {
@@ -28,12 +29,10 @@ export const TextFieldInput = <A, R, O extends boolean = false>(options: {
) = options.optional
? {
optional: true,
// eslint-disable-next-line react-hooks/rules-of-hooks
...yield* useOptionalInput({ ...options, ...props as TextFieldOptionalInputProps<A, R> }),
}
: {
optional: false,
// eslint-disable-next-line react-hooks/rules-of-hooks
...yield* useInput({ ...options, ...props as TextFieldInputProps<A, R> }),
}
@@ -67,4 +66,4 @@ export const TextFieldInput = <A, R, O extends boolean = false>(options: {
}
</Flex>
)
}).pipe(Memo.memo)
})

View File

@@ -14,6 +14,7 @@ declare module "@tanstack/react-router" {
}
}
// biome-ignore lint/style/noNonNullAssertion: React entrypoint
createRoot(document.getElementById("root")!).render(
<StrictMode>
<ReactRuntime.Provider runtime={runtime}>

View File

@@ -9,12 +9,18 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as FormRouteImport } from './routes/form'
import { Route as BlankRouteImport } from './routes/blank'
import { Route as IndexRouteImport } from './routes/index'
import { Route as DevMemoRouteImport } from './routes/dev/memo'
import { Route as DevInputRouteImport } from './routes/dev/input'
import { Route as DevAsyncRenderingRouteImport } from './routes/dev/async-rendering'
const FormRoute = FormRouteImport.update({
id: '/form',
path: '/form',
getParentRoute: () => rootRouteImport,
} as any)
const BlankRoute = BlankRouteImport.update({
id: '/blank',
path: '/blank',
@@ -44,6 +50,7 @@ const DevAsyncRenderingRoute = DevAsyncRenderingRouteImport.update({
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/blank': typeof BlankRoute
'/form': typeof FormRoute
'/dev/async-rendering': typeof DevAsyncRenderingRoute
'/dev/input': typeof DevInputRoute
'/dev/memo': typeof DevMemoRoute
@@ -51,6 +58,7 @@ export interface FileRoutesByFullPath {
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/blank': typeof BlankRoute
'/form': typeof FormRoute
'/dev/async-rendering': typeof DevAsyncRenderingRoute
'/dev/input': typeof DevInputRoute
'/dev/memo': typeof DevMemoRoute
@@ -59,6 +67,7 @@ export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/blank': typeof BlankRoute
'/form': typeof FormRoute
'/dev/async-rendering': typeof DevAsyncRenderingRoute
'/dev/input': typeof DevInputRoute
'/dev/memo': typeof DevMemoRoute
@@ -68,15 +77,23 @@ export interface FileRouteTypes {
fullPaths:
| '/'
| '/blank'
| '/form'
| '/dev/async-rendering'
| '/dev/input'
| '/dev/memo'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/blank' | '/dev/async-rendering' | '/dev/input' | '/dev/memo'
to:
| '/'
| '/blank'
| '/form'
| '/dev/async-rendering'
| '/dev/input'
| '/dev/memo'
id:
| '__root__'
| '/'
| '/blank'
| '/form'
| '/dev/async-rendering'
| '/dev/input'
| '/dev/memo'
@@ -85,6 +102,7 @@ export interface FileRouteTypes {
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
BlankRoute: typeof BlankRoute
FormRoute: typeof FormRoute
DevAsyncRenderingRoute: typeof DevAsyncRenderingRoute
DevInputRoute: typeof DevInputRoute
DevMemoRoute: typeof DevMemoRoute
@@ -92,6 +110,13 @@ export interface RootRouteChildren {
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/form': {
id: '/form'
path: '/form'
fullPath: '/form'
preLoaderRoute: typeof FormRouteImport
parentRoute: typeof rootRouteImport
}
'/blank': {
id: '/blank'
path: '/blank'
@@ -133,6 +158,7 @@ declare module '@tanstack/react-router' {
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
BlankRoute: BlankRoute,
FormRoute: FormRoute,
DevAsyncRenderingRoute: DevAsyncRenderingRoute,
DevInputRoute: DevInputRoute,
DevMemoRoute: DevMemoRoute,

View File

@@ -1,11 +1,10 @@
import { runtime } from "@/runtime"
import { Flex, Text, TextField } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router"
import { GetRandomValues, makeUuid4 } from "@typed/id"
import { Effect } from "effect"
import { Component, Memo, Suspense } from "effect-fc"
import { Hooks } from "effect-fc/hooks"
import { Async, Component, Hooks, Memoized } from "effect-fc"
import * as React from "react"
import { runtime } from "@/runtime"
// Generator version
@@ -51,7 +50,7 @@ const RouteComponent = Component.makeUntraced(function* AsyncRendering() {
// )
class AsyncComponent extends Component.makeUntraced(function* AsyncComponent() {
class AsyncComponent extends Component.makeUntraced("AsyncComponent")(function*() {
const SubComponentFC = yield* SubComponent
yield* Effect.sleep("500 millis") // Async operation
@@ -64,12 +63,12 @@ class AsyncComponent extends Component.makeUntraced(function* AsyncComponent() {
</Flex>
)
}).pipe(
Suspense.suspense,
Suspense.withOptions({ defaultFallback: <p>Loading...</p> }),
Async.async,
Async.withOptions({ defaultFallback: <p>Loading...</p> }),
) {}
class MemoizedAsyncComponent extends Memo.memo(AsyncComponent) {}
class MemoizedAsyncComponent extends Memoized.memoized(AsyncComponent) {}
class SubComponent extends Component.makeUntraced(function* SubComponent() {
class SubComponent extends Component.makeUntraced("SubComponent")(function*() {
const [state] = React.useState(yield* Hooks.useOnce(() => Effect.provide(makeUuid4, GetRandomValues.CryptoRandom)))
return <Text>{state}</Text>
}) {}

View File

@@ -1,10 +1,9 @@
import { TextFieldInput } from "@/lib/input/TextFieldInput"
import { runtime } from "@/runtime"
import { Container } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router"
import { Schema, SubscriptionRef } from "effect"
import { Component, Memo } from "effect-fc"
import { useInput, useOnce, useRefState } from "effect-fc/hooks"
import { Component, Hooks, Memoized } from "effect-fc"
import { TextFieldInput } from "@/lib/input/TextFieldInput"
import { runtime } from "@/runtime"
const IntFromString = Schema.NumberFromString.pipe(Schema.int())
@@ -12,18 +11,18 @@ const IntFromString = Schema.NumberFromString.pipe(Schema.int())
const IntTextFieldInput = TextFieldInput({ schema: IntFromString })
const StringTextFieldInput = TextFieldInput({ schema: Schema.String })
const Input = Component.makeUntraced(function* Input() {
const Input = Component.makeUntraced("Input")(function*() {
const IntTextFieldInputFC = yield* IntTextFieldInput
const StringTextFieldInputFC = yield* StringTextFieldInput
const intRef1 = yield* useOnce(() => SubscriptionRef.make(0))
const intRef2 = yield* useOnce(() => SubscriptionRef.make(0))
const stringRef = yield* useOnce(() => SubscriptionRef.make(""))
const intRef1 = yield* Hooks.useOnce(() => SubscriptionRef.make(0))
// const intRef2 = yield* useOnce(() => SubscriptionRef.make(0))
const stringRef = yield* Hooks.useOnce(() => SubscriptionRef.make(""))
// yield* useFork(() => Stream.runForEach(intRef1.changes, Console.log), [intRef1])
const input2 = yield* useInput({ schema: IntFromString, ref: intRef2 })
// const input2 = yield* useInput({ schema: IntFromString, ref: intRef2 })
const [str, setStr] = yield* useRefState(stringRef)
// const [str, setStr] = yield* useRefState(stringRef)
return (
<Container>
@@ -33,7 +32,7 @@ const Input = Component.makeUntraced(function* Input() {
</Container>
)
}).pipe(
Memo.memo,
Memoized.memoized,
Component.withRuntime(runtime.context)
)

View File

@@ -1,13 +1,13 @@
import { runtime } from "@/runtime"
import { Flex, Text, TextField } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router"
import { GetRandomValues, makeUuid4 } from "@typed/id"
import { Effect } from "effect"
import { Component, Memo } from "effect-fc"
import { Component, Memoized } from "effect-fc"
import * as React from "react"
import { runtime } from "@/runtime"
const RouteComponent = Component.makeUntraced(function* RouteComponent() {
const RouteComponent = Component.makeUntraced("RouteComponent")(function*() {
const [value, setValue] = React.useState("")
return (
@@ -25,12 +25,12 @@ const RouteComponent = Component.makeUntraced(function* RouteComponent() {
Component.withRuntime(runtime.context)
)
class SubComponent extends Component.makeUntraced(function* SubComponent() {
class SubComponent extends Component.makeUntraced("SubComponent")(function*() {
const id = yield* makeUuid4.pipe(Effect.provide(GetRandomValues.CryptoRandom))
return <Text>{id}</Text>
}) {}
class MemoizedSubComponent extends Memo.memo(SubComponent) {}
class MemoizedSubComponent extends Memoized.memoized(SubComponent) {}
export const Route = createFileRoute("/dev/memo")({
component: RouteComponent,

View File

@@ -0,0 +1,99 @@
import { Button, Container, Flex } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router"
import { Console, Effect, Option, ParseResult, Schema } from "effect"
import { Component, Form, Hooks } from "effect-fc"
import { TextFieldFormInput } from "@/lib/form/TextFieldFormInput"
import { DateTimeUtcFromZonedInput } from "@/lib/schema"
import { runtime } from "@/runtime"
const email = Schema.pattern<typeof Schema.String>(
/^(?!\.)(?!.*\.\.)([A-Z0-9_+-.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9-]*\.)+[A-Z]{2,}$/i,
{
identifier: "email",
title: "email",
message: () => "Not an email address",
},
)
const RegisterFormSchema = Schema.Struct({
email: Schema.String.pipe(email),
password: Schema.String.pipe(Schema.minLength(3)),
birth: Schema.OptionFromSelf(DateTimeUtcFromZonedInput),
})
class RegisterForm extends Effect.Service<RegisterForm>()("RegisterForm", {
scoped: Form.service({
schema: RegisterFormSchema.pipe(
Schema.compose(
Schema.transformOrFail(
Schema.typeSchema(RegisterFormSchema),
Schema.typeSchema(RegisterFormSchema),
{
decode: v => Effect.andThen(Effect.sleep("500 millis"), ParseResult.succeed(v)),
encode: ParseResult.succeed,
},
),
),
),
initialEncodedValue: { email: "", password: "", birth: Option.none() },
submit: v => Effect.sleep("500 millis").pipe(
Effect.andThen(Console.log(v)),
Effect.andThen(Effect.sync(() => alert("Done!"))),
),
debounce: "500 millis",
})
}) {}
class RegisterPage extends Component.makeUntraced("RegisterPage")(function*() {
const form = yield* RegisterForm
const submit = yield* Form.useSubmit(form)
const [canSubmit] = yield* Hooks.useSubscribables(form.canSubmitSubscribable)
const TextFieldFormInputFC = yield* TextFieldFormInput
return (
<Container width="300">
<form onSubmit={e => {
e.preventDefault()
void submit()
}}>
<Flex direction="column" gap="2">
<TextFieldFormInputFC
field={Form.useField(form, ["email"])}
/>
<TextFieldFormInputFC
field={Form.useField(form, ["password"])}
/>
<TextFieldFormInputFC
optional
type="datetime-local"
field={Form.useField(form, ["birth"])}
defaultValue=""
/>
<Button disabled={!canSubmit}>Submit</Button>
</Flex>
</form>
</Container>
)
}) {}
export const Route = createFileRoute("/form")({
component: Component.makeUntraced("RegisterRoute")(function*() {
const RegisterRouteFC = yield* Effect.provide(
RegisterPage,
yield* Hooks.useContext(RegisterForm.Default, { finalizerExecutionMode: "fork" }),
)
return <RegisterRouteFC />
}).pipe(
Component.withRuntime(runtime.context)
)
})

View File

@@ -1,17 +1,18 @@
import { createFileRoute } from "@tanstack/react-router"
import { Effect } from "effect"
import { Component, Hooks } from "effect-fc"
import { runtime } from "@/runtime"
import { Todos } from "@/todo/Todos"
import { TodosState } from "@/todo/TodosState.service"
import { createFileRoute } from "@tanstack/react-router"
import { Effect } from "effect"
import { Component } from "effect-fc"
import { useContext } from "effect-fc/hooks"
const TodosStateLive = TodosState.Default("todos")
const Index = Component.makeUntraced(function* Index() {
const context = yield* useContext(TodosStateLive, { finalizerExecutionMode: "fork" })
const TodosFC = yield* Effect.provide(Todos, context)
const Index = Component.makeUntraced("Index")(function*() {
const TodosFC = yield* Effect.provide(
Todos,
yield* Hooks.useContext(TodosStateLive, { finalizerExecutionMode: "fork" }),
)
return <TodosFC />
}).pipe(

View File

@@ -1,15 +1,13 @@
import { Box, Button, Flex, IconButton } from "@radix-ui/themes"
import { GetRandomValues, makeUuid4 } from "@typed/id"
import { Chunk, DateTime, Effect, Match, Option, Ref, Runtime, Schema, Stream, SubscriptionRef } from "effect"
import { Component, Hooks, Memoized, Subscribable, SubscriptionSubRef } from "effect-fc"
import { FaArrowDown, FaArrowUp } from "react-icons/fa"
import { FaDeleteLeft } from "react-icons/fa6"
import * as Domain from "@/domain"
import { TextAreaInput } from "@/lib/input/TextAreaInput"
import { TextFieldInput } from "@/lib/input/TextFieldInput"
import { DateTimeUtcFromZonedInput } from "@/lib/schema"
import { Box, Button, Flex, IconButton } from "@radix-ui/themes"
import { GetRandomValues, makeUuid4 } from "@typed/id"
import { Chunk, DateTime, Effect, Match, Option, Ref, Runtime, Schema, Stream, SubscriptionRef } from "effect"
import { Component, Memo } from "effect-fc"
import { useMemo, useOnce, useSubscribe } from "effect-fc/hooks"
import { Subscribable, SubscriptionSubRef } from "effect-fc/types"
import { FaArrowDown, FaArrowUp } from "react-icons/fa"
import { FaDeleteLeft } from "react-icons/fa6"
import { TodosState } from "./TodosState.service"
@@ -31,11 +29,11 @@ export type TodoProps = (
| { readonly _tag: "edit", readonly id: string }
)
export class Todo extends Component.makeUntraced(function* Todo(props: TodoProps) {
export class Todo extends Component.makeUntraced("Todo")(function*(props: TodoProps) {
const runtime = yield* Effect.runtime()
const state = yield* TodosState
const { ref, indexRef, contentRef, completedAtRef } = yield* useMemo(() => Match.value(props).pipe(
const { ref, indexRef, contentRef, completedAtRef } = yield* Hooks.useMemo(() => Match.value(props).pipe(
Match.tag("new", () => Effect.Do.pipe(
Effect.bind("ref", () => Effect.andThen(makeTodo, SubscriptionRef.make)),
Effect.let("indexRef", () => Subscribable.make({ get: Effect.succeed(-1), changes: Stream.empty })),
@@ -48,10 +46,9 @@ export class Todo extends Component.makeUntraced(function* Todo(props: TodoProps
Effect.let("contentRef", ({ ref }) => SubscriptionSubRef.makeFromPath(ref, ["content"])),
Effect.let("completedAtRef", ({ ref }) => SubscriptionSubRef.makeFromPath(ref, ["completedAt"])),
// eslint-disable-next-line react-hooks/exhaustive-deps
), [props._tag, props._tag === "edit" ? props.id : undefined])
const [index, size] = yield* useSubscribe(indexRef, state.sizeSubscribable)
const [index, size] = yield* Hooks.useSubscribables(indexRef, state.sizeSubscribable)
const StringTextAreaInputFC = yield* StringTextAreaInput
const OptionalDateTimeInputFC = yield* OptionalDateTimeInput
@@ -67,7 +64,7 @@ export class Todo extends Component.makeUntraced(function* Todo(props: TodoProps
<OptionalDateTimeInputFC
type="datetime-local"
ref={completedAtRef}
defaultValue={yield* useOnce(() => DateTime.now)}
defaultValue={yield* Hooks.useOnce(() => DateTime.now)}
/>
{props._tag === "new" &&
@@ -109,4 +106,6 @@ export class Todo extends Component.makeUntraced(function* Todo(props: TodoProps
}
</Flex>
)
}).pipe(Memo.memo) {}
}).pipe(
Memoized.memoized
) {}

View File

@@ -1,16 +1,15 @@
import { Container, Flex, Heading } from "@radix-ui/themes"
import { Chunk, Console, Effect } from "effect"
import { Component } from "effect-fc"
import { useOnce, useSubscribe } from "effect-fc/hooks"
import { Component, Hooks } from "effect-fc"
import { Todo } from "./Todo"
import { TodosState } from "./TodosState.service"
export class Todos extends Component.makeUntraced(function* Todos() {
export class Todos extends Component.makeUntraced("Todos")(function*() {
const state = yield* TodosState
const [todos] = yield* useSubscribe(state.ref)
const [todos] = yield* Hooks.useSubscribables(state.ref)
yield* useOnce(() => Effect.andThen(
yield* Hooks.useOnce(() => Effect.andThen(
Console.log("Todos mounted"),
Effect.addFinalizer(() => Console.log("Todos unmounted")),
))

View File

@@ -1,8 +1,8 @@
import { Todo } from "@/domain"
import { KeyValueStore } from "@effect/platform"
import { BrowserKeyValueStore } from "@effect/platform-browser"
import { Chunk, Console, Effect, Option, Schema, Stream, SubscriptionRef } from "effect"
import { Subscribable, SubscriptionSubRef } from "effect-fc/types"
import { Subscribable, SubscriptionSubRef } from "effect-fc"
import { Todo } from "@/domain"
export class TodosState extends Effect.Service<TodosState>()("TodosState", {

View File

@@ -22,15 +22,15 @@
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"plugins": [
{
"name": "@effect/language-service"
}
{ "name": "@effect/language-service" }
]
},
"include": ["src"]
}

View File

@@ -20,5 +20,6 @@
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}