From ed95e7789dbbb68ebc637faa951af830dda2786f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Fri, 22 Aug 2025 18:07:21 +0200 Subject: [PATCH] Todo state refactoring --- packages/example/src/todo/Todo.tsx | 52 +++-------------- .../example/src/todo/TodosState.service.ts | 58 ++++++++++++++++--- 2 files changed, 57 insertions(+), 53 deletions(-) diff --git a/packages/example/src/todo/Todo.tsx b/packages/example/src/todo/Todo.tsx index 40f3293..3906320 100644 --- a/packages/example/src/todo/Todo.tsx +++ b/packages/example/src/todo/Todo.tsx @@ -4,10 +4,10 @@ import { TextAreaInput } from "@/lib/TextAreaInput" import { TextFieldInput } from "@/lib/TextFieldInput" import { Box, Button, Flex, IconButton } from "@radix-ui/themes" import { GetRandomValues, makeUuid4 } from "@typed/id" -import { Chunk, DateTime, Effect, flow, identity, Match, Option, Ref, Runtime, Schema, SubscriptionRef } from "effect" +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 { SubscriptionSubRef } from "effect-fc/types" +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" @@ -38,14 +38,11 @@ export class Todo extends Component.makeUntraced(function* Todo(props: TodoProps const { ref, indexRef, contentRef, completedAtRef } = yield* useMemo(() => Match.value(props).pipe( Match.tag("new", () => Effect.Do.pipe( Effect.bind("ref", () => Effect.andThen(makeTodo, SubscriptionRef.make)), - Effect.bind("indexRef", () => SubscriptionRef.make(-1)), + Effect.let("indexRef", () => Subscribable.make({ get: Effect.succeed(-1), changes: Stream.empty })), )), Match.tag("edit", ({ id }) => Effect.Do.pipe( - Effect.let("ref", () => SubscriptionSubRef.makeFromChunkFindFirst(state.ref, v => v.id === id)), - Effect.let("indexRef", () => SubscriptionSubRef.makeFromGetSet(state.ref, { - get: flow(Chunk.findFirstIndex(v => v.id === id), Option.getOrThrow), - set: identity, - })), + Effect.let("ref", () => state.getElementRef(id)), + Effect.let("indexRef", () => state.getIndexSubscribable(id)), )), Match.exhaustive, @@ -93,52 +90,19 @@ export class Todo extends Component.makeUntraced(function* Todo(props: TodoProps Runtime.runSync(runtime)( - SubscriptionRef.updateEffect(state.ref, todos => Effect.Do.pipe( - Effect.bind("todo", () => ref), - Effect.bind("index", () => Chunk.findFirstIndex(todos, v => v.id === props.id)), - Effect.bind("previous", ({ index }) => Chunk.get(todos, index - 1)), - Effect.andThen(({ todo, index, previous }) => index > 0 - ? todos.pipe( - Chunk.replace(index, previous), - Chunk.replace(index - 1, todo), - ) - : todos - ), - )) - )} + onClick={() => Runtime.runSync(runtime)(state.moveLeft(props.id))} > = size - 1} - onClick={() => Runtime.runSync(runtime)( - SubscriptionRef.updateEffect(state.ref, todos => Effect.Do.pipe( - Effect.bind("todo", () => ref), - Effect.bind("index", () => Chunk.findFirstIndex(todos, v => v.id === props.id)), - Effect.bind("next", ({ index }) => Chunk.get(todos, index + 1)), - Effect.andThen(({ todo, index, next }) => index < Chunk.size(todos) - 1 - ? todos.pipe( - Chunk.replace(index, next), - Chunk.replace(index + 1, todo), - ) - : todos - ), - )) - )} + onClick={() => Runtime.runSync(runtime)(state.moveRight(props.id))} > - Runtime.runSync(runtime)( - SubscriptionRef.updateEffect(state.ref, todos => Effect.andThen( - Chunk.findFirstIndex(todos, v => v.id === props.id), - index => Chunk.remove(todos, index), - )) - )} - > + Runtime.runSync(runtime)(state.remove(props.id))}> diff --git a/packages/example/src/todo/TodosState.service.ts b/packages/example/src/todo/TodosState.service.ts index 7fed39e..d024847 100644 --- a/packages/example/src/todo/TodosState.service.ts +++ b/packages/example/src/todo/TodosState.service.ts @@ -2,7 +2,7 @@ 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 } from "effect-fc/types" +import { Subscribable, SubscriptionSubRef } from "effect-fc/types" export class TodosState extends Effect.Service()("TodosState", { @@ -18,7 +18,6 @@ export class TodosState extends Effect.Service()("TodosState", { onNone: () => Effect.succeed(Chunk.empty()), })) ) - const saveToLocalStorage = (todos: Chunk.Chunk) => Effect.andThen( Console.log("Saving todos to local storage..."), Chunk.isNonEmpty(todos) @@ -32,12 +31,6 @@ export class TodosState extends Effect.Service()("TodosState", { ) const ref = yield* SubscriptionRef.make(yield* readFromLocalStorage) - - const sizeSubscribable = Subscribable.make({ - get: Effect.andThen(ref, Chunk.size), - get changes() { return Stream.map(ref.changes, Chunk.size) }, - }) - yield* Effect.forkScoped(ref.changes.pipe( Stream.debounce("500 millis"), Stream.runForEach(saveToLocalStorage), @@ -47,7 +40,54 @@ export class TodosState extends Effect.Service()("TodosState", { Effect.ignore, )) - return { ref, sizeSubscribable } as const + const sizeSubscribable = Subscribable.make({ + get: Effect.andThen(ref, Chunk.size), + get changes() { return Stream.map(ref.changes, Chunk.size) }, + }) + const getElementRef = (id: string) => SubscriptionSubRef.makeFromChunkFindFirst(ref, v => v.id === id) + const getIndexSubscribable = (id: string) => Subscribable.make({ + get: Effect.flatMap(ref, Chunk.findFirstIndex(v => v.id === id)), + get changes() { return Stream.flatMap(ref.changes, Chunk.findFirstIndex(v => v.id === id)) }, + }) + + const moveLeft = (id: string) => SubscriptionRef.updateEffect(ref, todos => Effect.Do.pipe( + Effect.bind("index", () => Chunk.findFirstIndex(todos, v => v.id === id)), + Effect.bind("todo", ({ index }) => Chunk.get(todos, index)), + Effect.bind("previous", ({ index }) => Chunk.get(todos, index - 1)), + Effect.andThen(({ todo, index, previous }) => index > 0 + ? todos.pipe( + Chunk.replace(index, previous), + Chunk.replace(index - 1, todo), + ) + : todos + ), + )) + const moveRight = (id: string) => SubscriptionRef.updateEffect(ref, todos => Effect.Do.pipe( + Effect.bind("index", () => Chunk.findFirstIndex(todos, v => v.id === id)), + Effect.bind("todo", ({ index }) => Chunk.get(todos, index)), + Effect.bind("next", ({ index }) => Chunk.get(todos, index + 1)), + Effect.andThen(({ todo, index, next }) => index < Chunk.size(todos) - 1 + ? todos.pipe( + Chunk.replace(index, next), + Chunk.replace(index + 1, todo), + ) + : todos + ), + )) + const remove = (id: string) => SubscriptionRef.updateEffect(ref, todos => Effect.andThen( + Chunk.findFirstIndex(todos, v => v.id === id), + index => Chunk.remove(todos, index), + )) + + return { + ref, + sizeSubscribable, + getElementRef, + getIndexSubscribable, + moveLeft, + moveRight, + remove, + } as const }), dependencies: [BrowserKeyValueStore.layerLocalStorage],