0.2.4 (#38)
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com> Co-authored-by: Renovate Bot <renovate-bot@valverde.cloud> Reviewed-on: #38
This commit was merged in pull request #38.
This commit is contained in:
71
packages/example/src/routes/async.tsx
Normal file
71
packages/example/src/routes/async.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { HttpClient } from "@effect/platform"
|
||||
import { Container, Flex, Heading, Slider, Text, TextField } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Array, Effect, flow, Option, Schema } from "effect"
|
||||
import { Async, Component, Memoized } from "effect-fc"
|
||||
import * as React from "react"
|
||||
import { runtime } from "@/runtime"
|
||||
|
||||
|
||||
const Post = Schema.Struct({
|
||||
userId: Schema.Int,
|
||||
id: Schema.Int,
|
||||
title: Schema.String,
|
||||
body: Schema.String,
|
||||
})
|
||||
|
||||
interface AsyncFetchPostViewProps {
|
||||
readonly id: number
|
||||
}
|
||||
|
||||
class AsyncFetchPostView extends Component.make("AsyncFetchPostView")(function*(props: AsyncFetchPostViewProps) {
|
||||
const post = yield* Component.useOnChange(() => HttpClient.HttpClient.pipe(
|
||||
Effect.tap(Effect.sleep("500 millis")),
|
||||
Effect.andThen(client => client.get(`https://jsonplaceholder.typicode.com/posts/${ props.id }`)),
|
||||
Effect.andThen(response => response.json),
|
||||
Effect.andThen(Schema.decodeUnknown(Post)),
|
||||
), [props.id])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Heading>{post.title}</Heading>
|
||||
<Text>{post.body}</Text>
|
||||
</div>
|
||||
)
|
||||
}).pipe(
|
||||
Async.async,
|
||||
Async.withOptions({ defaultFallback: <Text>Default fallback</Text> }),
|
||||
Memoized.memoized,
|
||||
) {}
|
||||
|
||||
|
||||
const AsyncRouteComponent = Component.make("AsyncRouteView")(function*() {
|
||||
const [text, setText] = React.useState("Typing here should not trigger a refetch of the post")
|
||||
const [id, setId] = React.useState(1)
|
||||
|
||||
const AsyncFetchPost = yield* AsyncFetchPostView.use
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Flex direction="column" align="stretch" gap="2">
|
||||
<TextField.Root
|
||||
value={text}
|
||||
onChange={e => setText(e.currentTarget.value)}
|
||||
/>
|
||||
|
||||
<Slider
|
||||
value={[id]}
|
||||
onValueChange={flow(Array.head, Option.getOrThrow, setId)}
|
||||
/>
|
||||
|
||||
<AsyncFetchPost id={id} fallback={<Text>Loading post...</Text>} />
|
||||
</Flex>
|
||||
</Container>
|
||||
)
|
||||
}).pipe(
|
||||
Component.withRuntime(runtime.context)
|
||||
)
|
||||
|
||||
export const Route = createFileRoute("/async")({
|
||||
component: AsyncRouteComponent,
|
||||
})
|
||||
@@ -1,78 +0,0 @@
|
||||
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 { Async, Component, Memoized } from "effect-fc"
|
||||
import * as React from "react"
|
||||
import { runtime } from "@/runtime"
|
||||
|
||||
|
||||
// Generator version
|
||||
const RouteComponent = Component.makeUntraced(function* AsyncRendering() {
|
||||
const MemoizedAsyncComponentFC = yield* MemoizedAsyncComponent
|
||||
const AsyncComponentFC = yield* AsyncComponent
|
||||
const [input, setInput] = React.useState("")
|
||||
|
||||
return (
|
||||
<Flex direction="column" align="stretch" gap="2">
|
||||
<TextField.Root
|
||||
value={input}
|
||||
onChange={e => setInput(e.target.value)}
|
||||
/>
|
||||
|
||||
<MemoizedAsyncComponentFC fallback={React.useMemo(() => <p>Loading memoized...</p>, [])} />
|
||||
<AsyncComponentFC />
|
||||
</Flex>
|
||||
)
|
||||
}).pipe(
|
||||
Component.withRuntime(runtime.context)
|
||||
)
|
||||
|
||||
// Pipeline version
|
||||
// const RouteComponent = Component.make("RouteComponent")(() => Effect.Do,
|
||||
// Effect.bind("VMemoizedAsyncComponent", () => Component.useFC(MemoizedAsyncComponent)),
|
||||
// Effect.bind("VAsyncComponent", () => Component.useFC(AsyncComponent)),
|
||||
// Effect.let("input", () => React.useState("")),
|
||||
|
||||
// Effect.map(({ input: [input, setInput], VAsyncComponent, VMemoizedAsyncComponent }) =>
|
||||
// <Flex direction="column" align="stretch" gap="2">
|
||||
// <TextField.Root
|
||||
// value={input}
|
||||
// onChange={e => setInput(e.target.value)}
|
||||
// />
|
||||
|
||||
// <VMemoizedAsyncComponent />
|
||||
// <VAsyncComponent />
|
||||
// </Flex>
|
||||
// ),
|
||||
// ).pipe(
|
||||
// Component.withRuntime(runtime.context)
|
||||
// )
|
||||
|
||||
|
||||
class AsyncComponent extends Component.makeUntraced("AsyncComponent")(function*() {
|
||||
const SubComponentFC = yield* SubComponent
|
||||
|
||||
yield* Effect.sleep("500 millis") // Async operation
|
||||
// Cannot use React hooks after the async operation
|
||||
|
||||
return (
|
||||
<Flex direction="column" align="stretch">
|
||||
<Text>Rendered!</Text>
|
||||
<SubComponentFC />
|
||||
</Flex>
|
||||
)
|
||||
}).pipe(
|
||||
Async.async,
|
||||
Async.withOptions({ defaultFallback: <p>Loading...</p> }),
|
||||
) {}
|
||||
class MemoizedAsyncComponent extends Memoized.memoized(AsyncComponent) {}
|
||||
|
||||
class SubComponent extends Component.makeUntraced("SubComponent")(function*() {
|
||||
const [state] = React.useState(yield* Component.useOnMount(() => Effect.provide(makeUuid4, GetRandomValues.CryptoRandom)))
|
||||
return <Text>{state}</Text>
|
||||
}) {}
|
||||
|
||||
export const Route = createFileRoute("/dev/async-rendering")({
|
||||
component: RouteComponent
|
||||
})
|
||||
@@ -23,7 +23,7 @@ const SubComponent = Component.makeUntraced("SubComponent")(function*() {
|
||||
const ContextView = Component.makeUntraced("ContextView")(function*() {
|
||||
const [serviceValue, setServiceValue] = React.useState("test")
|
||||
const SubServiceLayer = React.useMemo(() => SubService.Default(serviceValue), [serviceValue])
|
||||
const SubComponentFC = yield* Effect.provide(SubComponent, yield* Component.useContext(SubServiceLayer))
|
||||
const SubComponentFC = yield* Effect.provide(SubComponent.use, yield* Component.useContextFromLayer(SubServiceLayer))
|
||||
|
||||
return (
|
||||
<Container>
|
||||
|
||||
@@ -17,8 +17,8 @@ const RouteComponent = Component.makeUntraced("RouteComponent")(function*() {
|
||||
onChange={e => setValue(e.target.value)}
|
||||
/>
|
||||
|
||||
{yield* Effect.map(SubComponent, FC => <FC />)}
|
||||
{yield* Effect.map(MemoizedSubComponent, FC => <FC />)}
|
||||
{yield* Effect.map(SubComponent.use, FC => <FC />)}
|
||||
{yield* Effect.map(MemoizedSubComponent.use, FC => <FC />)}
|
||||
</Flex>
|
||||
)
|
||||
}).pipe(
|
||||
|
||||
@@ -2,7 +2,8 @@ import { Button, Container, Flex, Text } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Console, Effect, Match, Option, ParseResult, Schema } from "effect"
|
||||
import { Component, Form, Subscribable } from "effect-fc"
|
||||
import { TextFieldFormInput } from "@/lib/form/TextFieldFormInput"
|
||||
import { TextFieldFormInputView } from "@/lib/form/TextFieldFormInputView"
|
||||
import { TextFieldOptionalFormInputView } from "@/lib/form/TextFieldOptionalFormInputView"
|
||||
import { DateTimeUtcFromZonedInput } from "@/lib/schema"
|
||||
import { runtime } from "@/runtime"
|
||||
|
||||
@@ -38,7 +39,7 @@ const RegisterFormSubmitSchema = Schema.Struct({
|
||||
birth: Schema.OptionFromSelf(Schema.DateTimeUtcFromSelf),
|
||||
})
|
||||
|
||||
class RegisterForm extends Effect.Service<RegisterForm>()("RegisterForm", {
|
||||
class RegisterFormService extends Effect.Service<RegisterFormService>()("RegisterFormService", {
|
||||
scoped: Form.service({
|
||||
schema: RegisterFormSchema.pipe(
|
||||
Schema.compose(
|
||||
@@ -62,15 +63,16 @@ class RegisterForm extends Effect.Service<RegisterForm>()("RegisterForm", {
|
||||
})
|
||||
}) {}
|
||||
|
||||
class RegisterFormView extends Component.makeUntraced("RegisterFormView")(function*() {
|
||||
const form = yield* RegisterForm
|
||||
class RegisterFormView extends Component.make("RegisterFormView")(function*() {
|
||||
const form = yield* RegisterFormService
|
||||
const [canSubmit, submitResult] = yield* Subscribable.useSubscribables([
|
||||
form.canSubmit,
|
||||
form.mutation.result,
|
||||
])
|
||||
|
||||
const runPromise = yield* Component.useRunPromise()
|
||||
const TextFieldFormInputFC = yield* TextFieldFormInput
|
||||
const TextFieldFormInput = yield* TextFieldFormInputView.use
|
||||
const TextFieldOptionalFormInput = yield* TextFieldOptionalFormInputView.use
|
||||
|
||||
yield* Component.useOnMount(() => Effect.gen(function*() {
|
||||
yield* Effect.addFinalizer(() => Console.log("RegisterFormView unmounted"))
|
||||
@@ -85,16 +87,15 @@ class RegisterFormView extends Component.makeUntraced("RegisterFormView")(functi
|
||||
void runPromise(form.submit)
|
||||
}}>
|
||||
<Flex direction="column" gap="2">
|
||||
<TextFieldFormInputFC
|
||||
<TextFieldFormInput
|
||||
field={yield* form.field(["email"])}
|
||||
/>
|
||||
|
||||
<TextFieldFormInputFC
|
||||
<TextFieldFormInput
|
||||
field={yield* form.field(["password"])}
|
||||
/>
|
||||
|
||||
<TextFieldFormInputFC
|
||||
optional
|
||||
<TextFieldOptionalFormInput
|
||||
type="datetime-local"
|
||||
field={yield* form.field(["birth"])}
|
||||
defaultValue=""
|
||||
@@ -115,13 +116,13 @@ class RegisterFormView extends Component.makeUntraced("RegisterFormView")(functi
|
||||
)
|
||||
}) {}
|
||||
|
||||
const RegisterPage = Component.makeUntraced("RegisterPage")(function*() {
|
||||
const RegisterFormViewFC = yield* Effect.provide(
|
||||
RegisterFormView,
|
||||
yield* Component.useContext(RegisterForm.Default),
|
||||
const RegisterPage = Component.make("RegisterPageView")(function*() {
|
||||
const RegisterForm = yield* Effect.provide(
|
||||
RegisterFormView.use,
|
||||
yield* Component.useContextFromLayer(RegisterFormService.Default),
|
||||
)
|
||||
|
||||
return <RegisterFormViewFC />
|
||||
return <RegisterForm />
|
||||
}).pipe(
|
||||
Component.withRuntime(runtime.context)
|
||||
)
|
||||
|
||||
@@ -2,19 +2,19 @@ import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Effect } from "effect"
|
||||
import { Component } from "effect-fc"
|
||||
import { runtime } from "@/runtime"
|
||||
import { Todos } from "@/todo/Todos"
|
||||
import { TodosState } from "@/todo/TodosState.service"
|
||||
import { TodosState } from "@/todo/TodosState"
|
||||
import { TodosView } from "@/todo/TodosView"
|
||||
|
||||
|
||||
const TodosStateLive = TodosState.Default("todos")
|
||||
|
||||
const Index = Component.makeUntraced("Index")(function*() {
|
||||
const TodosFC = yield* Effect.provide(
|
||||
Todos,
|
||||
yield* Component.useContext(TodosStateLive),
|
||||
const Index = Component.make("IndexView")(function*() {
|
||||
const Todos = yield* Effect.provide(
|
||||
TodosView.use,
|
||||
yield* Component.useContextFromLayer(TodosStateLive),
|
||||
)
|
||||
|
||||
return <TodosFC />
|
||||
return <Todos />
|
||||
}).pipe(
|
||||
Component.withRuntime(runtime.context)
|
||||
)
|
||||
|
||||
@@ -13,15 +13,16 @@ const Post = Schema.Struct({
|
||||
body: Schema.String,
|
||||
})
|
||||
|
||||
const ResultView = Component.makeUntraced("Result")(function*() {
|
||||
const ResultView = Component.make("ResultView")(function*() {
|
||||
const runPromise = yield* Component.useRunPromise()
|
||||
|
||||
const [idRef, query, mutation] = yield* Component.useOnMount(() => Effect.gen(function*() {
|
||||
const idRef = yield* SubscriptionRef.make(1)
|
||||
const key = Stream.map(idRef.changes, id => [id] as const)
|
||||
|
||||
const query = yield* Query.service({
|
||||
key: Stream.zipLatest(Stream.make("posts" as const), idRef.changes),
|
||||
f: ([, id]) => HttpClient.HttpClient.pipe(
|
||||
key,
|
||||
f: ([id]) => HttpClient.HttpClient.pipe(
|
||||
Effect.tap(Effect.sleep("500 millis")),
|
||||
Effect.andThen(client => client.get(`https://jsonplaceholder.typicode.com/posts/${ id }`)),
|
||||
Effect.andThen(response => response.json),
|
||||
|
||||
Reference in New Issue
Block a user