diff --git a/.trae/documents/plan_20260209_024840.md b/.trae/documents/plan_20260209_024840.md new file mode 100644 index 00000000..fbb963d7 --- /dev/null +++ b/.trae/documents/plan_20260209_024840.md @@ -0,0 +1,34 @@ +Voy a proceder con la actualización mayor a **v0.3.0** de `@effectify/solid-effect-atom`, implementando los cambios solicitados para alinearse con `effect-smol` y soportar Effect v3 mediante `@effect-atom/atom`. + +### Plan de Implementación Detallado + +1. **Reingeniería del Paquete (`packages/solid/effect-atom`)**: + * **Limpieza**: Eliminar archivos obsoletos (`primitives.ts`, `advanced-hooks.ts`, `solid-hydration.ts`, `ssr-utils.ts`, `context.ts`). + * **Nuevo Núcleo (Dash Case)**: + * Crear `src/hooks.ts`: Portado de `Hooks.ts` de `effect-smol`. + * Crear `src/registry-context.ts`: Portado de `RegistryContext.ts`. + * Actualizar `src/index.ts`: Exportar las nuevas primitivas. + * **Adaptación de Imports**: + * Reemplazar `effect/unstable/reactivity/*` por `@effect-atom/atom/*`. + * Asegurar compatibilidad con `effect` ^3.19.16. + +2. **Pruebas Unitarias**: + * Portar `test/index.test.tsx` desde `effect-smol`. + * Adaptar los imports para testear la implementación local (`../src/index.js`). + * Verificar que todos los tests pasen con `vitest`. + +3. **Actualización de Dependencias**: + * Actualizar `package.json` a la versión `0.3.0`. + * Verificar peerDependencies: `@effect-atom/atom` y `effect`. + +4. **Ejemplo en `apps/solid-example`**: + * Crear una nueva ruta de demostración: `src/routes/atom-demo.tsx`. + * Implementar ejemplos de uso de `useAtom`, `useAtomValue`, `useAtomInitialValues` y `RegistryProvider`. + +5. **Migración de Documentación**: + * Mover y reescribir la documentación en la aplicación de documentación (`apps/docs`). + * Crear/Actualizar: `apps/docs/src/content/docs/solid/effect-atom.mdx` (o estructura similar). + * Documentar la nueva API v0.3.0 y los breaking changes. + +### Confirmación +El resultado será un paquete más ligero, performante y alineado con el futuro estándar de Effect, totalmente documentado y testeado. ¿Procedemos? \ No newline at end of file diff --git a/README.md b/README.md index 9008a77a..73fb8006 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Effectify +[Documentation](https://devx-op.github.io/effectify/) + Monorepo of utilities for integrating [Effect](https://effect.website/) with different frameworks and libraries. ## Packages diff --git a/apps/docs/astro.config.ts b/apps/docs/astro.config.ts index 57f5ae13..efc45298 100644 --- a/apps/docs/astro.config.ts +++ b/apps/docs/astro.config.ts @@ -48,7 +48,7 @@ export default defineConfig({ }, { label: "Packages", - items: ["solid/packages/solid-query", "solid/packages/solid-ui", "solid/packages/chat-solid"], + items: ["solid/packages/solid-effect-atom", "solid/packages/solid-query", "solid/packages/solid-ui"], }, { label: "Reference", diff --git a/apps/docs/src/content/docs/es/solid/packages/chat-solid.md b/apps/docs/src/content/docs/es/solid/packages/chat-solid.md deleted file mode 100644 index d3125542..00000000 --- a/apps/docs/src/content/docs/es/solid/packages/chat-solid.md +++ /dev/null @@ -1,177 +0,0 @@ ---- -title: "@effectify/chat-solid" -description: Componentes y servicios de chat en tiempo real para aplicaciones SolidJS ---- - -# @effectify/chat-solid - -El paquete `@effectify/chat-solid` ofrece componentes y servicios de chat en tiempo real para aplicaciones SolidJS. Construido con Effect para un manejo de estado robusto y SolidJS para actualizaciones reactivas de UI, incluye soporte WebSocket, persistencia de mensajes y gestión de usuarios. - -## Instalación - -```bash -npm install @effectify/chat-solid -``` - -**Peer Dependencies:** - -```bash -npm install @effectify/solid-query @effectify/chat-domain solid-js -``` - -## Uso básico - -### Componente de chat simple - -```tsx -import { ChatRoom } from "@effectify/chat-solid/components/chat-room" -import { ChatProvider } from "@effectify/chat-solid/components/chat-provider" - -function App() { - return ( - - - - ) -} -``` - -### Interfaz de chat personalizada - -```tsx -import { ChatInput, ChatMessages, ChatUserList } from "@effectify/chat-solid/components" -import { useChatRoom } from "@effectify/chat-solid/hooks/use-chat-room" - -function CustomChatRoom() { - const { messages, users, sendMessage, isConnected } = useChatRoom() - - return ( -
-
-
- -
- -
-
- -
-
- ) -} -``` - -## Componentes - -### ChatProvider - -Proveedor raíz que gestiona estado y conexión WebSocket: - -```tsx -import { ChatProvider } from "@effectify/chat-solid/components/chat-provider" - -function App() { - return ( - - - - ) -} -``` - -### ChatRoom - -Componente completo de sala de chat: - -```tsx -import { ChatRoom } from "@effectify/chat-solid/components/chat-room" - -function MyChatApp() { - return ( -
- -
- ) -} -``` - -### ChatMessages - -Lista de mensajes: - -```tsx -import { ChatMessages } from "@effectify/chat-solid/components/chat-messages" -import { useChatRoom } from "@effectify/chat-solid/hooks/use-chat-room" - -function MessageArea() { - const { messages } = useChatRoom() - return ( - ( -
- {message.user.name}: {message.content} -
- )} - /> - ) -} -``` - -### ChatInput - -Entrada para enviar mensajes: - -```tsx -import { ChatInput } from "@effectify/chat-solid/components/chat-input" -import { useChatRoom } from "@effectify/chat-solid/hooks/use-chat-room" - -function MessageInput() { - const { sendMessage, isConnected } = useChatRoom() - return ( - { - // Indicador de escritura - }} - /> - ) -} -``` - -### ChatUserList - -Lista de usuarios: - -```tsx -import { ChatUserList } from "@effectify/chat-solid/components/chat-user-list" -import { useChatRoom } from "@effectify/chat-solid/hooks/use-chat-room" - -function UserSidebar() { - const { users } = useChatRoom() - return ( -
-

Usuarios en línea

- ( -
-
- {user.name} -
- )} - /> -
- ) -} -``` diff --git a/apps/docs/src/content/docs/es/solid/packages/solid-effect-atom.md b/apps/docs/src/content/docs/es/solid/packages/solid-effect-atom.md new file mode 100644 index 00000000..4d1efade --- /dev/null +++ b/apps/docs/src/content/docs/es/solid/packages/solid-effect-atom.md @@ -0,0 +1,188 @@ +--- +title: "@effectify/solid-effect-atom" +description: Herramientas reactivas para Effect con SolidJS +sidebar: + label: "@effectify/solid-effect-atom" + order: 1 +--- + +Bindings de SolidJS para la primitiva `Atom` de Effect. Esta librería permite utilizar el estado reactivo de Effect (`Atom`) dentro de componentes SolidJS de manera eficiente y segura. + +## Instalación + +```bash +npm install @effectify/solid-effect-atom @effect-atom/atom effect solid-js +``` + +## Configuración + +Para usar los átomos, debes envolver tu aplicación (o la parte que los use) con `RegistryProvider`. Esto provee el contexto necesario para el registro de átomos. + +```tsx +import { RegistryProvider } from "@effectify/solid-effect-atom" + +function App() { + return ( + + + + ) +} +``` + +## Uso Básico + +### Crear un Átomo + +Utiliza `Atom.make` del paquete `@effect-atom/atom`. + +```ts +import * as Atom from "@effect-atom/atom/Atom" + +const counterAtom = Atom.make(0) +``` + +### useAtom + +Hook para leer y escribir un átomo. Similar a `createSignal` de Solid. + +```tsx +import { useAtom } from "@effectify/solid-effect-atom" + +function Counter() { + const [count, setCount] = useAtom(counterAtom) + + return +} +``` + +### useAtomValue + +Hook para solo leer el valor de un átomo. Puedes pasar una función selectora para transformar el valor (computado). + +```tsx +import { useAtomValue } from "@effectify/solid-effect-atom" + +function Display() { + const count = useAtomValue(counterAtom) + const doubled = useAtomValue(counterAtom, (n) => n * 2) + + return ( +
+

Count: {count()}

+

Doubled: {doubled()}

+
+ ) +} +``` + +## Uso Avanzado + +### useAtomSet + +Útil cuando solo necesitas actualizar el átomo sin suscribirte a sus cambios. + +```tsx +import { useAtomSet } from "@effectify/solid-effect-atom" + +function ResetButton() { + const setCount = useAtomSet(counterAtom) + return +} +``` + +### useAtomSubscribe + +Se suscribe a los cambios del átomo manualmente. Útil para efectos secundarios (logging, analytics, etc.). + +```tsx +import { useAtomSubscribe } from "@effectify/solid-effect-atom" + +function Logger() { + useAtomSubscribe(counterAtom, (val) => { + console.log("Counter changed:", val) + }) + return null +} +``` + +### useAtomMount + +Monta manualmente un átomo. Útil si quieres mantener un átomo vivo en el registro sin leer su valor. + +```tsx +import { useAtomMount } from "@effectify/solid-effect-atom" + +function Keeper() { + useAtomMount(counterAtom) + return null +} +``` + +### useAtomInitialValues + +Útil para SSR o inicializar estado desde props. + +```tsx +import { useAtomInitialValues } from "@effectify/solid-effect-atom" + +function Initializer() { + useAtomInitialValues([[counterAtom, 100]]) + return null +} +``` + +### useAtomRefresh + +Fuerza la reevaluación o reinicio de un átomo. + +```tsx +import { useAtomRefresh } from "@effectify/solid-effect-atom" + +function Refresher() { + const refresh = useAtomRefresh(counterAtom) + return +} +``` + +### useAtomRef + +Para trabajar con referencias mutables (`AtomRef`). + +```tsx +import * as AtomRef from "@effect-atom/atom/AtomRef" +import { useAtomRef } from "@effectify/solid-effect-atom" + +const configRef = AtomRef.make({ theme: "dark" }) + +function Config() { + const config = useAtomRef(configRef) + + return ( + + ) +} +``` + +## Referencia de API + +### Hooks + +- **`useAtom(atom)`**: Retorna `[accessor, setter]`. +- **`useAtomValue(atom, selector?)`**: Retorna `accessor`. +- **`useAtomSet(atom)`**: Retorna solo el `setter`. +- **`useAtomSubscribe(atom, callback)`**: Se suscribe a cambios. +- **`useAtomMount(atom)`**: Monta el átomo en el registro. +- **`useAtomInitialValues(values)`**: Inicializa átomos en el registro actual. +- **`useAtomRefresh(atom)`**: Retorna una función para refrescar el átomo. +- **`useAtomRef(ref)`**: Se suscribe a un `AtomRef`. + +### Componentes + +- **`RegistryProvider`**: Proveedor de contexto para el registro de átomos. + +--- + +> **Nota**: Esta librería está diseñada para funcionar con Effect v3 y `@effect-atom/atom`. diff --git a/apps/docs/src/content/docs/es/solid/reference/solid-effect-atom.md b/apps/docs/src/content/docs/es/solid/reference/solid-effect-atom.md new file mode 100644 index 00000000..251e9bec --- /dev/null +++ b/apps/docs/src/content/docs/es/solid/reference/solid-effect-atom.md @@ -0,0 +1,119 @@ +--- +title: "@effectify/solid-effect-atom" +description: Referencia de API para @effectify/solid-effect-atom +sidebar: + label: Solid Effect Atom Reference +--- + +## Hooks + +### useAtom + +Se suscribe a un átomo y retorna una tupla `[accessor, setter]`. + +```ts +function useAtom( + atom: Atom.Writable, +): [Accessor, (value: W) => void] +``` + +### useAtomValue + +Se suscribe a un átomo y retorna su valor como un accessor. + +```ts +function useAtomValue(atom: Atom.Atom): Accessor +function useAtomValue(atom: Atom.Atom, f: (a: A) => B): Accessor +``` + +### useAtomSet + +Retorna una función setter para el átomo sin suscribirse a su valor. + +```ts +function useAtomSet(atom: Atom.Writable): (value: W) => void +``` + +### useAtomSubscribe + +Se suscribe a cambios en el valor de un átomo. + +```ts +function useAtomSubscribe( + atom: Atom.Atom, + f: (value: A) => void, + options?: { immediate?: boolean }, +): void +``` + +### useAtomMount + +Monta un átomo en el registro sin suscribirse a su valor. Es útil para mantener un átomo vivo. + +```ts +function useAtomMount(atom: Atom.Atom): void +``` + +### useAtomRefresh + +Retorna una función que fuerza al átomo a refrescar su valor. + +```ts +function useAtomRefresh(atom: Atom.Atom): () => void +``` + +### useAtomInitialValues + +Establece valores iniciales para átomos en el registro actual. Útil para SSR o inicialización. + +```ts +function useAtomInitialValues( + initialValues: Iterable<[Atom.Atom, any]>, +): void +``` + +### useAtomRef + +Se suscribe a un `AtomRef` y retorna su valor como un accessor. + +```ts +function useAtomRef(ref: AtomRef.ReadonlyRef): Accessor +``` + +### useAtomRefProp + +Crea un `AtomRef` derivado para una propiedad específica de un objeto almacenado en un `AtomRef`. + +```ts +function useAtomRefProp( + ref: AtomRef.AtomRef, + prop: K, +): AtomRef.AtomRef +``` + +### useAtomRefPropValue + +Se suscribe a una propiedad específica de un objeto almacenado en un `AtomRef`. + +```ts +function useAtomRefPropValue( + ref: AtomRef.AtomRef, + prop: K, +): Accessor +``` + +## Contexto + +### RegistryProvider + +Provee el contexto `AtomRegistry` al árbol de componentes. Esto es requerido para que funcionen los hooks de átomos. + +```tsx +function RegistryProvider(props: { + children?: JSX.Element + initialValues?: Iterable<[Atom.Atom, any]> + scheduleTask?: (f: () => void) => () => void + timeoutResolution?: number + defaultIdleTTL?: number +}): JSX.Element +``` diff --git a/apps/docs/src/content/docs/solid/packages/chat-solid.md b/apps/docs/src/content/docs/solid/packages/chat-solid.md deleted file mode 100644 index 512f3c46..00000000 --- a/apps/docs/src/content/docs/solid/packages/chat-solid.md +++ /dev/null @@ -1,654 +0,0 @@ ---- -title: "@effectify/chat-solid" -description: Real-time chat components and services for SolidJS applications ---- - -# @effectify/chat-solid - -The `@effectify/chat-solid` package provides real-time chat components and services for SolidJS applications. Built with Effect for robust state management and SolidJS for reactive UI updates, it offers a complete chat solution with WebSocket support, message persistence, and user management. - -## Installation - -```bash -npm install @effectify/chat-solid -``` - -**Peer Dependencies:** - -```bash -npm install @effectify/solid-query @effectify/chat-domain solid-js -``` - -## Basic Usage - -### Simple Chat Component - -```tsx -import { ChatRoom } from "@effectify/chat-solid/components/chat-room" -import { ChatProvider } from "@effectify/chat-solid/components/chat-provider" - -function App() { - return ( - - - - ) -} -``` - -### Custom Chat Interface - -```tsx -import { ChatInput, ChatMessages, ChatUserList } from "@effectify/chat-solid/components" -import { useChatRoom } from "@effectify/chat-solid/hooks/use-chat-room" - -function CustomChatRoom() { - const { - messages, - users, - sendMessage, - isConnected, - } = useChatRoom() - - return ( -
-
-
- -
- -
-
- -
-
- ) -} -``` - -## Components - -### ChatProvider - -The root provider that manages chat state and WebSocket connections: - -```tsx -import { ChatProvider } from "@effectify/chat-solid/components/chat-provider" - -function App() { - return ( - - - - ) -} -``` - -### ChatRoom - -A complete chat room component with messages, input, and user list: - -```tsx -import { ChatRoom } from "@effectify/chat-solid/components/chat-room" - -function MyChatApp() { - return ( -
- -
- ) -} -``` - -### ChatMessages - -Displays a list of chat messages with proper formatting: - -```tsx -import { ChatMessages } from "@effectify/chat-solid/components/chat-messages" -import { useChatRoom } from "@effectify/chat-solid/hooks/use-chat-room" - -function MessageArea() { - const { messages } = useChatRoom() - - return ( - ( -
- {message.user.name}: {message.content} -
- )} - /> - ) -} -``` - -### ChatInput - -Input component for sending messages: - -```tsx -import { ChatInput } from "@effectify/chat-solid/components/chat-input" -import { useChatRoom } from "@effectify/chat-solid/hooks/use-chat-room" - -function MessageInput() { - const { sendMessage, isConnected } = useChatRoom() - - return ( - { - // Handle typing indicator - }} - /> - ) -} -``` - -### ChatUserList - -Displays online users in the chat room: - -```tsx -import { ChatUserList } from "@effectify/chat-solid/components/chat-user-list" -import { useChatRoom } from "@effectify/chat-solid/hooks/use-chat-room" - -function UserSidebar() { - const { users } = useChatRoom() - - return ( -
-

Online Users

- ( -
-
- {user.name} -
- )} - /> -
- ) -} -``` - -## Hooks - -### useChatRoom - -Main hook for accessing chat room state and actions: - -```tsx -import { useChatRoom } from "@effectify/chat-solid/hooks/use-chat-room" - -function ChatComponent() { - const { - // State - messages, - users, - currentUser, - isConnected, - isLoading, - error, - - // Actions - sendMessage, - joinRoom, - leaveRoom, - - // Typing indicators - typingUsers, - startTyping, - stopTyping, - } = useChatRoom() - - const handleSendMessage = (content: string) => { - sendMessage({ content, type: "text" }) - } - - return ( - Connecting to chat...
}> - -
Error: {error()?.message}
-
-
Connected: {isConnected() ? "Yes" : "No"}
-
Messages: {messages().length}
-
Users: {users().length}
- - ) -} -``` - -### useChatMessages - -Hook for managing message-specific functionality: - -```tsx -import { useChatMessages } from "@effectify/chat-solid/hooks/use-chat-messages" - -function MessageManager() { - const { - messages, - sendMessage, - editMessage, - deleteMessage, - reactToMessage, - loadMoreMessages, - } = useChatMessages() - - const handleEdit = (messageId: string, newContent: string) => { - editMessage(messageId, { content: newContent }) - } - - const handleReact = (messageId: string, emoji: string) => { - reactToMessage(messageId, emoji) - } - - return ( -
- - {(message) => ( -
- {message.content} - -
- )} -
-
- ) -} -``` - -## Reactive Patterns - -### Real-time Message Updates - -```tsx -import { createEffect } from "solid-js" - -function ChatNotifications() { - const { messages } = useChatRoom() - - createEffect(() => { - const latestMessage = messages()[messages().length - 1] - if (latestMessage && latestMessage.userId !== currentUser()?.id) { - // Show notification for new message - new Notification(`New message from ${latestMessage.user.name}`) - } - }) - - return null -} -``` - -### Typing Indicators - -```tsx -import { createEffect, createSignal } from "solid-js" - -function TypingIndicator() { - const { typingUsers, startTyping, stopTyping } = useChatRoom() - const [isTyping, setIsTyping] = createSignal(false) - - let typingTimeout: number - - const handleInputChange = (value: string) => { - if (value.length > 0 && !isTyping()) { - setIsTyping(true) - startTyping() - } - - clearTimeout(typingTimeout) - typingTimeout = setTimeout(() => { - setIsTyping(false) - stopTyping() - }, 3000) - } - - return ( -
- handleInputChange(e.currentTarget.value)} - placeholder="Type a message..." - /> - 0}> -
- {typingUsers().map((u) => u.name).join(", ")} - {typingUsers().length === 1 ? " is" : " are"} typing... -
-
-
- ) -} -``` - -### Message Reactions - -```tsx -import { MessageReactions } from "@effectify/chat-solid/components/message-reactions" - -function MessageWithReactions(props: { message: Message }) { - const { reactToMessage } = useChatMessages() - - return ( -
-
{props.message.content}
- reactToMessage(props.message.id, emoji)} - /> -
- ) -} -``` - -## Services - -### ChatService - -Effect-based service for chat operations: - -```tsx -import { ChatService } from "@effectify/chat-solid/services/chat-service" -import { Effect } from "effect" - -// Send a message -const sendMessageEffect = ChatService.sendMessage({ - roomId: "room-123", - content: "Hello, world!", - type: "text", -}) - -// Join a room -const joinRoomEffect = ChatService.joinRoom("room-123") - -// Get message history -const getHistoryEffect = ChatService.getMessageHistory({ - roomId: "room-123", - limit: 50, - before: new Date(), -}) - -// Usage in component -function ChatComponent() { - const sendMessage = (content: string) => { - Effect.runPromise( - sendMessageEffect.pipe( - Effect.catchAll((error) => { - console.error("Failed to send message:", error) - return Effect.succeed(null) - }), - ), - ) - } - - return
{/* component JSX */}
-} -``` - -## Advanced Features - -### File Uploads - -```tsx -import { FileUpload } from "@effectify/chat-solid/components/file-upload" - -function ChatWithFiles() { - const { sendMessage } = useChatRoom() - - const handleFileUpload = (file: File) => { - sendMessage({ - type: "file", - content: file.name, - fileData: file, - }) - } - - return ( -
- -
- ) -} -``` - -### Message Search - -```tsx -import { createMemo, createSignal } from "solid-js" - -function MessageSearch() { - const { messages } = useChatRoom() - const [searchTerm, setSearchTerm] = createSignal("") - - const filteredMessages = createMemo(() => { - const term = searchTerm().toLowerCase() - if (!term) return messages() - - return messages().filter((message) => - message.content.toLowerCase().includes(term) || - message.user.name.toLowerCase().includes(term) - ) - }) - - return ( -
- setSearchTerm(e.currentTarget.value)} - placeholder="Search messages..." - /> - - {(message) => ( -
- {message.user.name}: {message.content} -
- )} -
-
- ) -} -``` - -## Configuration - -### WebSocket Configuration - -```tsx -const chatConfig = { - websocketUrl: 'ws://localhost:3001', - reconnectAttempts: 5, - reconnectDelay: 1000, - heartbeatInterval: 30000, - messageQueueSize: 100, - typingTimeout: 3000 -} - - - - -``` - -### Message Persistence - -```tsx -import { MessageStore } from '@effectify/chat-solid/services/message-store' - -// Configure local storage -const messageStore = MessageStore.localStorage({ - maxMessages: 1000, - ttl: 7 * 24 * 60 * 60 * 1000 // 7 days -}) - - - - -``` - -## Error Handling - -```tsx -import { ChatErrorBoundary } from "@effectify/chat-solid/components/chat-error-boundary" - -function App() { - return ( - ( -
-

Chat Error

-

{error.message}

- -
- )} - > - - - -
- ) -} -``` - -## Examples - -Check out the complete chat implementation in: - -- [SolidJS SPA Chat Example](https://github.com/devx-op/effectify/tree/main/apps/solid-app-spa) -- [SolidJS Start Chat Example](https://github.com/devx-op/effectify/tree/main/apps/solid-app-start) - -## Performance Optimization - -### Virtual Scrolling for Large Message Lists - -```tsx -import { createVirtualizer } from "@tanstack/solid-virtual" - -function VirtualizedMessages() { - const { messages } = useChatRoom() - let parentRef: HTMLDivElement - - const virtualizer = createVirtualizer({ - count: messages().length, - getScrollElement: () => parentRef, - estimateSize: () => 50, - }) - - return ( -
-
- - {(virtualItem) => ( -
- -
- )} -
-
-
- ) -} -``` - -## Best Practices - -### 1. Handle Connection States - -```tsx -function ChatStatus() { - const { isConnected, error } = useChatRoom() - - return ( -
- - Connected - - - Connecting... - - - Connection Error - -
- ) -} -``` - -### 2. Implement Message Queuing - -```tsx -function ReliableMessageSender() { - const { sendMessage, isConnected } = useChatRoom() - const [messageQueue, setMessageQueue] = createSignal([]) - - const queueMessage = (content: string) => { - if (isConnected()) { - sendMessage({ content, type: "text" }) - } else { - setMessageQueue((prev) => [...prev, content]) - } - } - - createEffect(() => { - if (isConnected() && messageQueue().length > 0) { - messageQueue().forEach((content) => { - sendMessage({ content, type: "text" }) - }) - setMessageQueue([]) - } - }) - - return { queueMessage } -} -``` diff --git a/apps/docs/src/content/docs/solid/packages/solid-effect-atom.md b/apps/docs/src/content/docs/solid/packages/solid-effect-atom.md new file mode 100644 index 00000000..7b685a6c --- /dev/null +++ b/apps/docs/src/content/docs/solid/packages/solid-effect-atom.md @@ -0,0 +1,188 @@ +--- +title: "@effectify/solid-effect-atom" +description: Reactive toolkit for Effect with SolidJS +sidebar: + label: "@effectify/solid-effect-atom" + order: 1 +--- + +SolidJS bindings for Effect's `Atom` primitive. This library allows you to use Effect's reactive state (`Atom`) within SolidJS components efficiently and safely. + +## Installation + +```bash +npm install @effectify/solid-effect-atom @effect-atom/atom effect solid-js +``` + +## Configuration + +To use atoms, you must wrap your application (or the part using them) with `RegistryProvider`. This provides the necessary context for atom registration. + +```tsx +import { RegistryProvider } from "@effectify/solid-effect-atom" + +function App() { + return ( + + + + ) +} +``` + +## Basic Usage + +### Create an Atom + +Use `Atom.make` from the `@effect-atom/atom` package. + +```ts +import * as Atom from "@effect-atom/atom/Atom" + +const counterAtom = Atom.make(0) +``` + +### useAtom + +Hook to read and write an atom. Similar to Solid's `createSignal`. + +```tsx +import { useAtom } from "@effectify/solid-effect-atom" + +function Counter() { + const [count, setCount] = useAtom(counterAtom) + + return +} +``` + +### useAtomValue + +Hook to only read an atom's value. You can pass a selector function to transform the value (computed). + +```tsx +import { useAtomValue } from "@effectify/solid-effect-atom" + +function Display() { + const count = useAtomValue(counterAtom) + const doubled = useAtomValue(counterAtom, (n) => n * 2) + + return ( +
+

Count: {count()}

+

Doubled: {doubled()}

+
+ ) +} +``` + +## Advanced Usage + +### useAtomSet + +Useful when you only need to update the atom without subscribing to its changes. + +```tsx +import { useAtomSet } from "@effectify/solid-effect-atom" + +function ResetButton() { + const setCount = useAtomSet(counterAtom) + return +} +``` + +### useAtomSubscribe + +Subscribes to atom changes manually. Useful for side effects (logging, analytics, etc.). + +```tsx +import { useAtomSubscribe } from "@effectify/solid-effect-atom" + +function Logger() { + useAtomSubscribe(counterAtom, (val) => { + console.log("Counter changed:", val) + }) + return null +} +``` + +### useAtomMount + +Manually mounts an atom. Useful if you want to keep an atom alive in the registry without rendering its value. + +```tsx +import { useAtomMount } from "@effectify/solid-effect-atom" + +function Keeper() { + useAtomMount(counterAtom) + return null +} +``` + +### useAtomInitialValues + +Useful for SSR or initializing state from props. + +```tsx +import { useAtomInitialValues } from "@effectify/solid-effect-atom" + +function Initializer() { + useAtomInitialValues([[counterAtom, 100]]) + return null +} +``` + +### useAtomRefresh + +Forces an atom to re-evaluate or reset. + +```tsx +import { useAtomRefresh } from "@effectify/solid-effect-atom" + +function Refresher() { + const refresh = useAtomRefresh(counterAtom) + return +} +``` + +### useAtomRef + +For working with mutable references (`AtomRef`). + +```tsx +import * as AtomRef from "@effect-atom/atom/AtomRef" +import { useAtomRef } from "@effectify/solid-effect-atom" + +const configRef = AtomRef.make({ theme: "dark" }) + +function Config() { + const config = useAtomRef(configRef) + + return ( + + ) +} +``` + +## API Reference + +### Hooks + +- **`useAtom(atom)`**: Returns `[accessor, setter]`. +- **`useAtomValue(atom, selector?)`**: Returns `accessor`. +- **`useAtomSet(atom)`**: Returns only the `setter`. +- **`useAtomSubscribe(atom, callback)`**: Subscribes to changes. +- **`useAtomMount(atom)`**: Mounts the atom in the registry. +- **`useAtomInitialValues(values)`**: Initializes atoms in the current registry. +- **`useAtomRefresh(atom)`**: Returns a function to refresh the atom. +- **`useAtomRef(ref)`**: Subscribes to an `AtomRef`. + +### Components + +- **`RegistryProvider`**: Context provider for atom registry. + +--- + +> **Note**: This library is designed to work with Effect v3 and `@effect-atom/atom`. diff --git a/apps/docs/src/content/docs/solid/reference/solid-effect-atom.md b/apps/docs/src/content/docs/solid/reference/solid-effect-atom.md new file mode 100644 index 00000000..28aef143 --- /dev/null +++ b/apps/docs/src/content/docs/solid/reference/solid-effect-atom.md @@ -0,0 +1,119 @@ +--- +title: "@effectify/solid-effect-atom" +description: API Reference for @effectify/solid-effect-atom +sidebar: + label: Solid Effect Atom Reference +--- + +## Hooks + +### useAtom + +Subscribes to an atom and returns a tuple of `[accessor, setter]`. + +```ts +function useAtom( + atom: Atom.Writable, +): [Accessor, (value: W) => void] +``` + +### useAtomValue + +Subscribes to an atom and returns its value as an accessor. + +```ts +function useAtomValue
(atom: Atom.Atom): Accessor +function useAtomValue(atom: Atom.Atom, f: (a: A) => B): Accessor +``` + +### useAtomSet + +Returns a setter function for the atom without subscribing to its value. + +```ts +function useAtomSet(atom: Atom.Writable): (value: W) => void +``` + +### useAtomSubscribe + +Subscribes to changes in an atom's value. + +```ts +function useAtomSubscribe( + atom: Atom.Atom, + f: (value: A) => void, + options?: { immediate?: boolean }, +): void +``` + +### useAtomMount + +Mounts an atom in the registry without subscribing to its value. This is useful for keeping an atom alive. + +```ts +function useAtomMount(atom: Atom.Atom): void +``` + +### useAtomRefresh + +Returns a function that forces an atom to refresh its value. + +```ts +function useAtomRefresh(atom: Atom.Atom): () => void +``` + +### useAtomInitialValues + +Sets initial values for atoms in the current registry. Useful for SSR or initialization. + +```ts +function useAtomInitialValues( + initialValues: Iterable<[Atom.Atom, any]>, +): void +``` + +### useAtomRef + +Subscribes to an `AtomRef` and returns its value as an accessor. + +```ts +function useAtomRef(ref: AtomRef.ReadonlyRef): Accessor +``` + +### useAtomRefProp + +Creates a derived `AtomRef` for a specific property of an object stored in an `AtomRef`. + +```ts +function useAtomRefProp( + ref: AtomRef.AtomRef, + prop: K, +): AtomRef.AtomRef +``` + +### useAtomRefPropValue + +Subscribes to a specific property of an object stored in an `AtomRef`. + +```ts +function useAtomRefPropValue( + ref: AtomRef.AtomRef, + prop: K, +): Accessor +``` + +## Context + +### RegistryProvider + +Provides the `AtomRegistry` context to the component tree. This is required for all atom hooks to work. + +```tsx +function RegistryProvider(props: { + children?: JSX.Element + initialValues?: Iterable<[Atom.Atom, any]> + scheduleTask?: (f: () => void) => () => void + timeoutResolution?: number + defaultIdleTTL?: number +}): JSX.Element +``` diff --git a/apps/react-router-example/tsconfig.app.json b/apps/react-router-example/tsconfig.app.json index 09df2357..70e2e0fe 100644 --- a/apps/react-router-example/tsconfig.app.json +++ b/apps/react-router-example/tsconfig.app.json @@ -8,7 +8,9 @@ "**/.server/**/*.ts", "**/.server/**/*.tsx", "**/.client/**/*.ts", - "**/.client/**/*.tsx" + "**/.client/**/*.tsx", + ".react-router/types/**/*", + "prisma/generated/**/*" ], "exclude": [ "tests/**/*.spec.ts", @@ -18,7 +20,16 @@ "tests/**/*.spec.js", "tests/**/*.test.js", "tests/**/*.spec.jsx", - "tests/**/*.test.jsx", - "prisma/generated/**/*" - ] + "tests/**/*.test.jsx" + ], + "compilerOptions": { + "composite": true, + "baseUrl": ".", + "outDir": "dist/out-tsc", + "paths": { + "@prisma/*": [ + "./prisma/generated/*" + ] + } + } } diff --git a/apps/react-router-example/tsconfig.json b/apps/react-router-example/tsconfig.json index 6bc7106d..6f50e164 100644 --- a/apps/react-router-example/tsconfig.json +++ b/apps/react-router-example/tsconfig.json @@ -16,7 +16,6 @@ "@prisma/*": ["./prisma/generated/*"] } }, - "files": [], "include": [], "references": [ { diff --git a/apps/solid-example/package.json b/apps/solid-example/package.json index ec67423f..926d35fb 100644 --- a/apps/solid-example/package.json +++ b/apps/solid-example/package.json @@ -2,6 +2,9 @@ "name": "@effectify/solid-example", "type": "module", "dependencies": { + "@effectify/solid-effect-atom": "workspace:*", + "@effect-atom/atom": "catalog:", + "effect": "catalog:", "@tailwindcss/vite": "catalog:", "@tanstack/router-plugin": "catalog:", "@tanstack/solid-router": "catalog:", diff --git a/apps/solid-example/src/components/Header.tsx b/apps/solid-example/src/components/Header.tsx index ad75fcb6..6e76823e 100644 --- a/apps/solid-example/src/components/Header.tsx +++ b/apps/solid-example/src/components/Header.tsx @@ -70,7 +70,21 @@ export default function Header() { Start - Server Functions - {/* Demo Links End */} + {/* Demo Atom Start */} + + setIsOpen(false)} + class="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2" + activeProps={{ + class: "flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2", + }} + > + + Start - @effecitfy/solid-effect-atom Atom Demo + + + {/* Demo Atom End */} diff --git a/apps/solid-example/src/routeTree.gen.ts b/apps/solid-example/src/routeTree.gen.ts index 3615b722..bc9133ef 100644 --- a/apps/solid-example/src/routeTree.gen.ts +++ b/apps/solid-example/src/routeTree.gen.ts @@ -9,9 +9,15 @@ // 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 AtomDemoRouteImport } from './routes/atom-demo' import { Route as IndexRouteImport } from './routes/index' import { Route as DemoStartServerFuncsRouteImport } from './routes/demo.start.server-funcs' +const AtomDemoRoute = AtomDemoRouteImport.update({ + id: '/atom-demo', + path: '/atom-demo', + getParentRoute: () => rootRouteImport, +} as any) const IndexRoute = IndexRouteImport.update({ id: '/', path: '/', @@ -25,32 +31,43 @@ const DemoStartServerFuncsRoute = DemoStartServerFuncsRouteImport.update({ export interface FileRoutesByFullPath { '/': typeof IndexRoute + '/atom-demo': typeof AtomDemoRoute '/demo/start/server-funcs': typeof DemoStartServerFuncsRoute } export interface FileRoutesByTo { '/': typeof IndexRoute + '/atom-demo': typeof AtomDemoRoute '/demo/start/server-funcs': typeof DemoStartServerFuncsRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute + '/atom-demo': typeof AtomDemoRoute '/demo/start/server-funcs': typeof DemoStartServerFuncsRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/demo/start/server-funcs' + fullPaths: '/' | '/atom-demo' | '/demo/start/server-funcs' fileRoutesByTo: FileRoutesByTo - to: '/' | '/demo/start/server-funcs' - id: '__root__' | '/' | '/demo/start/server-funcs' + to: '/' | '/atom-demo' | '/demo/start/server-funcs' + id: '__root__' | '/' | '/atom-demo' | '/demo/start/server-funcs' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute + AtomDemoRoute: typeof AtomDemoRoute DemoStartServerFuncsRoute: typeof DemoStartServerFuncsRoute } declare module '@tanstack/solid-router' { interface FileRoutesByPath { + '/atom-demo': { + id: '/atom-demo' + path: '/atom-demo' + fullPath: '/atom-demo' + preLoaderRoute: typeof AtomDemoRouteImport + parentRoute: typeof rootRouteImport + } '/': { id: '/' path: '/' @@ -70,6 +87,7 @@ declare module '@tanstack/solid-router' { const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, + AtomDemoRoute: AtomDemoRoute, DemoStartServerFuncsRoute: DemoStartServerFuncsRoute, } export const routeTree = rootRouteImport diff --git a/apps/solid-example/src/routes/atom-demo.tsx b/apps/solid-example/src/routes/atom-demo.tsx new file mode 100644 index 00000000..9b8bd9e6 --- /dev/null +++ b/apps/solid-example/src/routes/atom-demo.tsx @@ -0,0 +1,203 @@ +import { createFileRoute } from "@tanstack/solid-router" +import * as Atom from "@effect-atom/atom/Atom" +import * as AtomRef from "@effect-atom/atom/AtomRef" +import { + RegistryProvider, + useAtom, + useAtomInitialValues, + useAtomMount, + useAtomRef, + useAtomRefresh, + useAtomSet, + useAtomSubscribe, + useAtomValue, +} from "@effectify/solid-effect-atom" +import { createSignal } from "solid-js" + +export const Route = createFileRoute("/atom-demo")({ + component: AtomDemo, +}) + +const counterAtom = Atom.make(0) +const textAtom = Atom.make("Initial Text") +const refAtom = AtomRef.make({ count: 10, name: "AtomRef" }) +const setOnlyAtom = Atom.make(0) +const subscribeAtom = Atom.make(0) +const mountAtom = Atom.make(0) + +function Counter() { + const [count, setCount] = useAtom(counterAtom) + return ( +
+

Counter (useAtom)

+

{count()}

+
+ + +
+
+ ) +} + +function DoubledCounter() { + const doubled = useAtomValue(counterAtom, (n: number) => n * 2) + return ( +
+

Doubled (useAtomValue)

+

{doubled()}

+

Derived from counter value * 2

+
+ ) +} + +function InitialValuesDemo() { + useAtomInitialValues([[textAtom, "Overridden Initial Value"]]) + const [text] = useAtom(textAtom) + + return ( +
+

Initial Values

+

"{text()}"

+

Initialized via useAtomInitialValues

+
+ ) +} + +function RefreshDemo() { + const refresh = useAtomRefresh(counterAtom) + return ( +
+

Refresh Atom

+

Forces re-evaluation/reset of the atom

+ +
+ ) +} + +function AtomRefDemo() { + const state = useAtomRef(refAtom) + + return ( +
+

AtomRef

+
+

Count: {state().count}

+

Name: {state().name}

+
+ +
+ ) +} + +function SetOnlyDemo() { + const setCount = useAtomSet(setOnlyAtom) + const count = useAtomValue(setOnlyAtom) + + return ( +
+

Set Only (useAtomSet)

+

{count()}

+ +
+ ) +} + +function SubscribeDemo() { + const [logs, setLogs] = createSignal([]) + + useAtomSubscribe(subscribeAtom, (val: number) => { + setLogs((prev) => [`Value changed to: ${val}`, ...prev].slice(0, 3)) + }) + + const setCount = useAtomSet(subscribeAtom) + + return ( +
+

Subscribe (useAtomSubscribe)

+ +
+ {logs().map((log) =>
{log}
)} + {logs().length === 0 &&
No changes yet...
} +
+
+ ) +} + +function MountDemo() { + // This atom will log when mounted/unmounted if we added effects to it + // For this demo, we just show that useAtomMount can be called + useAtomMount(mountAtom) + + return ( +
+

Mount (useAtomMount)

+

Manually mounts an atom without reading its value.

+
+
+ Atom Mounted +
+
+ ) +} + +function AtomDemo() { + return ( + +
+
+

+ + Effect Atom + {" "} + Demo +

+ +

+ Demonstrating @effectify/solid-effect-atom v0.3.0 +

+ +
+ + + + + + + + +
+
+
+
+ ) +} diff --git a/packages/solid/effect-atom/README.md b/packages/solid/effect-atom/README.md index 67b5dfb1..cebaeffa 100644 --- a/packages/solid/effect-atom/README.md +++ b/packages/solid/effect-atom/README.md @@ -1,275 +1,184 @@ # @effectify/solid-effect-atom -**Reactive state management for SolidJS applications using Effect atoms** +[Documentation](https://devx-op.github.io/effectify/solid/packages/solid-effect-atom/) -`@effectify/solid-effect-atom` provides seamless integration between [Effect](https://effect.website) atoms and [SolidJS](https://solidjs.com) applications, leveraging SolidJS's fine-grained reactivity system for optimal performance. - -## Features - -- 🚀 **Fine-grained Reactivity**: Leverages SolidJS's reactive system for optimal performance -- ⚡ **Zero Re-renders**: Updates only the specific DOM nodes that need to change -- 🔄 **Async Support**: Built-in support for async atoms with loading states -- 🎯 **TypeScript**: Full TypeScript support with excellent type inference -- 🧪 **Well Tested**: Comprehensive test suite with 100% coverage -- 📦 **Lightweight**: Minimal bundle size impact +SolidJS bindings for Effect's `Atom` primitive. This library allows you to use Effect's reactive state (`Atom`) within SolidJS components efficiently and safely. ## Installation ```bash -npm install @effectify/solid-effect-atom -# or -pnpm add @effectify/solid-effect-atom -# or -yarn add @effectify/solid-effect-atom +npm install @effectify/solid-effect-atom @effect-atom/atom effect solid-js ``` -## Quick Start +## Configuration -```tsx -import { Atom } from "@effect-atom/atom" -import { RegistryProvider, useAtom, useAtomValue } from "@effectify/solid-effect-atom" +To use atoms, you must wrap your application (or the part using them) with `RegistryProvider`. This provides the necessary context for atom registration. -// Create an atom -const countAtom = Atom.make(0) - -function Counter() { - const [count, setCount] = useAtom(() => countAtom) - - return ( -
-

Count: {count()}

- -
- ) -} +```tsx +import { RegistryProvider } from "@effectify/solid-effect-atom" function App() { return ( - + ) } ``` -## Core Hooks +## Basic Usage -### useAtomValue +### Create an Atom -Read the current value of an atom and subscribe to changes. +Use `Atom.make` from the `@effect-atom/atom` package. -```tsx -const name = useAtomValue(() => nameAtom) -return

Hello, {name()}!

+```ts +import * as Atom from "@effect-atom/atom/Atom" + +const counterAtom = Atom.make(0) ``` ### useAtom -Get both the current value and a setter function for an atom. +Hook to read and write an atom. Similar to Solid's `createSignal`. ```tsx -const [count, setCount] = useAtom(() => countAtom) -``` +import { useAtom } from "@effectify/solid-effect-atom" -### useAtomSet - -Get only the setter function for an atom. +function Counter() { + const [count, setCount] = useAtom(counterAtom) -```tsx -const setCount = useAtomSet(() => countAtom) + return +} ``` -## Advanced Features - -### Async Atoms +### useAtomValue -Handle async operations with built-in loading states: +Hook to only read an atom's value. You can pass a selector function to transform the value (computed). ```tsx -import { Effect } from "effect" -import { useAtomSuspenseResult } from "@effectify/solid-effect-atom" - -const dataAtom = Atom.fn(() => - Effect.gen(function*() { - const response = yield* Effect.promise(() => fetch("/api/data")) - return yield* Effect.promise(() => response.json()) - }) -) +import { useAtomValue } from "@effectify/solid-effect-atom" -function AsyncData() { - const result = useAtomSuspenseResult(() => dataAtom) +function Display() { + const count = useAtomValue(counterAtom) + const doubled = useAtomValue(counterAtom, (n) => n * 2) return (
- {result.loading &&

Loading...

} - {result.error &&

Error: {result.error.message}

} - {result.data &&

Data: {JSON.stringify(result.data)}

} +

Count: {count()}

+

Doubled: {doubled()}

) } ``` -### Computed Atoms +## Advanced Usage + +### useAtomSet -Create derived state that automatically updates: +Useful when you only need to update the atom without subscribing to its changes. ```tsx -const firstNameAtom = Atom.make("John") -const lastNameAtom = Atom.make("Doe") -const fullNameAtom = Atom.make((get) => `${get(firstNameAtom)} ${get(lastNameAtom)}`) +import { useAtomSet } from "@effectify/solid-effect-atom" -function FullName() { - const fullName = useAtomValue(() => fullNameAtom) - return

{fullName()}

+function ResetButton() { + const setCount = useAtomSet(counterAtom) + return } ``` -## Performance Benefits - -SolidJS's fine-grained reactivity system means that atom-solid can update only the specific parts of your UI that depend on changed atoms, without re-rendering entire components. - -### Benchmark Results - -Based on our performance benchmarks: - -- **Atom Creation**: ~2M ops/sec -- **Registry Get**: ~6M ops/sec -- **Registry Set**: ~4M ops/sec -- **Computed Atoms**: ~454K ops/sec -- **Subscriptions**: ~1M ops/sec -- **Memory Usage**: ~5KB per 1000 atoms +### useAtomSubscribe -Run benchmarks yourself: `pnpm benchmark` +Subscribes to atom changes manually. Useful for side effects (logging, analytics, etc.). -### Comparison with React - -| Feature | atom-react | atom-solid | -| -------------- | ---------------------- | -------------------- | -| Re-renders | Component re-renders | Fine-grained updates | -| Performance | Good | Excellent | -| Bundle size | ~15kb | ~12kb | -| Learning curve | Familiar to React devs | SolidJS concepts | +```tsx +import { useAtomSubscribe } from "@effectify/solid-effect-atom" -## Migration from atom-react +function Logger() { + useAtomSubscribe(counterAtom, (val) => { + console.log("Counter changed:", val) + }) + return null +} +``` -### Key Differences +### useAtomMount -#### 1. **Hook Signatures** +Manually mounts an atom. Useful if you want to keep an atom alive in the registry without rendering its value. ```tsx -// atom-react -const value = useAtomValue(myAtom) +import { useAtomMount } from "@effectify/solid-effect-atom" -// atom-solid -const value = useAtomValue(() => myAtom) +function Keeper() { + useAtomMount(counterAtom) + return null +} ``` -#### 2. **Return Values** +### useAtomInitialValues + +Useful for SSR or initializing state from props. ```tsx -// atom-react - Direct values -const count = useAtomValue(countAtom) -return
{count}
+import { useAtomInitialValues } from "@effectify/solid-effect-atom" -// atom-solid - Signal accessors -const count = useAtomValue(() => countAtom) -return
{count()}
+function Initializer() { + useAtomInitialValues([[counterAtom, 100]]) + return null +} ``` -#### 3. **Suspense Implementation** +### useAtomRefresh + +Forces an atom to re-evaluate or reset. ```tsx -// atom-react - Direct Promise throwing -const result = useAtomSuspense(asyncAtom) +import { useAtomRefresh } from "@effectify/solid-effect-atom" -// atom-solid - createResource integration -const result = useAtomSuspense(() => asyncAtom) +function Refresher() { + const refresh = useAtomRefresh(counterAtom) + return +} ``` -#### 4. **Performance Characteristics** +### useAtomRef -- **atom-react**: React reconciliation, component re-renders -- **atom-solid**: Fine-grained updates, no virtual DOM +For working with mutable references (`AtomRef`). -### Migration Steps - -1. **Update hook calls**: Add factory functions -2. **Update JSX**: Add `()` to access signal values -3. **Update Suspense**: Wrap with SolidJS Suspense components -4. **Test thoroughly**: Different reactivity model may expose edge cases +```tsx +import * as AtomRef from "@effect-atom/atom/AtomRef" +import { useAtomRef } from "@effectify/solid-effect-atom" -## Examples +const configRef = AtomRef.make({ theme: "dark" }) -Check out the [sample application](../../sample/solid) for complete examples including: +function Config() { + const config = useAtomRef(configRef) -- Counter with computed values -- Async data fetching -- Todo list management -- Shared state between components + return ( + + ) +} +``` ## API Reference -### Core Hooks +### Hooks -- `useAtomValue` - Read atom values -- `useAtom` - Read and write atom values -- `useAtomSet` - Write-only atom access +- **`useAtom(atom)`**: Returns `[accessor, setter]`. +- **`useAtomValue(atom, selector?)`**: Returns `accessor`. +- **`useAtomSet(atom)`**: Returns only the `setter`. +- **`useAtomSubscribe(atom, callback)`**: Subscribes to changes. +- **`useAtomMount(atom)`**: Mounts the atom in the registry. +- **`useAtomInitialValues(values)`**: Initializes atoms in the current registry. +- **`useAtomRefresh(atom)`**: Returns a function to refresh the atom. +- **`useAtomRef(ref)`**: Subscribes to an `AtomRef`. -### Advanced Hooks +### Components -- `useAtomSuspenseResult` - Handle async atoms with loading states -- `useAtomSubscribe` - Subscribe to atom changes for side effects -- `useAtomMount` - Ensure atoms are mounted and active +- **`RegistryProvider`**: Context provider for atom registry. -### Context +--- -- `RegistryProvider` - Provide atom registry to component tree -- `useRegistry` - Access the current registry - -## Best Practices - -1. **Use atom factories**: Always pass a function that returns the atom - ```tsx - // ✅ Good - const value = useAtomValue(() => myAtom) - - // ❌ Bad - const value = useAtomValue(myAtom) - ``` - -2. **Prefer computed atoms**: Use computed atoms for derived state - ```tsx - // ✅ Good - const doubledAtom = Atom.make((get) => get(countAtom) * 2) - - // ❌ Bad - const doubled = useAtomValue(() => countAtom, (count) => count * 2) - ``` - -3. **Use useAtomSet for write-only operations**: When you only need to update - ```tsx - // ✅ Good - const setCount = useAtomSet(() => countAtom) - - // ❌ Bad - const [, setCount] = useAtom(() => countAtom) - ``` - -## Documentation - -Full documentation: [https://github.com/devx-op/effectify/tree/master/packages/solid/effect-atom/docs](https://github.com/devx-op/effectify/tree/master/packages/solid/effect-atom/docs) - -## License - -MIT - -## Links - -- [Effect Website](https://effect.website) -- [SolidJS Website](https://solidjs.com) -- [GitHub Repository](https://github.com/devx-op/effectify) - -``` -``` +> **Note**: This library is designed to work with Effect v3 and `@effect-atom/atom`. diff --git a/packages/solid/effect-atom/docs/advanced-hooks.ts.md b/packages/solid/effect-atom/docs/advanced-hooks.ts.md deleted file mode 100644 index c44c5f19..00000000 --- a/packages/solid/effect-atom/docs/advanced-hooks.ts.md +++ /dev/null @@ -1,381 +0,0 @@ ---- -title: "AdvancedHooks.ts" -parent: "@effectify/solid-effect-atom" -nav_order: 4 ---- - -# Advanced Hooks - -Advanced hooks for complex use cases and async atom management. - -## useAtomSuspenseResult - -Handle async atoms with built-in loading, success, and error states without throwing. - -### Signature - -```typescript -export const useAtomSuspenseResult:
( - atomFactory: () => Atom.Atom>, -) => { - readonly loading: boolean - readonly data: A | undefined - readonly error: any | undefined -} -``` - -### Parameters - -- `atomFactory`: A function that returns an atom containing a `Result` - -### Returns - -An object with: - -- `loading`: `true` when the atom is in loading state -- `data`: The successful result data, or `undefined` if loading/error -- `error`: The error if the atom failed, or `undefined` if loading/success - -### Examples - -#### Basic Async Data Fetching - -```tsx -import { Atom } from "@effect-atom/atom" -import { Effect } from "effect" -import { useAtomSuspenseResult } from "@effectify/solid-effect-atom" - -const dataAtom = Atom.fn(() => - Effect.gen(function*() { - yield* Effect.sleep("1 second") - const response = yield* Effect.promise(() => fetch("/api/data")) - return yield* Effect.promise(() => response.json()) - }) -) - -function AsyncData() { - const result = useAtomSuspenseResult(() => dataAtom) - - return ( -
- {result.loading &&

Loading data...

} - {result.error && ( -

- Error: {result.error.message} -

- )} - {result.data && ( -
-

Data loaded successfully!

-
{JSON.stringify(result.data, null, 2)}
-
- )} -
- ) -} -``` - -#### With Refresh Functionality - -```tsx -const refreshableDataAtom = Atom.fn(() => - Effect.gen(function*() { - const timestamp = new Date().toLocaleTimeString() - yield* Effect.sleep("500 millis") - return `Data loaded at ${timestamp}` - }) -) - -function RefreshableData() { - const result = useAtomSuspenseResult(() => refreshableDataAtom) - const refreshData = useAtomSet(() => refreshableDataAtom) - - return ( -
- {result.loading &&

Loading...

} - {result.error &&

Error: {result.error.message}

} - {result.data && ( -
-

{result.data}

- -
- )} -
- ) -} -``` - -#### Error Handling - -```tsx -const riskyAtom = Atom.fn(() => - Effect.gen(function*() { - const shouldFail = Math.random() > 0.5 - if (shouldFail) { - yield* Effect.fail(new Error("Random failure")) - } - return "Success!" - }) -) - -function RiskyOperation() { - const result = useAtomSuspenseResult(() => riskyAtom) - const retry = useAtomSet(() => riskyAtom) - - return ( -
- {result.loading &&

Attempting operation...

} - {result.error && ( -
-

- Operation failed: {result.error.message} -

- -
- )} - {result.data && ( -

- {result.data} -

- )} -
- ) -} -``` - ---- - -## useAtomSubscribe - -Subscribe to atom changes for side effects without reading the atom value. - -### Signature - -```typescript -export const useAtomSubscribe:
( - atomFactory: () => Atom.Atom, - callback: (value: A) => void, -) => void -``` - -### Parameters - -- `atomFactory`: A function that returns the atom to subscribe to -- `callback`: Function called whenever the atom value changes - -### Examples - -#### Logging Changes - -```tsx -import { useAtomSubscribe } from "@effectify/solid-effect-atom" - -const countAtom = Atom.make(0) - -function Logger() { - useAtomSubscribe(() => countAtom, (count) => { - console.log(`Count changed to: ${count}`) - }) - - return null // This component doesn't render anything -} - -function App() { - return ( -
- - -
- ) -} -``` - -#### Local Storage Sync - -```tsx -const userPreferencesAtom = Atom.make({ - theme: "light", - language: "en", -}) - -function LocalStorageSync() { - useAtomSubscribe(() => userPreferencesAtom, (preferences) => { - localStorage.setItem("userPreferences", JSON.stringify(preferences)) - }) - - return null -} -``` - -#### Analytics Tracking - -```tsx -const pageViewAtom = Atom.make("") - -function AnalyticsTracker() { - useAtomSubscribe(() => pageViewAtom, (page) => { - if (page) { - // Track page view - analytics.track("page_view", { page }) - } - }) - - return null -} -``` - ---- - -## useAtomMount - -Ensure an atom is mounted and active in the registry. - -### Signature - -```typescript -export const useAtomMount:
(atomFactory: () => Atom.Atom) => void -``` - -### Parameters - -- `atomFactory`: A function that returns the atom to mount - -### Examples - -#### Preloading Data - -```tsx -const expensiveDataAtom = Atom.fn(() => - Effect.gen(function*() { - // Expensive computation or API call - yield* Effect.sleep("2 seconds") - return "Expensive data" - }) -) - -function DataPreloader() { - // Mount the atom to start loading immediately - useAtomMount(() => expensiveDataAtom) - - return null -} - -function App() { - return ( -
- - {/* Data will be loading in background */} - -
- ) -} -``` - -#### Keeping Atoms Alive - -```tsx -const importantAtom = Atom.make("important data") - -function AtomKeeper() { - // Keep this atom mounted even if no components are using it - useAtomMount(() => importantAtom) - - return null -} -``` - ---- - -## Performance and Best Practices - -### When to Use Advanced Hooks - -#### useAtomSuspenseResult - -- ✅ When you need to handle loading/error states manually -- ✅ When you want to show custom loading/error UI -- ✅ When you need to access error details -- ❌ When you want automatic suspense behavior (use regular hooks instead) - -#### useAtomSubscribe - -- ✅ For side effects (logging, analytics, storage sync) -- ✅ When you don't need the atom value in render -- ✅ For triggering other actions based on atom changes -- ❌ For rendering data (use `useAtomValue` instead) - -#### useAtomMount - -- ✅ For preloading expensive atoms -- ✅ For keeping important atoms alive -- ✅ For background data fetching -- ❌ For atoms that are already used by components - -### Memory Management - -Advanced hooks are automatically cleaned up when components unmount: - -```tsx -function MyComponent() { - // Subscription is automatically cleaned up on unmount - useAtomSubscribe(() => myAtom, callback) - - // Mount is automatically released on unmount - useAtomMount(() => expensiveAtom) - - return
...
-} -``` - -### Error Boundaries - -For better error handling with async atoms, consider using error boundaries: - -```tsx -import { ErrorBoundary } from "solid-js" - -function App() { - return ( -
Error: {err.message}
}> - -
- ) -} -``` - -### Testing Advanced Hooks - -```tsx -import { render } from "@solidjs/testing-library" -import { vi } from "vitest" - -test("useAtomSubscribe calls callback", () => { - const callback = vi.fn() - const testAtom = Atom.make("initial") - - function TestComponent() { - useAtomSubscribe(() => testAtom, callback) - return null - } - - const { unmount } = render(() => ( - - - - )) - - // Trigger atom change - registry.set(testAtom, "changed") - - expect(callback).toHaveBeenCalledWith("changed") - - unmount() -}) -``` diff --git a/packages/solid/effect-atom/docs/context.md b/packages/solid/effect-atom/docs/context.md deleted file mode 100644 index 9e508b82..00000000 --- a/packages/solid/effect-atom/docs/context.md +++ /dev/null @@ -1,276 +0,0 @@ ---- -title: "Context.ts" -parent: "@effectify/solid-effect-atom" -nav_order: 3 ---- - -# Context - -Registry provider and context management for atom-solid. - -## RegistryProvider - -Provides an atom registry to the component tree. All atom operations within the provider will use this registry. - -### Signature - -```typescript -export const RegistryProvider: (props: RegistryProviderProps) => JSX.Element -``` - -### Props - -```typescript -export interface RegistryProviderProps { - readonly children?: any - readonly registry?: Registry.Registry - readonly initialValues?: Iterable, any]> - readonly scheduleTask?: (f: () => void) => void - readonly timeoutResolution?: number - readonly defaultIdleTTL?: number -} -``` - -#### Parameters - -- `children`: Child components that will have access to the registry -- `registry` (optional): Custom registry instance. If not provided, a new registry will be created -- `initialValues` (optional): Initial values for atoms -- `scheduleTask` (optional): Custom task scheduler function -- `timeoutResolution` (optional): Timeout resolution in milliseconds (default: 200) -- `defaultIdleTTL` (optional): Default idle time-to-live in milliseconds (default: 400) - -### Examples - -#### Basic Usage - -```tsx -import { RegistryProvider } from "@effectify/solid-effect-atom" - -function App() { - return ( - - - - - ) -} -``` - -#### With Custom Registry - -```tsx -import { Registry } from "@effect-atom/atom" -import { RegistryProvider } from "@effectify/solid-effect-atom" - -const customRegistry = Registry.make({ - timeoutResolution: 1000, - defaultIdleTTL: 2000, -}) - -function App() { - return ( - - - - ) -} -``` - -#### With Initial Values - -```tsx -import { Atom } from "@effect-atom/atom" -import { RegistryProvider } from "@effectify/solid-effect-atom" - -const countAtom = Atom.make(0) -const nameAtom = Atom.make("") - -function App() { - const initialValues = [ - [countAtom, 10], - [nameAtom, "John Doe"], - ] as const - - return ( - - - - ) -} -``` - -#### With Custom Configuration - -```tsx -function App() { - const scheduleTask = (task: () => void) => { - // Custom scheduling logic - setTimeout(task, 0) - } - - return ( - - - - ) -} -``` - ---- - -## useRegistry - -Access the current registry from context. Useful for advanced use cases where you need direct registry access. - -### Signature - -```typescript -export const useRegistry: () => Registry.Registry -``` - -### Returns - -The current `Registry.Registry` instance from context, or the default registry if no provider is found. - -### Examples - -#### Direct Registry Access - -```tsx -import { useRegistry } from "@effectify/solid-effect-atom" -import { Atom } from "@effect-atom/atom" - -function AdvancedComponent() { - const registry = useRegistry() - const myAtom = Atom.make("initial") - - const handleDirectUpdate = () => { - // Direct registry manipulation (advanced use case) - registry.set(myAtom, "updated directly") - } - - return ( - - ) -} -``` - -#### Registry Information - -```tsx -function RegistryInfo() { - const registry = useRegistry() - - return ( -
-

Registry timeout resolution: {registry.timeoutResolution}ms

-

Default idle TTL: {registry.defaultIdleTTL}ms

-
- ) -} -``` - ---- - -## Registry Concepts - -### What is a Registry? - -A registry is the central store that manages atom state and subscriptions. It: - -- **Stores atom values**: Keeps track of the current value of each atom -- **Manages subscriptions**: Handles which components are listening to which atoms -- **Coordinates updates**: Ensures that all subscribers are notified when atoms change -- **Handles cleanup**: Automatically cleans up unused atoms and subscriptions - -### Registry Lifecycle - -1. **Creation**: Registry is created when `RegistryProvider` mounts -2. **Atom mounting**: Atoms are mounted when first accessed by hooks -3. **Subscription management**: Components subscribe/unsubscribe as they mount/unmount -4. **Cleanup**: Unused atoms are cleaned up based on TTL settings -5. **Disposal**: Registry is disposed when provider unmounts - -### Multiple Registries - -You can have multiple registries in your application for different scopes: - -```tsx -function App() { - return ( -
- {/* Global registry */} - - - - {/* Scoped registry for modal */} - - - - -
- ) -} -``` - -### Performance Considerations - -#### Registry Configuration - -- **timeoutResolution**: Lower values = more frequent cleanup checks = higher CPU usage -- **defaultIdleTTL**: Lower values = more aggressive cleanup = lower memory usage -- **scheduleTask**: Custom schedulers can optimize for specific use cases - -#### Best Practices - -1. **Use a single registry per app**: Unless you need isolation, one registry is usually sufficient -2. **Configure timeouts appropriately**: Balance between memory usage and cleanup overhead -3. **Avoid direct registry access**: Use hooks instead of direct registry manipulation -4. **Initialize heavy atoms lazily**: Don't put expensive computations in initial values - -### Error Handling - -The registry handles errors gracefully: - -```tsx -// Atoms that fail will be marked as failed -const failingAtom = Atom.fn(() => Effect.fail("Something went wrong")) - -function ErrorHandling() { - const result = useAtomSuspenseResult(() => failingAtom) - - if (result.error) { - return
Error: {result.error.message}
- } - - return
Success: {result.data}
-} -``` - -### Testing with Registries - -For testing, you can provide a clean registry for each test: - -```tsx -import { Registry } from "@effect-atom/atom" -import { render } from "@solidjs/testing-library" - -test("component with atoms", () => { - const testRegistry = Registry.make() - - render(() => ( - - - - )) - - // Test assertions... -}) -``` diff --git a/packages/solid/effect-atom/docs/debugging-guide.md b/packages/solid/effect-atom/docs/debugging-guide.md deleted file mode 100644 index 5acad1bc..00000000 --- a/packages/solid/effect-atom/docs/debugging-guide.md +++ /dev/null @@ -1,341 +0,0 @@ ---- -title: "Debugging Guide" -parent: "@effectify/solid-effect-atom" -nav_order: 5 ---- - -# Debugging Guide - -Advanced debugging techniques and troubleshooting for `@effectify/solid-effect-atom`. - -## Table of Contents - -- [Common Issues](#common-issues) -- [Debugging Tools](#debugging-tools) -- [Performance Debugging](#performance-debugging) -- [SSR Debugging](#ssr-debugging) -- [Error Handling](#error-handling) - -## Common Issues - -### 1. Atom Not Updating - -**Problem**: Atom value changes but UI doesn't update. - -```tsx -// ❌ Problem: Creating atom inside component -function MyComponent() { - const atom = Atom.make(0) // New atom on every render! - const [value, setValue] = useAtom(() => atom) - // ... -} - -// ✅ Solution: Create atom outside component -const counterAtom = Atom.make(0) - -function MyComponent() { - const [value, setValue] = useAtom(() => counterAtom) - // ... -} -``` - -**Debugging Steps**: - -1. Check if atom is created outside component -2. Verify atom factory function is stable -3. Use browser DevTools to inspect atom state - -### 2. Memory Leaks - -**Problem**: Atoms not being garbage collected. - -```tsx -// ❌ Problem: keepAlive atoms without cleanup -const persistentAtom = Atom.make(data).pipe(Atom.keepAlive) - -// ✅ Solution: Proper cleanup -const dataAtom = Atom.make(data) // Auto-dispose when not used - -// Or manual cleanup for keepAlive atoms -onCleanup(() => { - registry.dispose() -}) -``` - -### 3. Infinite Loops - -**Problem**: Circular dependencies between atoms. - -```tsx -// ❌ Problem: Circular dependency -const atomA = Atom.make((get) => get(atomB) + 1) -const atomB = Atom.make((get) => get(atomA) + 1) - -// ✅ Solution: Break the cycle -const baseAtom = Atom.make(0) -const atomA = Atom.make((get) => get(baseAtom) + 1) -const atomB = Atom.make((get) => get(baseAtom) + 2) -``` - -## Debugging Tools - -### 1. Browser DevTools - -Enable detailed logging in development: - -```tsx -// Add to your main.tsx in development -if (import.meta.env.DEV) { - // Log atom state changes - const originalSet = registry.set - registry.set = function(atom, value) { - console.log("Atom updated:", { atom: atom.toString(), value }) - return originalSet.call(this, atom, value) - } -} -``` - -### 2. Atom Inspector - -Create a debugging component to inspect atom state: - -```tsx -function AtomInspector({ atom, name }: { atom: Atom.Atom; name: string }) { - const value = useAtomValue(() => atom) - - return ( -
-

{name}

-
{JSON.stringify(value(), null, 2)}
-
- ) -} - -// Usage - -``` - -### 3. Performance Profiler - -Track atom computation performance: - -```tsx -function createProfiledAtom(computation: () => T, name: string) { - return Atom.make(() => { - const start = performance.now() - const result = computation() - const end = performance.now() - - if (end - start > 10) { // Log slow computations - console.warn(`Slow atom computation: ${name} took ${end - start}ms`) - } - - return result - }) -} -``` - -## Performance Debugging - -### 1. Identifying Slow Atoms - -```tsx -// Wrap expensive computations -const expensiveAtom = Atom.make((get) => { - console.time("expensive-computation") - const result = heavyComputation(get(dataAtom)) - console.timeEnd("expensive-computation") - return result -}) -``` - -### 2. Subscription Tracking - -```tsx -// Track subscription count -let subscriptionCount = 0 - -const trackedAtom = Atom.make((get) => { - subscriptionCount++ - console.log(`Atom subscriptions: ${subscriptionCount}`) - return get(baseAtom) * 2 -}) -``` - -### 3. Bundle Size Analysis - -Check what's being imported: - -```bash -# Analyze bundle size -pnpm add -D @rollup/plugin-analyzer -``` - -```tsx -// Check imports -import { Atom } from "@effectify/solid-effect-atom" // ✅ Tree-shakeable -import * as AtomSolid from "@effectify/solid-effect-atom" // ❌ Imports everything -``` - -## SSR Debugging - -### 1. Hydration Mismatches - -```tsx -// Debug hydration state -function DebugHydration() { - const [isHydrated, setIsHydrated] = createSignal(false) - - onMount(() => { - setIsHydrated(true) - }) - - return ( -
-

Hydration status: {isHydrated() ? "Hydrated" : "SSR"}

- {/* Your components */} -
- ) -} -``` - -### 2. SSR State Inspection - -```tsx -// Server-side debugging -export async function renderApp() { - const registry = createSSRRegistry() - - // Debug preloaded atoms - const result = await preloadAtoms(registry, criticalAtoms) - console.log("SSR State:", { - atoms: result.dehydratedState.length, - errors: result.errors, - timeouts: result.timeouts, - }) - - return { html, state: result.dehydratedState } -} -``` - -## Error Handling - -### 1. Atom Error Boundaries - -```tsx -function AtomErrorBoundary(props: { children: JSX.Element }) { - return ( - ( -
-

Atom Error

-
- Error Details -
{err.stack}
-
- -
- )} - > - {props.children} -
- ) -} -``` - -### 2. Effect Error Handling - -```tsx -const resilientAtom = Atom.fn(() => - Effect.gen(function*() { - const data = yield* Effect.tryPromise(() => fetch("/api/data").then((r) => r.json())).pipe( - Effect.timeout("5000ms"), - Effect.retry({ times: 3 }), - Effect.catchAll((error) => { - console.error("Atom error:", error) - return Effect.succeed({ error: true, message: error.message }) - }), - ) - return data - }) -) -``` - -### 3. Development Warnings - -```tsx -// Add development-only warnings -if (import.meta.env.DEV) { - const warnSlowAtoms = (atom: Atom.Atom, time: number) => { - if (time > 100) { - console.warn(`Slow atom detected: ${atom.toString()} took ${time}ms`) - } - } - - // Patch atom creation to add timing - const originalMake = Atom.make - Atom.make = function(...args) { - const atom = originalMake.apply(this, args) - // Add timing wrapper - return atom - } -} -``` - -## Best Practices - -### 1. Naming Conventions - -```tsx -// Use descriptive names -const userProfileAtom = Atom.make(null) // ✅ -const atom1 = Atom.make(null) // ❌ - -// Add labels for debugging -const debugAtom = Atom.make(0).pipe( - Atom.withLabel("counter-debug"), -) -``` - -### 2. Error Reporting - -```tsx -// Centralized error reporting -const errorReportingAtom = Atom.fn((get, error: Error) => - Effect.gen(function*() { - // Log to console in development - if (import.meta.env.DEV) { - console.error("Atom error:", error) - } - - // Report to service in production - if (import.meta.env.PROD) { - yield* Effect.tryPromise(() => - fetch("/api/errors", { - method: "POST", - body: JSON.stringify({ error: error.message, stack: error.stack }), - }) - ) - } - }) -) -``` - -### 3. Testing Helpers - -```tsx -// Test utilities -export function createTestRegistry() { - const registry = Registry.make() - - // Add debugging helpers - registry.debug = { - getAtomCount: () => registry.getNodes().size, - logState: () => console.log(Array.from(registry.getNodes().entries())), - } - - return registry -} -``` - -This debugging guide provides comprehensive tools and techniques for troubleshooting `@effectify/solid-effect-atom` applications in development and production environments. diff --git a/packages/solid/effect-atom/docs/examples.md b/packages/solid/effect-atom/docs/examples.md deleted file mode 100644 index 79672ce8..00000000 --- a/packages/solid/effect-atom/docs/examples.md +++ /dev/null @@ -1,456 +0,0 @@ ---- -title: "Examples" -parent: "@effectify/solid-effect-atom" -nav_order: 3 ---- - -# Examples - -Complete examples demonstrating various use cases with `@effectify/solid-effect-atom`. - -## Table of Contents - -- [Todo App](#todo-app) -- [Shopping Cart](#shopping-cart) -- [Real-time Chat](#real-time-chat) -- [Data Dashboard](#data-dashboard) - -## Todo App - -A complete todo application demonstrating CRUD operations, filtering, and persistence. - -### Atoms - -```tsx -// atoms/todos.ts -import { Atom } from "@effectify/solid-effect-atom" -import { Effect } from "effect" - -export interface Todo { - id: string - text: string - completed: boolean - createdAt: Date -} - -export type Filter = "all" | "active" | "completed" - -// Base atoms -export const todosAtom = Atom.make([]) -export const filterAtom = Atom.make("all") - -// Computed atoms -export const filteredTodosAtom = Atom.make((get) => { - const todos = get(todosAtom) - const filter = get(filterAtom) - - switch (filter) { - case "active": - return todos.filter((todo) => !todo.completed) - case "completed": - return todos.filter((todo) => todo.completed) - default: - return todos - } -}) - -export const todoStatsAtom = Atom.make((get) => { - const todos = get(todosAtom) - const completed = todos.filter((todo) => todo.completed).length - const active = todos.length - completed - - return { total: todos.length, completed, active } -}) - -// Actions -export const addTodoAtom = Atom.fn((get, text: string) => - Effect.gen(function*() { - const currentTodos = get(todosAtom) - const newTodo: Todo = { - id: crypto.randomUUID(), - text: text.trim(), - completed: false, - createdAt: new Date(), - } - - const updatedTodos = [...currentTodos, newTodo] - yield* Effect.sync(() => todosAtom.set(updatedTodos)) - - // Persist to localStorage - yield* Effect.sync(() => localStorage.setItem("todos", JSON.stringify(updatedTodos))) - - return newTodo - }) -) - -export const toggleTodoAtom = Atom.fn((get, id: string) => - Effect.gen(function*() { - const currentTodos = get(todosAtom) - const updatedTodos = currentTodos.map((todo) => todo.id === id ? { ...todo, completed: !todo.completed } : todo) - - yield* Effect.sync(() => todosAtom.set(updatedTodos)) - yield* Effect.sync(() => localStorage.setItem("todos", JSON.stringify(updatedTodos))) - }) -) - -export const deleteTodoAtom = Atom.fn((get, id: string) => - Effect.gen(function*() { - const currentTodos = get(todosAtom) - const updatedTodos = currentTodos.filter((todo) => todo.id !== id) - - yield* Effect.sync(() => todosAtom.set(updatedTodos)) - yield* Effect.sync(() => localStorage.setItem("todos", JSON.stringify(updatedTodos))) - }) -) -``` - -### Components - -```tsx -// components/TodoApp.tsx -import { useAtom, useAtomSet, useAtomValue } from "@effectify/solid-effect-atom" -import { createSignal, For } from "solid-js" -import { - addTodoAtom, - deleteTodoAtom, - type Filter, - filterAtom, - filteredTodosAtom, - todosAtom, - todoStatsAtom, - toggleTodoAtom, -} from "../atoms/todos" - -function TodoInput() { - const [newTodo, setNewTodo] = createSignal("") - const addTodo = useAtomSet(() => addTodoAtom) - - const handleSubmit = (e: Event) => { - e.preventDefault() - const text = newTodo().trim() - if (text) { - addTodo(text) - setNewTodo("") - } - } - - return ( -
- setNewTodo(e.currentTarget.value)} - placeholder="What needs to be done?" - class="new-todo" - /> - -
- ) -} - -function TodoItem(props: { todo: Todo }) { - const toggleTodo = useAtomSet(() => toggleTodoAtom) - const deleteTodo = useAtomSet(() => deleteTodoAtom) - - return ( -
  • - toggleTodo(props.todo.id)} - /> - {props.todo.text} - -
  • - ) -} - -function TodoList() { - const filteredTodos = useAtomValue(() => filteredTodosAtom) - - return ( -
      - - {(todo) => } - - {filteredTodos().length === 0 &&
    • No todos found
    • } -
    - ) -} - -function TodoFilters() { - const [filter, setFilter] = useAtom(() => filterAtom) - - const filters: { key: Filter; label: string }[] = [ - { key: "all", label: "All" }, - { key: "active", label: "Active" }, - { key: "completed", label: "Completed" }, - ] - - return ( -
    - - {(filterOption) => ( - - )} - -
    - ) -} - -function TodoStats() { - const stats = useAtomValue(() => todoStatsAtom) - - return ( -
    - Total: {stats().total} - Active: {stats().active} - Completed: {stats().completed} -
    - ) -} - -export function TodoApp() { - return ( -
    -

    Todo App

    - - - - -
    - ) -} -``` - -## Shopping Cart - -A shopping cart with product catalog, cart management, and checkout. - -### Atoms - -```tsx -// atoms/shopping.ts -import { Atom } from "@effectify/solid-effect-atom" -import { Effect } from "effect" - -export interface Product { - id: string - name: string - price: number - image: string - description: string -} - -export interface CartItem { - product: Product - quantity: number -} - -// Base atoms -export const productsAtom = Atom.make([]) -export const cartAtom = Atom.make([]) - -// Computed atoms -export const cartTotalAtom = Atom.make((get) => { - const cart = get(cartAtom) - return cart.reduce((total, item) => total + (item.product.price * item.quantity), 0) -}) - -export const cartItemCountAtom = Atom.make((get) => { - const cart = get(cartAtom) - return cart.reduce((count, item) => count + item.quantity, 0) -}) - -// Actions -export const addToCartAtom = Atom.fn((get, product: Product, quantity = 1) => - Effect.gen(function*() { - const currentCart = get(cartAtom) - const existingItem = currentCart.find((item) => item.product.id === product.id) - - let updatedCart: CartItem[] - if (existingItem) { - updatedCart = currentCart.map((item) => - item.product.id === product.id - ? { ...item, quantity: item.quantity + quantity } - : item - ) - } else { - updatedCart = [...currentCart, { product, quantity }] - } - - yield* Effect.sync(() => cartAtom.set(updatedCart)) - }) -) - -export const removeFromCartAtom = Atom.fn((get, productId: string) => - Effect.gen(function*() { - const currentCart = get(cartAtom) - const updatedCart = currentCart.filter((item) => item.product.id !== productId) - yield* Effect.sync(() => cartAtom.set(updatedCart)) - }) -) - -export const updateQuantityAtom = Atom.fn((get, productId: string, quantity: number) => - Effect.gen(function*() { - const currentCart = get(cartAtom) - - if (quantity <= 0) { - const updatedCart = currentCart.filter((item) => item.product.id !== productId) - yield* Effect.sync(() => cartAtom.set(updatedCart)) - } else { - const updatedCart = currentCart.map((item) => - item.product.id === productId - ? { ...item, quantity } - : item - ) - yield* Effect.sync(() => cartAtom.set(updatedCart)) - } - }) -) -``` - -### Components - -```tsx -// components/ShoppingApp.tsx -import { useAtomSet, useAtomValue } from "@effectify/solid-effect-atom" -import { For } from "solid-js" -import { - addToCartAtom, - cartAtom, - cartItemCountAtom, - cartTotalAtom, - type Product, - productsAtom, - removeFromCartAtom, - updateQuantityAtom, -} from "../atoms/shopping" - -function ProductCard(props: { product: Product }) { - const addToCart = useAtomSet(() => addToCartAtom) - - return ( -
    - {props.product.name} -

    {props.product.name}

    -

    {props.product.description}

    -

    ${props.product.price.toFixed(2)}

    - -
    - ) -} - -function ProductCatalog() { - const products = useAtomValue(() => productsAtom) - - return ( -
    -

    Products

    -
    - - {(product) => } - -
    -
    - ) -} - -function CartItem(props: { item: CartItem }) { - const updateQuantity = useAtomSet(() => updateQuantityAtom) - const removeFromCart = useAtomSet(() => removeFromCartAtom) - - return ( -
    - {props.item.product.name} -
    -

    {props.item.product.name}

    -

    ${props.item.product.price.toFixed(2)}

    -
    -
    - - {props.item.quantity} - -
    - -
    - ) -} - -function ShoppingCart() { - const cart = useAtomValue(() => cartAtom) - const total = useAtomValue(() => cartTotalAtom) - - return ( -
    -

    Shopping Cart

    - {cart().length === 0 ?

    Your cart is empty

    : ( - <> - - {(item) => } - -
    -

    Total: ${total().toFixed(2)}

    - -
    - - )} -
    - ) -} - -function CartBadge() { - const itemCount = useAtomValue(() => cartItemCountAtom) - - return ( -
    - 🛒 {itemCount() > 0 && {itemCount()}} -
    - ) -} - -export function ShoppingApp() { - return ( -
    -
    -

    Shopping App

    - -
    -
    - - -
    -
    - ) -} -``` diff --git a/packages/solid/effect-atom/docs/guides.md b/packages/solid/effect-atom/docs/guides.md deleted file mode 100644 index 1a5f5b6d..00000000 --- a/packages/solid/effect-atom/docs/guides.md +++ /dev/null @@ -1,687 +0,0 @@ ---- -title: "Usage Guides" -parent: "@effectify/solid-effect-atom" -nav_order: 2 ---- - -# Usage Guides - -Practical guides for common patterns and use cases with `@effectify/solid-effect-atom`. - -## Table of Contents - -- [Getting Started](#getting-started) -- [State Management Patterns](#state-management-patterns) -- [Async Operations](#async-operations) -- [Performance Optimization](#performance-optimization) -- [Error Handling](#error-handling) -- [Testing](#testing) - -## Getting Started - -### Project Setup - -1. **Install dependencies**: - -```bash -pnpm add @effectify/solid-effect-atom @effect-atom/atom effect solid-js -``` - -2. **Setup your app**: - -```tsx -// src/index.tsx -import { render } from "solid-js/web" -import { RegistryProvider } from "@effectify/solid-effect-atom" -import App from "./App" - -render(() => ( - - - -), document.getElementById("root")!) -``` - -3. **Create your first atom**: - -```tsx -// src/atoms.ts -import { Atom } from "@effectify/solid-effect-atom" - -export const counterAtom = Atom.make(0) -export const userAtom = Atom.make(null) -``` - -### Basic Component - -```tsx -// src/Counter.tsx -import { useAtom } from "@effectify/solid-effect-atom" -import { counterAtom } from "./atoms" - -export function Counter() { - const [count, setCount] = useAtom(() => counterAtom) - - return ( -
    -

    Counter: {count()}

    - - - -
    - ) -} -``` - -## State Management Patterns - -### 1. Shared State - -Share state between multiple components: - -```tsx -// atoms.ts -export const themeAtom = Atom.make<"light" | "dark">("light") - -// Header.tsx -function Header() { - const [theme, setTheme] = useAtom(() => themeAtom) - - return ( -
    - -
    - ) -} - -// Sidebar.tsx -function Sidebar() { - const theme = useAtomValue(() => themeAtom) - - return ( - - ) -} -``` - -### 2. Computed State - -Create derived state with computed atoms: - -```tsx -// atoms.ts -export const todosAtom = Atom.make([]) - -export const completedTodosAtom = Atom.make((get) => get(todosAtom).filter((todo) => todo.completed)) - -export const todoStatsAtom = Atom.make((get) => { - const todos = get(todosAtom) - const completed = get(completedTodosAtom) - - return { - total: todos.length, - completed: completed.length, - remaining: todos.length - completed.length, - completionRate: todos.length > 0 ? completed.length / todos.length : 0, - } -}) - -// TodoStats.tsx -function TodoStats() { - const stats = useAtomValue(() => todoStatsAtom) - - return ( -
    -

    Total: {stats().total}

    -

    Completed: {stats().completed}

    -

    Remaining: {stats().remaining}

    -

    Progress: {Math.round(stats().completionRate * 100)}%

    -
    - ) -} -``` - -### 3. Form State - -Manage complex form state: - -```tsx -// atoms.ts -interface UserForm { - name: string - email: string - age: number -} - -export const userFormAtom = Atom.make({ - name: "", - email: "", - age: 0, -}) - -export const formValidationAtom = Atom.make((get) => { - const form = get(userFormAtom) - const errors: Partial = {} - - if (!form.name.trim()) errors.name = "Name is required" - if (!form.email.includes("@")) errors.email = "Invalid email" - if (form.age < 18) errors.age = "Must be 18 or older" - - return { - errors, - isValid: Object.keys(errors).length === 0, - } -}) - -// UserForm.tsx -function UserForm() { - const [form, setForm] = useAtom(() => userFormAtom) - const validation = useAtomValue(() => formValidationAtom) - - const updateField = (field: keyof UserForm, value: any) => { - setForm({ ...form(), [field]: value }) - } - - return ( -
    - updateField("name", e.currentTarget.value)} - placeholder="Name" - /> - {validation().errors.name && {validation().errors.name}} - - updateField("email", e.currentTarget.value)} - placeholder="Email" - /> - {validation().errors.email && {validation().errors.email}} - - updateField("age", parseInt(e.currentTarget.value))} - placeholder="Age" - /> - {validation().errors.age && {validation().errors.age}} - - -
    - ) -} -``` - -## Async Operations - -### 1. API Data Fetching - -```tsx -// api.ts -import { Effect } from "effect" - -interface User { - id: number - name: string - email: string -} - -export const fetchUser = (id: number) => - Effect.gen(function*() { - const response = yield* Effect.tryPromise(() => fetch(`/api/users/${id}`)) - const user = yield* Effect.tryPromise(() => response.json()) - return user as User - }) - -// atoms.ts -export const userIdAtom = Atom.make(1) - -export const userAtom = Atom.fn((get) => fetchUser(get(userIdAtom))) - -// UserProfile.tsx -function UserProfile() { - const result = useAtomSuspenseResult(() => userAtom) - const [userId, setUserId] = useAtom(() => userIdAtom) - const refreshUser = useAtomSet(() => userAtom) - - return ( -
    - setUserId(parseInt(e.currentTarget.value))} - /> - - {(() => { - const current = result() - if (current.loading) return

    Loading user...

    - if (current.error) return

    Error: {String(current.error)}

    - - return ( -
    -

    {current.data.name}

    -

    {current.data.email}

    - -
    - ) - })()} -
    - ) -} -``` - -### 2. Optimistic Updates - -```tsx -// atoms.ts -export const optimisticTodosAtom = Atom.make([]) - -export const addTodoAtom = Atom.fn((get, title: string) => - Effect.gen(function*() { - const tempId = Date.now() - const tempTodo = { id: tempId, title, completed: false } - - // Optimistic update - const currentTodos = get(optimisticTodosAtom) - yield* Effect.sync(() => optimisticTodosAtom.set([...currentTodos, tempTodo])) - - try { - // API call - const newTodo = yield* Effect.tryPromise(() => - fetch("/api/todos", { - method: "POST", - body: JSON.stringify({ title }), - headers: { "Content-Type": "application/json" }, - }).then((r) => r.json()) - ) - - // Replace temp todo with real one - const updatedTodos = get(optimisticTodosAtom).map((todo) => todo.id === tempId ? newTodo : todo) - yield* Effect.sync(() => optimisticTodosAtom.set(updatedTodos)) - - return newTodo - } catch (error) { - // Rollback on error - const rolledBackTodos = get(optimisticTodosAtom).filter( - (todo) => todo.id !== tempId, - ) - yield* Effect.sync(() => optimisticTodosAtom.set(rolledBackTodos)) - yield* Effect.fail(error) - } - }) -) -``` - -## Performance Optimization - -### 1. Selective Subscriptions - -Only subscribe to specific parts of large objects: - -```tsx -// Instead of subscribing to entire user object -const user = useAtomValue(() => userAtom) -const name = () => user().name // Re-renders when any user field changes - -// Create specific atoms for better performance -const userNameAtom = Atom.make((get) => get(userAtom)?.name ?? "") -const userName = useAtomValue(() => userNameAtom) // Only re-renders when name changes -``` - -### 2. Memoized Computations - -```tsx -// Expensive computation atom -const expensiveComputationAtom = Atom.make((get) => { - const data = get(dataAtom) - const filters = get(filtersAtom) - - // This only re-runs when data or filters change - return data - .filter((item) => filters.categories.includes(item.category)) - .sort((a, b) => a[filters.sortBy] - b[filters.sortBy]) - .slice(0, filters.limit) -}) -``` - -### 3. Lazy Loading - -```tsx -// Only load data when needed -const lazyDataAtom = Atom.fn(() => - Effect.gen(function*() { - // This only runs when the atom is first accessed - yield* Effect.sleep(100) // Simulate delay - const data = yield* Effect.tryPromise(() => fetch("/api/heavy-data")) - return yield* Effect.tryPromise(() => data.json()) - }) -) - -function LazyComponent() { - const [shouldLoad, setShouldLoad] = createSignal(false) - - return ( -
    - {!shouldLoad() ? - ( - - ) : - } -
    - ) -} - -function DataDisplay() { - const result = useAtomSuspenseResult(() => lazyDataAtom) - // Data only loads when this component mounts - - return ( -
    - {(() => { - const current = result() - if (current.loading) return

    Loading...

    - if (current.error) return

    Error loading data

    - return
    {JSON.stringify(current.data, null, 2)}
    - })()} -
    - ) -} -``` - -## Error Handling - -### 1. Graceful Error Recovery - -```tsx -// atoms.ts -export const apiDataAtom = Atom.fn(() => - Effect.gen(function*() { - const data = yield* Effect.tryPromise(() => fetch("/api/data").then((r) => r.json())) - return data - }).pipe( - Effect.catchAll((error) => Effect.succeed({ error: String(error), data: null })), - ) -) - -// ErrorBoundary.tsx -function DataWithErrorHandling() { - const result = useAtomSuspenseResult(() => apiDataAtom) - - return ( -
    - {(() => { - const current = result() - if (current.loading) return

    Loading...

    - - if (current.data?.error) { - return ( -
    -

    Failed to load data: {current.data.error}

    - -
    - ) - } - - return - })()} -
    - ) -} -``` - -### 2. Error Boundaries - -```tsx -// ErrorBoundary.tsx -import { ErrorBoundary } from "solid-js" - -function AppWithErrorBoundary() { - return ( - ( -
    -

    Something went wrong

    -

    {err.message}

    - -
    - )} - > - -
    - ) -} -``` - -### 3. Retry Logic - -```tsx -// atoms.ts -export const retryableDataAtom = Atom.fn(() => - Effect.gen(function*() { - const data = yield* Effect.tryPromise(() => fetch("/api/unreliable-endpoint").then((r) => r.json())) - return data - }).pipe( - Effect.retry({ - times: 3, - delay: (attempt) => `${attempt * 1000}ms`, - }), - ) -) -``` - -## Testing - -### 1. Unit Testing Atoms - -```tsx -// atoms.test.ts -import { describe, expect, test } from "vitest" -import { Registry } from "@effect-atom/atom" -import { counterAtom, doubledAtom } from "./atoms" - -describe("Counter Atoms", () => { - test("should increment counter", () => { - const registry = Registry.make() - - expect(registry.get(counterAtom)).toBe(0) - - registry.set(counterAtom, 5) - expect(registry.get(counterAtom)).toBe(5) - }) - - test("should compute doubled value", () => { - const registry = Registry.make() - - registry.set(counterAtom, 10) - expect(registry.get(doubledAtom)).toBe(20) - }) -}) -``` - -### 2. Component Testing - -```tsx -// Counter.test.tsx -import { fireEvent, render } from "@solidjs/testing-library" -import { describe, expect, test } from "vitest" -import { RegistryProvider } from "@effectify/solid-effect-atom" -import { Counter } from "./Counter" - -describe("Counter Component", () => { - test("should render and increment", async () => { - const { getByText, getByRole } = render(() => ( - - - - )) - - expect(getByText("Counter: 0")).toBeInTheDocument() - - const incrementButton = getByRole("button", { name: /increment/i }) - fireEvent.click(incrementButton) - - expect(getByText("Counter: 1")).toBeInTheDocument() - }) -}) -``` - -### 3. Async Testing - -```tsx -// AsyncComponent.test.tsx -import { render, waitFor } from "@solidjs/testing-library" -import { describe, expect, test, vi } from "vitest" -import { Effect } from "effect" -import { RegistryProvider } from "@effectify/solid-effect-atom" -import { AsyncDataComponent } from "./AsyncDataComponent" - -// Mock the API -vi.mock("./api", () => ({ - fetchData: vi.fn(() => Effect.succeed({ message: "Test data" })), -})) - -describe("AsyncDataComponent", () => { - test("should handle loading and success states", async () => { - const { getByText } = render(() => ( - - - - )) - - // Should show loading initially - expect(getByText("Loading...")).toBeInTheDocument() - - // Should show data after loading - await waitFor(() => { - expect(getByText("Test data")).toBeInTheDocument() - }) - }) -}) -``` - -### 4. Integration Testing - -```tsx -// App.test.tsx -import { fireEvent, render, waitFor } from "@solidjs/testing-library" -import { describe, expect, test } from "vitest" -import { RegistryProvider } from "@effectify/solid-effect-atom" -import { App } from "./App" - -describe("App Integration", () => { - test("should handle complete user flow", async () => { - const { getByText, getByPlaceholderText, getByRole } = render(() => ( - - - - )) - - // Add a todo - const input = getByPlaceholderText("Add todo...") - const addButton = getByRole("button", { name: /add/i }) - - fireEvent.input(input, { target: { value: "Test todo" } }) - fireEvent.click(addButton) - - // Verify todo appears - await waitFor(() => { - expect(getByText("Test todo")).toBeInTheDocument() - }) - - // Mark as complete - const checkbox = getByRole("checkbox") - fireEvent.click(checkbox) - - // Verify completion - await waitFor(() => { - expect(getByText("Completed: 1")).toBeInTheDocument() - }) - }) -}) -``` - -## Best Practices Summary - -### Do's ✅ - -- **Use atom factories**: Always pass `() => atom` to hooks -- **Leverage computed atoms**: Create derived state for better performance -- **Handle errors gracefully**: Use Effect's error handling capabilities -- **Test thoroughly**: Unit test atoms and integration test components -- **Optimize selectively**: Use specific atoms for parts of large objects -- **Clean up properly**: Let SolidJS handle cleanup automatically - -### Don'ts ❌ - -- **Don't pass atoms directly**: Use `() => atom` instead of `atom` -- **Don't create registries manually**: Use `RegistryProvider` -- **Don't ignore errors**: Always handle async operation failures -- **Don't over-optimize**: Profile before optimizing -- **Don't mutate atoms directly**: Use setters or registry methods -- **Don't forget TypeScript**: Leverage type safety for better DX - -## Advanced Patterns - -### 1. Atom Families - -```tsx -// Create atoms dynamically based on parameters -const todoAtomFamily = (id: string) => Atom.make(null) - -// Cache atoms to avoid recreation -const todoAtoms = new Map>() - -export const getTodoAtom = (id: string) => { - if (!todoAtoms.has(id)) { - todoAtoms.set(id, todoAtomFamily(id)) - } - return todoAtoms.get(id)! -} -``` - -### 2. Middleware Pattern - -```tsx -// Create reusable atom enhancers -const withLogging = (atom: Atom.Atom, name: string) => - Atom.make((get) => { - const value = get(atom) - console.log(`${name}:`, value) - return value - }) - -const withPersistence = (atom: Atom.Atom, key: string) => - Atom.make((get) => { - const value = get(atom) - localStorage.setItem(key, JSON.stringify(value)) - return value - }) - -// Usage -export const persistedCounterAtom = withPersistence( - withLogging(counterAtom, "counter"), - "counter-value", -) -``` - -This completes our comprehensive usage guide covering all major patterns and best practices for `@effectify/solid-effect-atom`! diff --git a/packages/solid/effect-atom/docs/index.md b/packages/solid/effect-atom/docs/index.md deleted file mode 100644 index 7c46587c..00000000 --- a/packages/solid/effect-atom/docs/index.md +++ /dev/null @@ -1,255 +0,0 @@ ---- -title: "@effectify/solid-effect-atom" -has_children: true -permalink: /docs/atom-solid -nav_order: 6 ---- - -# @effectify/solid-effect-atom - -Reactive state management for SolidJS applications using Effect Atom. - -## Overview - -`@effectify/solid-effect-atom` provides seamless integration between [Effect Atom](../atom) and [SolidJS](https://solidjs.com), leveraging SolidJS's fine-grained reactivity system for optimal performance. - -## Key Features - -- 🚀 **Fine-grained Reactivity**: Leverages SolidJS signals for precise updates -- ⚡ **High Performance**: Superior performance compared to virtual DOM solutions -- 🔄 **Effect Integration**: Full support for Effect's async operations and error handling -- 🧪 **Type Safe**: Complete TypeScript support with excellent type inference -- 🎯 **Small Bundle**: Minimal overhead with tree-shaking support -- 🧹 **Automatic Cleanup**: Memory-efficient with automatic subscription management - -## Quick Start - -### Installation - -```bash -pnpm add @effectify/solid-effect-atom @effect-atom/atom effect solid-js -``` - -### Basic Usage - -```tsx -import { Atom, RegistryProvider, useAtom, useAtomValue } from "@effectify/solid-effect-atom" -import { render } from "solid-js/web" - -// Create an atom -const counterAtom = Atom.make(0) - -function Counter() { - const [count, setCount] = useAtom(() => counterAtom) - - return ( -
    -

    Count: {count()}

    - -
    - ) -} - -function App() { - return ( - - - - ) -} - -render(() => , document.getElementById("root")!) -``` - -## Core Concepts - -### Atoms as Signals - -In SolidJS, atoms are exposed as signals (functions that return values). This enables fine-grained reactivity: - -```tsx -const value = useAtomValue(() => myAtom) -// value() returns the current value -// Components automatically update when the atom changes -``` - -### Registry Provider - -All atom operations require a registry context: - -```tsx -import { RegistryProvider } from "@effectify/solid-effect-atom" - -function App() { - return ( - - {/* Your app components */} - - ) -} -``` - -## API Reference - -### Hooks - -- [`useAtomValue`](./Primitives.ts.md#useatomvalue) - Read atom values as signals -- [`useAtom`](./Primitives.ts.md#useatom) - Read and write atom values -- [`useAtomSet`](./Primitives.ts.md#useatomset) - Write-only access to atoms -- [`useAtomSuspenseResult`](./AdvancedHooks.ts.md#useatomsuspenseresult) - Handle async atoms with suspense -- [`useAtomSubscribe`](./AdvancedHooks.ts.md#useatomsubscribe) - Subscribe to atom changes -- [`useAtomRef`](./AdvancedHooks.ts.md#useatomref) - Work with atom references -- [`useAtomMount`](./AdvancedHooks.ts.md#useatommount) - Mount atoms with cleanup -- [`useAtomRefresh`](./AdvancedHooks.ts.md#useatomrefresh) - Refresh async atoms - -### Components - -- [`RegistryProvider`](./Context.ts.md#registryprovider) - Provide atom registry context - -### Context - -- [`useRegistry`](./Context.ts.md#useregistry) - Access the current registry - -## Examples - -### Basic Counter - -```tsx -import { Atom, useAtom } from "@effectify/solid-effect-atom" - -const counterAtom = Atom.make(0) - -function Counter() { - const [count, setCount] = useAtom(() => counterAtom) - - return ( -
    -

    Count: {count()}

    - - - -
    - ) -} -``` - -### Computed Values - -```tsx -import { Atom, useAtomValue } from "@effectify/solid-effect-atom" - -const baseAtom = Atom.make(10) -const doubledAtom = Atom.make((get) => get(baseAtom) * 2) - -function ComputedExample() { - const base = useAtomValue(() => baseAtom) - const doubled = useAtomValue(() => doubledAtom) - - return ( -
    -

    Base: {base()}

    -

    Doubled: {doubled()}

    -
    - ) -} -``` - -### Async Data - -```tsx -import { Atom, useAtomSet, useAtomSuspenseResult } from "@effectify/solid-effect-atom" -import { Effect } from "effect" - -const dataAtom = Atom.fn(() => - Effect.gen(function*() { - yield* Effect.sleep(1000) - return `Data loaded at ${new Date().toLocaleTimeString()}` - }) -) - -function AsyncExample() { - const result = useAtomSuspenseResult(() => dataAtom) - const refresh = useAtomSet(() => dataAtom) - - return ( -
    - {(() => { - const current = result() - if (current.loading) return

    Loading...

    - if (current.error) return

    Error: {String(current.error)}

    - return

    Data: {current.data}

    - })()} - -
    - ) -} -``` - -## Performance - -`@effectify/solid-effect-atom` is highly optimized: - -- **~10M operations/second** for async atom operations -- **~2KB memory per atom** in stress tests -- **Fine-grained updates** - only affected components re-render -- **Automatic cleanup** - no memory leaks - -## Comparison with React - -| Feature | atom-solid | atom-react | -| ------------------- | ------------ | --------------- | -| Bundle Size | ~15KB | ~25KB | -| Runtime Performance | 20%+ faster | Baseline | -| Memory Usage | 15%+ less | Baseline | -| Reactivity | Fine-grained | Component-level | -| TypeScript | Excellent | Good | - -## Migration from atom-react - -The APIs are very similar, with the main difference being that atom-solid returns signals: - -```tsx -// atom-react -const value = useAtomValue(atom) -return
    {value}
    - -// atom-solid -const value = useAtomValue(() => atom) -return
    {value()}
    // Note the function call -``` - -## Best Practices - -1. **Use atom factories**: Pass functions to hooks to enable proper reactivity -2. **Leverage computed atoms**: Create derived state with `Atom.make((get) => ...)` -3. **Handle async properly**: Use `useAtomSuspenseResult` for async atoms -4. **Clean up subscriptions**: Use `useAtomSubscribe` for side effects -5. **Optimize renders**: Take advantage of fine-grained reactivity - -## Documentation - -### Comprehensive Guides - -- [Usage Guides](./guides.md) - Comprehensive usage patterns and best practices -- [Examples](./examples.md) - Complete examples including Todo App and Shopping Cart -- [SSR Guide](./ssr-guide.md) - Server-side rendering with SolidStart -- [Debugging Guide](./debugging-guide.md) - Advanced debugging techniques and tools -- [Troubleshooting](./troubleshooting.md) - Common problems and solutions - -## Troubleshooting - -### Common Issues - -**Atoms not updating**: Make sure you're using atom factories (`() => atom`) in hooks. - -**Memory leaks**: Ensure you're using `RegistryProvider` and not creating registries manually. - -**TypeScript errors**: Check that you have the correct peer dependencies installed. - -For more detailed troubleshooting, see the [Troubleshooting Guide](./troubleshooting.md). - -## Contributing - -See the main [Effect Atom repository](https://github.com/tim-smart/effect-atom) for contribution guidelines. diff --git a/packages/solid/effect-atom/docs/index.ts.md b/packages/solid/effect-atom/docs/index.ts.md deleted file mode 100644 index ba3af68d..00000000 --- a/packages/solid/effect-atom/docs/index.ts.md +++ /dev/null @@ -1,225 +0,0 @@ ---- -title: "index.ts" -parent: "@effectify/solid-effect-atom" -nav_order: 1 ---- - -# @effectify/solid-effect-atom - -**Reactive state management for SolidJS applications using Effect atoms** - -`@effectify/solid-effect-atom` provides seamless integration between [Effect](https://effect.website) atoms and [SolidJS](https://solidjs.com) applications, leveraging SolidJS's fine-grained reactivity system for optimal performance. - -## Installation - -```bash -npm install @effectify/solid-effect-atom -# or -pnpm add @effectify/solid-effect-atom -# or -yarn add @effectify/solid-effect-atom -``` - -## Quick Start - -```tsx -import { Atom } from "@effect-atom/atom" -import { RegistryProvider, useAtom, useAtomValue } from "@effectify/solid-effect-atom" - -// Create an atom -const countAtom = Atom.make(0) - -function Counter() { - const [count, setCount] = useAtom(() => countAtom) - - return ( -
    -

    Count: {count()}

    - -
    - ) -} - -function App() { - return ( - - - - ) -} -``` - -## Key Features - -- **🚀 Fine-grained Reactivity**: Leverages SolidJS's reactive system for optimal performance -- **⚡ Zero Re-renders**: Updates only the specific DOM nodes that need to change -- **🔄 Async Support**: Built-in support for async atoms with loading states -- **🎯 TypeScript**: Full TypeScript support with excellent type inference -- **🧪 Well Tested**: Comprehensive test suite with 100% coverage -- **📦 Lightweight**: Minimal bundle size impact - -## Core Concepts - -### Atoms - -Atoms are the fundamental units of state in Effect. They can hold any value and can be derived from other atoms. - -```tsx -import { Atom } from "@effect-atom/atom" - -// Simple atom -const nameAtom = Atom.make("John") - -// Computed atom -const greetingAtom = Atom.make((get) => `Hello, ${get(nameAtom)}!`) - -// Async atom -const dataAtom = Atom.fn(() => - Effect.gen(function*() { - const response = yield* Effect.promise(() => fetch("/api/data")) - return yield* Effect.promise(() => response.json()) - }) -) -``` - -### Registry - -The registry manages atom state and subscriptions. Use `RegistryProvider` to provide a registry to your component tree. - -```tsx -import { RegistryProvider } from "@effectify/solid-effect-atom" - -function App() { - return ( - - {/* Your app components */} - - ) -} -``` - -## Hooks - -### useAtomValue - -Read the current value of an atom and subscribe to changes. - -```tsx -import { useAtomValue } from "@effectify/solid-effect-atom" - -function DisplayName() { - const name = useAtomValue(() => nameAtom) - return

    Name: {name()}

    -} -``` - -### useAtom - -Get both the current value and a setter function for an atom. - -```tsx -import { useAtom } from "@effectify/solid-effect-atom" - -function NameInput() { - const [name, setName] = useAtom(() => nameAtom) - - return ( - setName(e.currentTarget.value)} - /> - ) -} -``` - -### useAtomSet - -Get only the setter function for an atom. - -```tsx -import { useAtomSet } from "@effectify/solid-effect-atom" - -function ResetButton() { - const resetName = useAtomSet(() => nameAtom) - - return ( - - ) -} -``` - -## Advanced Usage - -### Async Atoms with Suspense - -Handle async atoms with built-in loading states. - -```tsx -import { useAtomSuspenseResult } from "@effectify/solid-effect-atom" - -function AsyncData() { - const result = useAtomSuspenseResult(() => dataAtom) - - return ( -
    - {result.loading &&

    Loading...

    } - {result.error &&

    Error: {result.error.message}

    } - {result.data &&

    Data: {JSON.stringify(result.data)}

    } -
    - ) -} -``` - -### Atom Subscriptions - -Subscribe to atom changes for side effects. - -```tsx -import { useAtomSubscribe } from "@effectify/solid-effect-atom" - -function Logger() { - useAtomSubscribe(() => countAtom, (value) => { - console.log("Count changed to:", value) - }) - - return null -} -``` - -## Performance Benefits - -SolidJS's fine-grained reactivity system means that atom-solid can update only the specific parts of your UI that depend on changed atoms, without re-rendering entire components. This results in: - -- **Faster updates**: Only affected DOM nodes are updated -- **Better performance**: No unnecessary re-renders or reconciliation -- **Smoother UX**: Consistent performance even with complex state - -## Comparison with React - -| Feature | atom-react | atom-solid | -| -------------- | ---------------------- | -------------------- | -| Re-renders | Component re-renders | Fine-grained updates | -| Performance | Good | Excellent | -| Bundle size | ~15kb | ~12kb | -| Learning curve | Familiar to React devs | SolidJS concepts | - -## Examples - -Check out the [sample application](https://github.com/tim-smart/effect-atom/tree/main/sample/solid) for complete examples including: - -- Counter with computed values -- Async data fetching -- Todo list management -- Shared state between components - -## API Reference - -See the individual hook documentation for detailed API information: - -- [Primitives](./Primitives.ts.html) - Core hooks (useAtomValue, useAtom, useAtomSet) -- [Context](./Context.ts.html) - Registry provider and context -- [AdvancedHooks](./AdvancedHooks.ts.html) - Advanced hooks for complex use cases diff --git a/packages/solid/effect-atom/docs/primitives.md b/packages/solid/effect-atom/docs/primitives.md deleted file mode 100644 index bbb7b410..00000000 --- a/packages/solid/effect-atom/docs/primitives.md +++ /dev/null @@ -1,278 +0,0 @@ ---- -title: "Primitives.ts" -parent: "@effectify/solid-effect-atom" -nav_order: 2 ---- - -# Primitives - -Core hooks for integrating Effect atoms with SolidJS components. - -## useAtomValue - -Read the current value of an atom and subscribe to changes. - -### Signature - -```typescript -export const useAtomValue: { -
    (atomFactory: () => Atom.Atom): Accessor - (atomFactory: () => Atom.Atom, f: (_: A) => B): Accessor -} -``` - -### Parameters - -- `atomFactory`: A function that returns the atom to read from -- `f` (optional): Transform function to apply to the atom value - -### Returns - -A SolidJS `Accessor` that returns the current atom value. - -### Examples - -#### Basic Usage - -```tsx -import { Atom } from "@effect-atom/atom" -import { useAtomValue } from "@effectify/solid-effect-atom" - -const nameAtom = Atom.make("John") - -function DisplayName() { - const name = useAtomValue(() => nameAtom) - - return

    Hello, {name()}!

    -} -``` - -#### With Transform Function - -```tsx -const countAtom = Atom.make(5) - -function DisplayDoubled() { - const doubled = useAtomValue(() => countAtom, (count) => count * 2) - - return

    Doubled: {doubled()}

    -} -``` - -#### With Computed Atoms - -```tsx -const firstNameAtom = Atom.make("John") -const lastNameAtom = Atom.make("Doe") -const fullNameAtom = Atom.make((get) => `${get(firstNameAtom)} ${get(lastNameAtom)}`) - -function DisplayFullName() { - const fullName = useAtomValue(() => fullNameAtom) - - return

    Full name: {fullName()}

    -} -``` - ---- - -## useAtom - -Get both the current value and a setter function for an atom. - -### Signature - -```typescript -export const useAtom:
    ( - atomFactory: () => Atom.Writable, -) => readonly [Accessor, (value: A | ((prev: A) => A)) => void] -``` - -### Parameters - -- `atomFactory`: A function that returns a writable atom - -### Returns - -A tuple containing: - -1. `Accessor`: The current atom value -2. `(value: A | ((prev: A) => A)) => void`: Setter function - -### Examples - -#### Basic Usage - -```tsx -import { Atom } from "@effect-atom/atom" -import { useAtom } from "@effectify/solid-effect-atom" - -const countAtom = Atom.make(0) - -function Counter() { - const [count, setCount] = useAtom(() => countAtom) - - return ( -
    -

    Count: {count()}

    - - -
    - ) -} -``` - -#### With Function Updater - -```tsx -function Counter() { - const [count, setCount] = useAtom(() => countAtom) - - return ( -
    -

    Count: {count()}

    - - -
    - ) -} -``` - -#### Form Input - -```tsx -const nameAtom = Atom.make("") - -function NameInput() { - const [name, setName] = useAtom(() => nameAtom) - - return ( - setName(e.currentTarget.value)} - placeholder="Enter your name" - /> - ) -} -``` - ---- - -## useAtomSet - -Get only the setter function for an atom. Useful when you only need to update an atom without reading its value. - -### Signature - -```typescript -export const useAtomSet:
    ( - atomFactory: () => Atom.Writable, -) => (value: A | ((prev: A) => A)) => void -``` - -### Parameters - -- `atomFactory`: A function that returns a writable atom - -### Returns - -A setter function that can accept either a new value or an updater function. - -### Examples - -#### Reset Button - -```tsx -import { Atom } from "@effect-atom/atom" -import { useAtomSet } from "@effectify/solid-effect-atom" - -const countAtom = Atom.make(0) - -function ResetButton() { - const resetCount = useAtomSet(() => countAtom) - - return ( - - ) -} -``` - -#### Action Buttons - -```tsx -const todoListAtom = Atom.make([]) - -function TodoActions() { - const setTodos = useAtomSet(() => todoListAtom) - - const addTodo = (text: string) => { - setTodos((prev) => [...prev, text]) - } - - const clearAll = () => { - setTodos([]) - } - - return ( -
    - - -
    - ) -} -``` - ---- - -## Performance Notes - -### Fine-grained Reactivity - -SolidJS's fine-grained reactivity system ensures that only the specific parts of your UI that depend on changed atoms are updated. This means: - -- **No component re-renders**: Unlike React, SolidJS doesn't re-render entire components -- **Surgical updates**: Only the DOM nodes that need to change are updated -- **Consistent performance**: Performance remains consistent regardless of component complexity - -### Best Practices - -1. **Use atom factories**: Always pass a function that returns the atom, not the atom directly: - ```tsx - // ✅ Good - const value = useAtomValue(() => myAtom) - - // ❌ Bad - const value = useAtomValue(myAtom) - ``` - -2. **Memoize expensive computations**: Use computed atoms for expensive calculations: - ```tsx - // ✅ Good - computed once, cached - const expensiveAtom = Atom.make((get) => expensiveCalculation(get(dataAtom))) - - // ❌ Bad - recalculated on every render - const value = useAtomValue(() => dataAtom, expensiveCalculation) - ``` - -3. **Prefer useAtomSet for write-only operations**: When you only need to update an atom: - ```tsx - // ✅ Good - no unnecessary subscription - const setCount = useAtomSet(() => countAtom) - - // ❌ Bad - creates unnecessary subscription - const [, setCount] = useAtom(() => countAtom) - ``` diff --git a/packages/solid/effect-atom/docs/ssr-guide.md b/packages/solid/effect-atom/docs/ssr-guide.md deleted file mode 100644 index a871381b..00000000 --- a/packages/solid/effect-atom/docs/ssr-guide.md +++ /dev/null @@ -1,463 +0,0 @@ ---- -title: "SSR Guide" -parent: "@effectify/solid-effect-atom" -nav_order: 4 ---- - -# Server-Side Rendering (SSR) Guide - -Complete guide for using `@effectify/solid-effect-atom` with Server-Side Rendering, including SolidStart integration. - -## Table of Contents - -- [Overview](#overview) -- [Basic SSR Setup](#basic-ssr-setup) -- [SolidStart Integration](#solidstart-integration) -- [Hydration Strategies](#hydration-strategies) -- [Best Practices](#best-practices) - -## Overview - -`@effectify/solid-effect-atom` provides comprehensive SSR support through: - -- **HydrationBoundary**: Component for managing client-side hydration -- **SSR Utilities**: Helper functions for server-side atom preloading -- **Isomorphic Atoms**: Atoms that behave differently on server vs client -- **State Serialization**: Safe serialization/deserialization of atom state - -## Basic SSR Setup - -### 1. Server-Side Rendering - -```tsx -// server.tsx -import { renderToString } from "solid-js/web" -import { createSSRRegistry, preloadAtoms, RegistryProvider, serializeState } from "@effectify/solid-effect-atom" -import { App } from "./App" -import { criticalAtoms } from "./atoms" - -export async function renderApp(url: string) { - // Create SSR-optimized registry - const registry = createSSRRegistry({ - timeout: 5000, - includeErrors: false, - }) - - // Preload critical atoms - const ssrResult = await preloadAtoms(registry, criticalAtoms, { - timeout: 3000, - }) - - // Render app to string - const html = renderToString(() => ( - - - - )) - - // Serialize state for client hydration - const serializedState = serializeState(ssrResult.dehydratedState) - - return { - html, - state: serializedState, - errors: ssrResult.errors, - timeouts: ssrResult.timeouts, - } -} -``` - -### 2. Client-Side Hydration - -```tsx -// client.tsx -import { hydrate } from "solid-js/web" -import { - deserializeState, - HydrationBoundary, - markHydrationComplete, - RegistryProvider, -} from "@effectify/solid-effect-atom" -import { App } from "./App" - -// Get serialized state from server -const serializedState = document.getElementById("atom-state")?.textContent || "[]" -const dehydratedState = deserializeState(serializedState) - -hydrate(() => ( - - - - - -), document.getElementById("root")!) - -// Mark hydration as complete -markHydrationComplete() -``` - -### 3. HTML Template - -```html - - - - SSR App - - -
    {{HTML}}
    - - - - -``` - -## SolidStart Integration - -### 1. Root Component Setup - -```tsx -// src/root.tsx -import { Suspense } from "solid-js" -import { Body, ErrorBoundary, FileRoutes, Head, Html, Meta, Routes, Scripts, Title } from "solid-start" -import { deserializeState, HydrationBoundary, RegistryProvider } from "@effectify/solid-effect-atom" - -export default function Root() { - // Get hydration state from server - const getHydrationState = () => { - if (typeof window !== "undefined") { - const stateElement = document.getElementById("atom-state") - if (stateElement) { - return deserializeState(stateElement.textContent || "[]") - } - } - return [] - } - - return ( - - - SolidStart + Effect Atom - - - - - - - - - - - - - - - - - - - ) -} -``` - -### 2. Route-Level Data Loading - -```tsx -// src/routes/users/[id].tsx -import { createRouteData, useParams } from "solid-start" -import { createSSRRegistry, isSSR, preloadAtoms, useAtomValue } from "@effectify/solid-effect-atom" -import { createUserAtom, userAtom } from "~/atoms/user" - -// Server-side data loading -export function routeData() { - return createRouteData(async (key) => { - const userId = key.params.id - - if (isSSR()) { - // Preload user data on server - const registry = createSSRRegistry() - const userAtomInstance = createUserAtom(userId) - - const result = await preloadAtoms(registry, [userAtomInstance]) - return { - userId, - ssrState: result.dehydratedState, - } - } - - return { userId, ssrState: [] } - }) -} - -export default function UserPage() { - const params = useParams() - const data = useRouteData() - - // Create user atom for this specific user - const userAtomInstance = () => createUserAtom(params.id) - const user = useAtomValue(userAtomInstance) - - return ( -
    -

    User Profile

    - {(() => { - const userData = user() - if (!userData) return

    Loading...

    - - return ( -
    -

    {userData.name}

    -

    {userData.email}

    -
    - ) - })()} -
    - ) -} -``` - -### 3. API Routes for Data - -```tsx -// src/routes/api/users/[id].ts -import { json } from "solid-start/api" -import type { APIEvent } from "solid-start/api" - -export async function GET({ params }: APIEvent) { - const userId = params.id - - // Fetch user data from database - const user = await fetchUserFromDB(userId) - - if (!user) { - return new Response("User not found", { status: 404 }) - } - - return json(user) -} - -async function fetchUserFromDB(id: string) { - // Your database logic here - return { - id, - name: `User ${id}`, - email: `user${id}@example.com`, - } -} -``` - -## Hydration Strategies - -### 1. Progressive Hydration - -```tsx -// atoms/progressive.ts -import { Atom, createSSRAtom, isHydrating } from "@effectify/solid-effect-atom" -import { Effect } from "effect" - -// Critical data that should be SSR'd -export const criticalDataAtom = Atom.fn(() => - Effect.gen(function*() { - const data = yield* Effect.tryPromise(() => fetch("/api/critical-data").then((r) => r.json())) - return data - }) -) - -// Non-critical data that can load after hydration -export const nonCriticalDataAtom = Atom.fn(() => - Effect.gen(function*() { - // Only load after hydration is complete - if (isHydrating()) { - return null - } - - const data = yield* Effect.tryPromise(() => fetch("/api/non-critical-data").then((r) => r.json())) - return data - }) -) - -// Fallback atom for SSR -export const userPreferencesAtom = createSSRAtom( - // Server fallback - { theme: "light", language: "en" }, - // Client atom - Atom.fn(() => - Effect.gen(function*() { - const prefs = localStorage.getItem("user-preferences") - return prefs ? JSON.parse(prefs) : { theme: "light", language: "en" } - }) - ), -) -``` - -### 2. Selective Hydration - -```tsx -// components/SelectiveHydration.tsx -import { createSignal, onMount } from "solid-js" -import { isSSR, useAtomValue } from "@effectify/solid-effect-atom" -import { heavyDataAtom } from "../atoms" - -export function HeavyComponent() { - const [shouldHydrate, setShouldHydrate] = createSignal(false) - - // Only hydrate when component becomes visible - onMount(() => { - if (!isSSR()) { - const observer = new IntersectionObserver((entries) => { - if (entries[0].isIntersecting) { - setShouldHydrate(true) - observer.disconnect() - } - }) - - const element = document.getElementById("heavy-component") - if (element) observer.observe(element) - } - }) - - return ( -
    - {shouldHydrate() ? :
    Loading heavy content...
    } -
    - ) -} - -function HydratedContent() { - const data = useAtomValue(() => heavyDataAtom) - - return ( -
    - {/* Heavy content here */} -
    {JSON.stringify(data(), null, 2)}
    -
    - ) -} -``` - -### 3. Error Boundaries for SSR - -```tsx -// components/SSRErrorBoundary.tsx -import { ErrorBoundary } from "solid-js" -import { isSSR } from "@effectify/solid-effect-atom" - -export function SSRErrorBoundary(props: { children: any }) { - return ( - { - // Different error handling for SSR vs client - if (isSSR()) { - console.error("SSR Error:", error) - return
    Server error occurred
    - } - - return ( -
    -

    Something went wrong

    -

    {error.message}

    - -
    - ) - }} - > - {props.children} -
    - ) -} -``` - -## Best Practices - -### 1. Critical vs Non-Critical Data - -```tsx -// Identify critical atoms that should be SSR'd -const criticalAtoms = [ - userAtom, - navigationAtom, - themeAtom, -] - -// Non-critical atoms can load after hydration -const nonCriticalAtoms = [ - analyticsAtom, - recommendationsAtom, - adsAtom, -] -``` - -### 2. Timeout Handling - -```tsx -// Configure appropriate timeouts for different data types -const ssrConfig = { - critical: { timeout: 2000 }, - normal: { timeout: 5000 }, - optional: { timeout: 1000 }, -} -``` - -### 3. Error Recovery - -```tsx -// Graceful degradation for SSR failures -export const resilientDataAtom = Atom.fn(() => - Effect.gen(function*() { - const data = yield* Effect.tryPromise(() => fetch("/api/data").then((r) => r.json())).pipe( - Effect.timeout("3000ms"), - Effect.catchAll(() => Effect.succeed({ fallback: true, message: "Using fallback data" })), - ) - return data - }) -) -``` - -### 4. Performance Monitoring - -```tsx -// Monitor SSR performance -export async function renderWithMetrics(url: string) { - const startTime = Date.now() - - const result = await renderApp(url) - - const metrics = { - renderTime: Date.now() - startTime, - atomsPreloaded: result.state.length, - errors: result.errors.length, - timeouts: result.timeouts.length, - } - - console.log("SSR Metrics:", metrics) - - return { ...result, metrics } -} -``` - -## Troubleshooting - -### Common Issues - -1. **Hydration Mismatches**: Ensure server and client render the same content -2. **Memory Leaks**: Use appropriate timeouts and cleanup in SSR context -3. **Performance**: Don't preload too many atoms on the server -4. **Error Handling**: Always provide fallbacks for failed atom loads - -### Debug Tools - -```tsx -// Debug SSR state -export function debugSSRState(registry: Registry) { - const nodes = registry.getNodes() - console.log("SSR Registry State:", { - atomCount: nodes.size, - atoms: Array.from(nodes.entries()).map(([atom, node]) => ({ - key: node.key, - hasValue: registry.has(atom), - })), - }) -} -``` - -This completes the comprehensive SSR guide for `@effectify/solid-effect-atom`! diff --git a/packages/solid/effect-atom/docs/troubleshooting.md b/packages/solid/effect-atom/docs/troubleshooting.md deleted file mode 100644 index dab8c365..00000000 --- a/packages/solid/effect-atom/docs/troubleshooting.md +++ /dev/null @@ -1,407 +0,0 @@ ---- -title: "Troubleshooting" -parent: "@effectify/solid-effect-atom" -nav_order: 6 ---- - -# Troubleshooting - -Common problems and solutions when using `@effectify/solid-effect-atom`. - -## Table of Contents - -- [Installation Issues](#installation-issues) -- [TypeScript Errors](#typescript-errors) -- [Runtime Errors](#runtime-errors) -- [Performance Issues](#performance-issues) -- [SolidJS Specific Issues](#solidjs-specific-issues) - -## Installation Issues - -### Peer Dependency Warnings - -**Problem**: Warnings about peer dependencies during installation. - -```bash -npm WARN peer dep missing: solid-js@^1.8.0, required by @effectify/solid-effect-atom -``` - -**Solution**: Install the required peer dependencies: - -```bash -pnpm add solid-js effect -# or -npm install solid-js effect -``` - -### Version Conflicts - -**Problem**: Conflicting versions of Effect or SolidJS. - -**Solution**: Check your package.json and ensure compatible versions: - -```json -{ - "dependencies": { - "solid-js": "^1.8.0", - "effect": "^3.0.0", - "@effectify/solid-effect-atom": "^0.1.0" - } -} -``` - -## TypeScript Errors - -### Generic Type Inference Issues - -**Problem**: TypeScript can't infer atom types correctly. - -```tsx -// ❌ Error: Type 'unknown' is not assignable to type 'string' -const atom = Atom.make() -const [value, setValue] = useAtom(() => atom) -setValue("hello") // Error here -``` - -**Solution**: Provide explicit types: - -```tsx -// ✅ Explicit type annotation -const atom = Atom.make("initial") -const [value, setValue] = useAtom(() => atom) -setValue("hello") // Works! - -// ✅ Or use type assertion -const atom = Atom.make("initial") // Inferred as Atom -``` - -### Effect Type Errors - -**Problem**: Complex Effect types causing TypeScript errors. - -```tsx -// ❌ Complex Effect type -const complexAtom = Atom.fn(() => - Effect.gen(function*() { - const a = yield* Effect.succeed(1) - const b = yield* Effect.fail("error") - return a + b - }) -) -``` - -**Solution**: Simplify or add explicit types: - -```tsx -// ✅ Explicit return type -const complexAtom = Atom.fn((): Effect.Effect => - Effect.gen(function*() { - const a = yield* Effect.succeed(1) - const b = yield* Effect.fail("error") - return a + b - }) -) -``` - -### Module Resolution Issues - -**Problem**: TypeScript can't find module declarations. - -```tsx -// ❌ Cannot find module '@effectify/solid-effect-atom' -import { Atom } from "@effectify/solid-effect-atom" -``` - -**Solution**: Check your tsconfig.json: - -```json -{ - "compilerOptions": { - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "preserve", - "jsxImportSource": "solid-js" - } -} -``` - -## Runtime Errors - -### "Atom is not defined" Error - -**Problem**: Atom is undefined at runtime. - -```tsx -// ❌ Atom created inside component -function MyComponent() { - const atom = Atom.make(0) // Creates new atom on every render - // ... -} -``` - -**Solution**: Create atoms outside components: - -```tsx -// ✅ Atom created outside component -const counterAtom = Atom.make(0) - -function MyComponent() { - const [count, setCount] = useAtom(() => counterAtom) - // ... -} -``` - -### Registry Not Found Error - -**Problem**: Using hooks outside of RegistryProvider. - -```tsx -// ❌ No RegistryProvider -function App() { - const value = useAtomValue(() => myAtom) // Error! - return
    {value()}
    -} -``` - -**Solution**: Wrap your app with RegistryProvider: - -```tsx -// ✅ With RegistryProvider -function App() { - return ( - - - - ) -} - -function MyComponent() { - const value = useAtomValue(() => myAtom) // Works! - return
    {value()}
    -} -``` - -### Circular Dependency Error - -**Problem**: Atoms depend on each other circularly. - -```tsx -// ❌ Circular dependency -const atomA = Atom.make((get) => get(atomB) + 1) -const atomB = Atom.make((get) => get(atomA) + 1) // Error! -``` - -**Solution**: Restructure dependencies: - -```tsx -// ✅ Use a base atom -const baseAtom = Atom.make(0) -const atomA = Atom.make((get) => get(baseAtom) + 1) -const atomB = Atom.make((get) => get(baseAtom) + 2) - -// ✅ Or use a derived pattern -const configAtom = Atom.make({ a: 1, b: 2 }) -const atomA = Atom.make((get) => get(configAtom).a) -const atomB = Atom.make((get) => get(configAtom).b) -``` - -## Performance Issues - -### Slow Rendering - -**Problem**: UI updates are slow or janky. - -**Diagnosis**: Check for expensive computations in atoms: - -```tsx -// ❌ Expensive computation on every access -const expensiveAtom = Atom.make((get) => { - const data = get(dataAtom) - return data.map((item) => heavyComputation(item)) // Slow! -}) -``` - -**Solution**: Optimize computations: - -```tsx -// ✅ Memoize expensive computations -const expensiveAtom = Atom.make((get) => { - const data = get(dataAtom) - return data.map((item) => memoizedHeavyComputation(item)) -}) - -// ✅ Or split into smaller atoms -const processedDataAtom = Atom.make((get) => { - const data = get(dataAtom) - return data.map(processItem) -}) -``` - -### Memory Leaks - -**Problem**: Memory usage keeps growing. - -**Diagnosis**: Check for atoms that aren't being cleaned up: - -```tsx -// ❌ keepAlive atoms without cleanup -const persistentAtom = Atom.make(data).pipe(Atom.keepAlive) -``` - -**Solution**: Proper cleanup: - -```tsx -// ✅ Auto-dispose atoms (default behavior) -const autoAtom = Atom.make(data) // Cleaned up when not used - -// ✅ Manual cleanup for keepAlive atoms -onCleanup(() => { - registry.dispose() -}) -``` - -### Too Many Re-renders - -**Problem**: Components re-render too frequently. - -**Diagnosis**: Check atom dependencies: - -```tsx -// ❌ Atom depends on frequently changing data -const derivedAtom = Atom.make((get) => { - const timestamp = get(timestampAtom) // Updates every second - const data = get(dataAtom) - return { data, timestamp } -}) -``` - -**Solution**: Split dependencies: - -```tsx -// ✅ Separate concerns -const dataAtom = Atom.make(initialData) -const timestampAtom = Atom.make(Date.now()) - -// Only subscribe to what you need -function MyComponent() { - const data = useAtomValue(() => dataAtom) // Only updates when data changes - return
    {JSON.stringify(data())}
    -} -``` - -## SolidJS Specific Issues - -### Signal vs Atom Confusion - -**Problem**: Mixing SolidJS signals with atoms incorrectly. - -```tsx -// ❌ Don't mix signals and atoms directly -const [signal, setSignal] = createSignal(0) -const atom = Atom.make((get) => signal()) // Won't react to signal changes -``` - -**Solution**: Use atoms consistently: - -```tsx -// ✅ Use atoms for shared state -const atom = Atom.make(0) -const [value, setValue] = useAtom(() => atom) - -// ✅ Or convert signals to atoms if needed -const signalAtom = Atom.make(() => signal()) -``` - -### SSR Hydration Issues - -**Problem**: Hydration mismatches between server and client. - -```tsx -// ❌ Different values on server vs client -const timeAtom = Atom.make(new Date().toISOString()) -``` - -**Solution**: Use SSR-safe patterns: - -```tsx -// ✅ SSR-safe atom -const timeAtom = Atom.make(() => { - if (typeof window === "undefined") { - return "SSR" // Server value - } - return new Date().toISOString() // Client value -}) - -// ✅ Or use SSR utilities -const ssrTimeAtom = createSSRAtom("SSR", clientTimeAtom) -``` - -### Suspense Boundary Issues - -**Problem**: Suspense boundaries not working with async atoms. - -```tsx -// ❌ Using regular useAtomValue with async atom -function MyComponent() { - const data = useAtomValue(() => asyncAtom) // May not suspend properly - return
    {data()}
    -} -``` - -**Solution**: Use proper suspense hooks: - -```tsx -// ✅ Use useAtomSuspense for async atoms -function MyComponent() { - const data = useAtomSuspense(() => asyncAtom) - return
    {data}
    -} - -// ✅ Or handle loading states manually -function MyComponent() { - const result = useAtomValue(() => asyncAtom) - - return ( -
    - {result()._tag === "Success" &&
    {result().value}
    } - {result()._tag === "Initial" &&
    Loading...
    } - {result()._tag === "Failure" &&
    Error occurred
    } -
    - ) -} -``` - -## Getting Help - -If you're still experiencing issues: - -1. **Check the documentation**: Review the [guides](./guides.md) and [examples](./examples.md) -2. **Search existing issues**: Look through [GitHub issues](https://github.com/effect-ts/atom/issues) -3. **Create a minimal reproduction**: Use [StackBlitz](https://stackblitz.com) or [CodeSandbox](https://codesandbox.io) -4. **Ask for help**: Open a new issue with your reproduction case - -## Common Patterns - -### Debugging Checklist - -When something isn't working: - -- [ ] Is the atom created outside the component? -- [ ] Is the component wrapped in RegistryProvider? -- [ ] Are you using the correct hook for your use case? -- [ ] Are there any circular dependencies? -- [ ] Is TypeScript configured correctly? -- [ ] Are peer dependencies installed? - -### Performance Checklist - -When performance is poor: - -- [ ] Are expensive computations memoized? -- [ ] Are atoms split appropriately? -- [ ] Are you avoiding unnecessary re-renders? -- [ ] Are keepAlive atoms cleaned up properly? -- [ ] Is the bundle size reasonable? diff --git a/packages/solid/effect-atom/package.json b/packages/solid/effect-atom/package.json index fb1a8514..964edcfe 100644 --- a/packages/solid/effect-atom/package.json +++ b/packages/solid/effect-atom/package.json @@ -1,6 +1,6 @@ { "name": "@effectify/solid-effect-atom", - "version": "0.2.1", + "version": "0.3.0", "description": "Reactive toolkit for Effect with SolidJS", "type": "module", "publishConfig": { diff --git a/packages/solid/effect-atom/src/advanced-hooks.ts b/packages/solid/effect-atom/src/advanced-hooks.ts deleted file mode 100644 index 718d51d6..00000000 --- a/packages/solid/effect-atom/src/advanced-hooks.ts +++ /dev/null @@ -1,163 +0,0 @@ -/** - * @since 1.0.0 - */ - -import type * as Atom from "@effect-atom/atom/Atom" -import type * as Result from "@effect-atom/atom/Result" -import * as Cause from "effect/Cause" -import { type Accessor, createEffect, createMemo, createResource, createSignal, onCleanup } from "solid-js" -import { useRegistry } from "./context.js" - -/** - * @since 1.0.0 - * @category hooks - */ -export const useAtomSuspense = ( - atomFactory: () => Atom.Atom>, - options?: { - readonly suspendOnWaiting?: boolean | undefined - readonly includeFailure?: IncludeFailure | undefined - }, -): Accessor | (IncludeFailure extends true ? Result.Failure : never)> => { - const registry = useRegistry() - const atomRef = createMemo(atomFactory) - - // Use createResource for proper SolidJS suspense integration - const [resource] = createResource( - () => ({ - atom: atomRef(), - suspendOnWaiting: options?.suspendOnWaiting ?? false, - includeFailure: options?.includeFailure ?? false, - }), - ({ atom, suspendOnWaiting }) => { - const current = registry.get(atom) - - // If should suspend on waiting and is waiting, wait for resolution - if (current._tag === "Initial" || (suspendOnWaiting && current.waiting)) { - return new Promise>((resolve) => { - const unsubscribe = registry.subscribe(atom, (result) => { - if (result._tag !== "Initial" && !(suspendOnWaiting && result.waiting)) { - unsubscribe() - resolve(result) - } - }) - }) - } - - return current - }, - ) - - // Return the resource directly as an accessor - return () => { - const result = resource() - - // If resource is undefined, it means we're still loading - // createResource handles suspense automatically - if (result === undefined) { - // Let createResource handle the suspense - we should not reach here - // if suspense is working correctly - const current = registry.get(atomRef()) - return current as any - } - - if (result._tag === "Failure" && !options?.includeFailure) { - throw Cause.squash(result.cause) - } - - return result as any - } -} - -/** - * @since 1.0.0 - * @category hooks - */ -export interface SuspenseResult { - readonly loading: boolean - readonly data: A | null - readonly error: Cause.Cause | null -} - -/** - * Alternative to useAtomSuspense that doesn't throw but returns loading state - * @since 1.0.0 - * @category hooks - */ -export const useAtomSuspenseResult = ( - atomFactory: () => Atom.Atom>, -): Accessor> => { - const registry = useRegistry() - const atomRef = createMemo(atomFactory) - const [value, setValue] = createSignal(registry.get(atomRef())) - - createEffect(() => { - const atom = atomRef() - const unsubscribe = registry.subscribe(atom, (newValue) => { - setValue(() => newValue) - }) - onCleanup(unsubscribe) - }) - - return createMemo(() => { - const current = value() - switch (current._tag) { - case "Initial": - return { loading: true, data: null, error: null } - case "Success": - return { loading: current.waiting, data: current.value, error: null } - case "Failure": - return { loading: current.waiting, data: null, error: current.cause } - default: - return { loading: true, data: null, error: null } - } - }) -} - -/** - * @since 1.0.0 - * @category hooks - */ -export const useAtomError = ( - atomFactory: () => Atom.Atom>, -): Accessor | null> => { - const registry = useRegistry() - const atomRef = createMemo(atomFactory) - const [value, setValue] = createSignal(registry.get(atomRef())) - - createEffect(() => { - const atom = atomRef() - const unsubscribe = registry.subscribe(atom, (newValue) => { - setValue(() => newValue) - }) - onCleanup(unsubscribe) - }) - - return createMemo(() => { - const current = value() - return current._tag === "Failure" ? current.cause : null - }) -} - -/** - * @since 1.0.0 - * @category hooks - */ -export const useAtomLoading = (atomFactory: () => Atom.Atom>): Accessor => { - const registry = useRegistry() - const atomRef = createMemo(atomFactory) - const [value, setValue] = createSignal(registry.get(atomRef())) - - createEffect(() => { - const atom = atomRef() - const unsubscribe = registry.subscribe(atom, (newValue) => { - setValue(() => newValue) - }) - onCleanup(unsubscribe) - }) - - return createMemo(() => { - const current = value() - return current._tag === "Initial" || current.waiting - }) -} diff --git a/packages/solid/effect-atom/src/context.ts b/packages/solid/effect-atom/src/context.ts deleted file mode 100644 index 1a243d4d..00000000 --- a/packages/solid/effect-atom/src/context.ts +++ /dev/null @@ -1,81 +0,0 @@ -/** - * @since 1.0.0 - */ - -import type * as Atom from "@effect-atom/atom/Atom" -import * as Registry from "@effect-atom/atom/Registry" -import { globalValue } from "effect/GlobalValue" -import { createContext, type JSX, useContext } from "solid-js" - -/** - * @since 1.0.0 - * @category context - */ -export function scheduleTask(f: () => void): void { - setTimeout(f, 0) -} - -/** - * @since 1.0.0 - * @category context - */ -export const RegistryContext = createContext() - -/** - * @since 1.0.0 - * @category context - */ -export const defaultRegistry: Registry.Registry = globalValue( - "@effect-atom/atom-solid/defaultRegistry", - () => - Registry.make({ - scheduleTask, - defaultIdleTTL: 400, - }), -) - -/** - * @since 1.0.0 - * @category context - */ -export const useRegistry = (): Registry.Registry => { - const registry = useContext(RegistryContext) - if (registry === undefined) { - return defaultRegistry - } - return registry -} - -/** - * @since 1.0.0 - * @category context - */ -export interface RegistryProviderProps { - readonly children?: any - readonly registry?: Registry.Registry - readonly initialValues?: Iterable, any]> - readonly scheduleTask?: (f: () => void) => void - readonly timeoutResolution?: number - readonly defaultIdleTTL?: number -} - -/** - * @since 1.0.0 - * @category context - */ -export const RegistryProvider = (props: RegistryProviderProps): JSX.Element => { - const registry = props.registry ?? - Registry.make({ - scheduleTask: props.scheduleTask ?? scheduleTask, - initialValues: props.initialValues, - timeoutResolution: props.timeoutResolution, - defaultIdleTTL: props.defaultIdleTTL ?? 400, - }) - - return RegistryContext.Provider({ - value: registry, - get children() { - return props.children - }, - }) -} diff --git a/packages/solid/effect-atom/src/hooks.ts b/packages/solid/effect-atom/src/hooks.ts new file mode 100644 index 00000000..633792ed --- /dev/null +++ b/packages/solid/effect-atom/src/hooks.ts @@ -0,0 +1,214 @@ +/** + * @since 0.3.0 + */ +import * as Cause from "effect/Cause" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import type * as Result from "@effect-atom/atom/Result" +import * as Atom from "@effect-atom/atom/Atom" +import type * as AtomRef from "@effect-atom/atom/AtomRef" +import * as AtomRegistry from "@effect-atom/atom/Registry" +import type { Accessor } from "solid-js" +import { createSignal, onCleanup, useContext } from "solid-js" +import { RegistryContext } from "./registry-context.js" + +const initialValuesSet = new WeakMap>>() + +/** + * @since 0.3.0 + * @category hooks + */ +export const useAtomInitialValues = (initialValues: Iterable, any]>): void => { + const registry = useContext(RegistryContext) + if (!registry) return + let set = initialValuesSet.get(registry) + if (set === undefined) { + set = new WeakSet() + initialValuesSet.set(registry, set) + } + for (const [atom, value] of initialValues) { + if (!set.has(atom)) { + set.add(atom) + ;(registry as any).set(atom, value) + } + } +} + +/** + * @since 0.3.0 + * @category hooks + */ +export const useAtomValue: { +
    (atom: Atom.Atom): Accessor + (atom: Atom.Atom, f: (_: A) => B): Accessor +} = (atom: Atom.Atom, f?: (_: A) => A): Accessor => { + const registry = useContext(RegistryContext) + if (!registry) { + throw new Error("RegistryContext not found. Ensure you are using RegistryProvider.") + } + return createAtomAccessor(registry, f ? Atom.map(atom, f) : atom) +} + +function createAtomAccessor(registry: AtomRegistry.Registry, atom: Atom.Atom): Accessor { + const [value, setValue] = createSignal(registry.get(atom)) + onCleanup(registry.subscribe(atom, setValue as any)) + return value +} + +function mountAtom(registry: AtomRegistry.Registry, atom: Atom.Atom): void { + onCleanup(registry.mount(atom)) +} + +function setAtom( + registry: AtomRegistry.Registry, + atom: Atom.Writable, + options?: { + readonly mode?: ([R] extends [Result.Result] ? Mode : "value") | undefined + }, +): "promise" extends Mode ? ( + (value: W) => Promise> + ) : + "promiseExit" extends Mode ? ( + (value: W) => Promise, Result.Result.Failure>> + ) : + ((value: W | ((value: R) => W)) => void) +{ + if (options?.mode === "promise" || options?.mode === "promiseExit") { + return ((value: W) => { + registry.set(atom, value) + const promise = Effect.runPromiseExit( + AtomRegistry.getResult(registry, atom as Atom.Atom>, { + suspendOnWaiting: true, + }), + ) + return options!.mode === "promise" ? promise.then(flattenExit) : promise + }) as any + } + return ((value: W | ((value: R) => W)) => { + registry.set(atom, typeof value === "function" ? (value as any)(registry.get(atom)) : value) + }) as any +} + +const flattenExit = (exit: Exit.Exit): A => { + if (Exit.isSuccess(exit)) return exit.value + throw Cause.squash(exit.cause) +} + +/** + * @since 0.3.0 + * @category hooks + */ +export const useAtomMount = (atom: Atom.Atom): void => { + const registry = useContext(RegistryContext) + if (!registry) return + mountAtom(registry, atom) +} + +/** + * @since 0.3.0 + * @category hooks + */ +export const useAtomSet = < + R, + W, + Mode extends "value" | "promise" | "promiseExit" = never, +>( + atom: Atom.Writable, + options?: { + readonly mode?: ([R] extends [Result.Result] ? Mode : "value") | undefined + }, +): "promise" extends Mode ? ( + (value: W) => Promise> + ) : + "promiseExit" extends Mode ? ( + (value: W) => Promise, Result.Result.Failure>> + ) : + ((value: W | ((value: R) => W)) => void) => +{ + const registry = useContext(RegistryContext) + if (!registry) { + throw new Error("RegistryContext not found. Ensure you are using RegistryProvider.") + } + mountAtom(registry, atom) + return setAtom(registry, atom, options) +} + +/** + * @since 0.3.0 + * @category hooks + */ +export const useAtomRefresh = (atom: Atom.Atom): () => void => { + const registry = useContext(RegistryContext) + if (!registry) { + throw new Error("RegistryContext not found. Ensure you are using RegistryProvider.") + } + mountAtom(registry, atom) + return () => registry.refresh(atom) +} + +/** + * @since 0.3.0 + * @category hooks + */ +export const useAtom = ( + atom: Atom.Writable, + options?: { + readonly mode?: ([R] extends [Result.Result] ? Mode : "value") | undefined + }, +): readonly [ + value: Accessor, + write: "promise" extends Mode ? ( + (value: W) => Promise> + ) : + "promiseExit" extends Mode ? ( + (value: W) => Promise, Result.Result.Failure>> + ) : + ((value: W | ((value: R) => W)) => void), +] => { + const registry = useContext(RegistryContext) + if (!registry) { + throw new Error("RegistryContext not found. Ensure you are using RegistryProvider.") + } + return [ + createAtomAccessor(registry, atom), + setAtom(registry, atom, options), + ] as const +} + +/** + * @since 0.3.0 + * @category hooks + */ +export const useAtomSubscribe = ( + atom: Atom.Atom, + f: (_: A) => void, + options?: { readonly immediate?: boolean }, +): void => { + const registry = useContext(RegistryContext) + if (!registry) return + onCleanup(registry.subscribe(atom, f, options)) +} + +/** + * @since 0.3.0 + * @category hooks + */ +export const useAtomRef = (ref: AtomRef.ReadonlyRef): Accessor => { + const [value, setValue] = createSignal(ref.value) + onCleanup(ref.subscribe(setValue)) + return value +} + +/** + * @since 0.3.0 + * @category hooks + */ +export const useAtomRefProp = (ref: AtomRef.AtomRef, prop: K): AtomRef.AtomRef => + ref.prop(prop) + +/** + * @since 0.3.0 + * @category hooks + */ +export const useAtomRefPropValue = (ref: AtomRef.AtomRef, prop: K): Accessor => + useAtomRef(useAtomRefProp(ref, prop)) diff --git a/packages/solid/effect-atom/src/index.ts b/packages/solid/effect-atom/src/index.ts index ded619cd..b638a572 100644 --- a/packages/solid/effect-atom/src/index.ts +++ b/packages/solid/effect-atom/src/index.ts @@ -1,71 +1,13 @@ /** - * @since 1.0.0 - * @category re-exports + * @since 0.3.0 */ -export * as Atom from "@effect-atom/atom/Atom" /** - * @since 1.0.0 - * @category re-exports + * @since 0.3.0 */ -export * as AtomHttpApi from "@effect-atom/atom/AtomHttpApi" +export * from "./hooks.js" /** - * @since 1.0.0 - * @category re-exports + * @since 0.3.0 */ -export * as AtomRef from "@effect-atom/atom/AtomRef" - -/** - * @since 1.0.0 - * @category re-exports - */ -export * as AtomRpc from "@effect-atom/atom/AtomRpc" - -/** - * @since 1.0.0 - * @category re-exports - */ -export * as Hydration from "@effect-atom/atom/Hydration" - -/** - * @since 1.0.0 - * @category re-exports - */ -export * as Registry from "@effect-atom/atom/Registry" - -/** - * @since 1.0.0 - * @category re-exports - */ -export * as Result from "@effect-atom/atom/Result" - -/** - * @since 1.0.0 - * @category hooks - */ -export * from "./advanced-hooks.js" - -/** - * @since 1.0.0 - * @category context - */ -export * from "./context.js" - -/** - * @since 1.0.0 - * @category hooks - */ -export * from "./primitives.js" - -/** - * @since 1.0.0 - * @category components - */ -export * from "./solid-hydration.js" - -/** - * @since 1.0.0 - * @category SSR - */ -export * from "./ssr-utils.js" +export * from "./registry-context.js" diff --git a/packages/solid/effect-atom/src/primitives.ts b/packages/solid/effect-atom/src/primitives.ts deleted file mode 100644 index 396dce56..00000000 --- a/packages/solid/effect-atom/src/primitives.ts +++ /dev/null @@ -1,304 +0,0 @@ -/** - * @since 1.0.0 - */ - -import * as Atom from "@effect-atom/atom/Atom" -import type * as AtomRef from "@effect-atom/atom/AtomRef" -import * as Registry from "@effect-atom/atom/Registry" -import type * as Result from "@effect-atom/atom/Result" -import { Effect } from "effect" -import * as Cause from "effect/Cause" -import * as Exit from "effect/Exit" -import { globalValue } from "effect/GlobalValue" -import { type Accessor, batch, createEffect, createMemo, createSignal, onCleanup } from "solid-js" -import { useRegistry } from "./context.js" - -interface AtomStore { - readonly subscribe: (f: () => void) => () => void - readonly snapshot: () => A - readonly getServerSnapshot: () => A -} - -const storeRegistry = globalValue( - "@effect-atom/atom-solid/storeRegistry", - () => new WeakMap, AtomStore>>(), -) - -function makeStore(registry: Registry.Registry, atom: Atom.Atom): AtomStore { - let stores = storeRegistry.get(registry) - if (stores === undefined) { - stores = new WeakMap() - storeRegistry.set(registry, stores) - } - const store = stores.get(atom) - if (store !== undefined) { - return store - } - const newStore: AtomStore = { - subscribe(f) { - return registry.subscribe(atom, () => { - f() - }) - }, - snapshot() { - return registry.get(atom) - }, - getServerSnapshot() { - return Atom.getServerValue(atom, registry) - }, - } - stores.set(atom, newStore) - return newStore -} - -// Cache for store signals to avoid recreating them -const storeSignals = new WeakMap>() - -// Optimization: Track active subscriptions to enable lazy cleanup -const activeSubscriptions = new WeakMap void>>() - -function useStore(registry: Registry.Registry, atom: Atom.Atom): Accessor { - const store = makeStore(registry, atom) - - // Optimization: Reuse signals for the same atom to avoid unnecessary subscriptions - if (storeSignals.has(atom)) { - return storeSignals.get(atom)! - } - - const [value, setValue] = createSignal(store.snapshot()) - - createEffect(() => { - const unsubscribe = store.subscribe(() => { - const newValue = store.snapshot() - // Use batch to avoid unnecessary updates during rapid changes - batch(() => setValue(() => newValue)) - }) - - // Track active subscription for lazy cleanup - let subscriptions = activeSubscriptions.get(atom) - if (!subscriptions) { - subscriptions = new Set() - activeSubscriptions.set(atom, subscriptions) - } - subscriptions.add(unsubscribe) - - onCleanup(() => { - unsubscribe() - // Clean up tracking - const subs = activeSubscriptions.get(atom) - if (subs) { - subs.delete(unsubscribe) - if (subs.size === 0) { - activeSubscriptions.delete(atom) - storeSignals.delete(atom) - } - } - }) - }) - - // Cache the signal accessor - storeSignals.set(atom, value) - return value -} - -const initialValuesSet = globalValue( - "@effect-atom/atom-solid/initialValuesSet", - () => new WeakMap>>(), -) - -/** - * @since 1.0.0 - * @category hooks - */ -export const useAtomInitialValues = (initialValues: Iterable, any]>): void => { - const registry = useRegistry() - let set = initialValuesSet.get(registry) - if (set === undefined) { - set = new WeakSet() - initialValuesSet.set(registry, set) - } - for (const [atom, value] of initialValues) { - if (!set.has(atom)) { - set.add(atom) - ;(registry as any).ensureNode(atom).setValue(value) - } - } -} - -/** - * @since 1.0.0 - * @category hooks - */ -export const useAtomValue: { - (atomFactory: () => Atom.Atom): Accessor - (atomFactory: () => Atom.Atom, f: (_: A) => B): Accessor -} = (atomFactory: () => Atom.Atom, f?: (_: A) => A): Accessor => { - const registry = useRegistry() - const atomRef = createMemo(atomFactory) // Use createMemo like useAtom does - - // Mount the atom to ensure it's active and can receive updates - mountAtom(registry, atomRef()) - - if (f) { - const mappedAtomRef = createMemo(() => Atom.map(atomRef(), f)) - mountAtom(registry, mappedAtomRef()) - return useStore(registry, mappedAtomRef()) - } - - return useStore(registry, atomRef()) -} - -function mountAtom(registry: Registry.Registry, atom: Atom.Atom): void { - createEffect(() => { - const dispose = registry.mount(atom) - onCleanup(dispose) - }) -} - -function setAtom( - registry: Registry.Registry, - atom: Atom.Writable, - options?: { - readonly mode?: ([R] extends [Result.Result] ? Mode : "value") | undefined - }, -): "promise" extends Mode ? (value: W) => Promise> - : "promiseExit" extends Mode ? (value: W) => Promise, Result.Result.Failure>> - : (value: W | ((value: R) => W)) => void -{ - if (options?.mode === "promise" || options?.mode === "promiseExit") { - return ((value: W) => { - registry.set(atom, value) - const promise = Effect.runPromiseExit( - Registry.getResult(registry, atom as Atom.Atom>, { suspendOnWaiting: true }), - ) - return options!.mode === "promise" ? promise.then(flattenExit) : promise - }) as any - } - - return ((value: W | ((value: R) => W)) => { - registry.set(atom, typeof value === "function" ? (value as any)(registry.get(atom)) : value) - }) as any -} - -const flattenExit = (exit: Exit.Exit): A => { - if (Exit.isSuccess(exit)) { - return exit.value - } - throw Cause.squash(exit.cause) -} - -/** - * @since 1.0.0 - * @category hooks - */ -export const useAtomMount = (atomFactory: () => Atom.Atom): void => { - const registry = useRegistry() - const atomRef = createMemo(atomFactory) - mountAtom(registry, atomRef()) -} - -/** - * @since 1.0.0 - * @category hooks - */ -export const useAtomSet = ( - atomFactory: () => Atom.Writable, - options?: { - readonly mode?: ([R] extends [Result.Result] ? Mode : "value") | undefined - }, -): "promise" extends Mode ? (value: W) => Promise> - : "promiseExit" extends Mode ? (value: W) => Promise, Result.Result.Failure>> - : (value: W | ((value: R) => W)) => void => -{ - const registry = useRegistry() - const atomRef = createMemo(atomFactory) - mountAtom(registry, atomRef()) - return setAtom(registry, atomRef(), options) -} - -/** - * @since 1.0.0 - * @category hooks - */ -export const useAtomRefresh = (atomFactory: () => Atom.Atom): () => void => { - const registry = useRegistry() - const atomRef = createMemo(atomFactory) - mountAtom(registry, atomRef()) - return () => { - registry.refresh(atomRef()) - } -} - -/** - * @since 1.0.0 - * @category hooks - */ -export const useAtom = ( - atomFactory: () => Atom.Writable, - options?: { - readonly mode?: ([R] extends [Result.Result] ? Mode : "value") | undefined - }, -): readonly [ - value: Accessor, - write: "promise" extends Mode ? (value: W) => Promise> - : "promiseExit" extends Mode ? (value: W) => Promise, Result.Result.Failure>> - : (value: W | ((value: R) => W)) => void, -] => { - const registry = useRegistry() - const atomRef = createMemo(atomFactory) - return [useStore(registry, atomRef()), setAtom(registry, atomRef(), options)] as const -} - -/** - * @since 1.0.0 - * @category hooks - */ -export const useAtomSubscribe = ( - atomFactory: () => Atom.Atom, - f: (_: A) => void, - options?: { readonly immediate?: boolean }, -): void => { - const registry = useRegistry() - const atomRef = createMemo(atomFactory) - - createEffect(() => { - const atom = atomRef() - const unsubscribe = registry.subscribe(atom, f, options) - onCleanup(unsubscribe) - }) -} - -/** - * @since 1.0.0 - * @category hooks - */ -export const useAtomRef = (refFactory: () => AtomRef.ReadonlyRef): Accessor => { - const refRef = createMemo(refFactory) - const [value, setValue] = createSignal(refRef().value) - - createEffect(() => { - const ref = refRef() - const unsubscribe = ref.subscribe(setValue) - onCleanup(unsubscribe) - }) - - return value -} - -/** - * @since 1.0.0 - * @category hooks - */ -export const useAtomRefProp = ( - refFactory: () => AtomRef.AtomRef, - prop: K, -): () => AtomRef.AtomRef => createMemo(() => refFactory().prop(prop)) - -/** - * @since 1.0.0 - * @category hooks - */ -export const useAtomRefPropValue = ( - refFactory: () => AtomRef.AtomRef, - prop: K, -): Accessor => useAtomRef(() => useAtomRefProp(refFactory, prop)()) diff --git a/packages/solid/effect-atom/src/registry-context.ts b/packages/solid/effect-atom/src/registry-context.ts new file mode 100644 index 00000000..cd38b01e --- /dev/null +++ b/packages/solid/effect-atom/src/registry-context.ts @@ -0,0 +1,39 @@ +/** + * @since 0.3.0 + */ +import type * as Atom from "@effect-atom/atom/Atom" +import * as AtomRegistry from "@effect-atom/atom/Registry" +import type { JSX } from "solid-js" +import { createComponent, createContext, onCleanup } from "solid-js" + +/** + * @since 0.3.0 + * @category context + */ +export const RegistryContext = createContext(AtomRegistry.make()) + +/** + * @since 0.3.0 + * @category context + */ +export const RegistryProvider = (options: { + readonly children?: JSX.Element | undefined + readonly initialValues?: Iterable, any]> | undefined + readonly scheduleTask?: ((f: () => void) => () => void) | undefined + readonly timeoutResolution?: number | undefined + readonly defaultIdleTTL?: number | undefined +}) => { + const registry = AtomRegistry.make({ + scheduleTask: options.scheduleTask, + initialValues: options.initialValues, + timeoutResolution: options.timeoutResolution, + defaultIdleTTL: options.defaultIdleTTL, + }) + onCleanup(() => registry.dispose()) + return createComponent(RegistryContext.Provider, { + value: registry, + get children() { + return options.children + }, + }) +} diff --git a/packages/solid/effect-atom/src/solid-hydration.ts b/packages/solid/effect-atom/src/solid-hydration.ts deleted file mode 100644 index 6cc630b3..00000000 --- a/packages/solid/effect-atom/src/solid-hydration.ts +++ /dev/null @@ -1,122 +0,0 @@ -/** - * @since 1.0.0 - */ - -import * as Hydration from "@effect-atom/atom/Hydration" -import * as Result from "@effect-atom/atom/Result" -import { createEffect, createMemo, type JSX } from "solid-js" -import { useRegistry } from "./context.js" - -/** - * Check if hydration data is newer than the existing node value - * - * @since 1.0.0 - * @category utils - */ -function isHydrationDataNewer(existingNode: any, dehydratedAtom: Hydration.DehydratedAtomValue): boolean { - try { - const currentValue = existingNode.value() - - // If current value is a Success Result, compare timestamps - if (Result.isResult(currentValue) && Result.isSuccess(currentValue)) { - return dehydratedAtom.dehydratedAt > currentValue.timestamp - } - - // For Failure Results, check if there's a previousSuccess with timestamp - if (Result.isResult(currentValue) && Result.isFailure(currentValue)) { - const previousSuccess = currentValue.previousSuccess - if (previousSuccess._tag === "Some" && Result.isSuccess(previousSuccess.value)) { - return dehydratedAtom.dehydratedAt > previousSuccess.value.timestamp - } - } - - // For Initial Results or non-Result values, we can't determine age reliably - // Default to hydrating if the dehydrated data is recent (within last 5 minutes) - const fiveMinutesAgo = Date.now() - 5 * 60 * 1000 - return dehydratedAtom.dehydratedAt > fiveMinutesAgo - } catch { - // If we can't get the current value, default to hydrating - return true - } -} - -/** - * @since 1.0.0 - * @category components - */ -export interface HydrationBoundaryProps { - state?: Iterable - children?: JSX.Element -} - -/** - * @since 1.0.0 - * @category components - */ -export function HydrationBoundary(props: HydrationBoundaryProps) { - const registry = useRegistry() - - // This createMemo is for performance reasons only, everything inside it must - // be safe to run in every render and code here should be read as "in render". - // - // This code needs to happen during the render phase, because after initial - // SSR, hydration needs to happen _before_ children render. Also, if hydrating - // during a transition, we want to hydrate as much as is safe in render so - // we can prerender as much as possible. - // - // For any Atom values that already exist in the registry, we want to hold back on - // hydrating until _after_ the render phase. The reason for this is that during - // transitions, we don't want the existing Atom values and subscribers to update to - // the new data on the current page, only _after_ the transition is committed. - // If the transition is aborted, we will have hydrated any _new_ Atom values, but - // we throw away the fresh data for any existing ones to avoid unexpectedly - // updating the UI. - const hydrationQueue = createMemo(() => { - const state = props.state - if (state) { - const dehydratedAtoms = Hydration.toValues(Array.from(state)) - const nodes = registry.getNodes() - - const newDehydratedAtoms: Array = [] - const existingDehydratedAtoms: Array = [] - - for (const dehydratedAtom of dehydratedAtoms) { - const existingNode = nodes.get(dehydratedAtom.key) - - if (existingNode) { - // This Atom value already exists, check if hydration data is newer - const shouldHydrate = isHydrationDataNewer(existingNode, dehydratedAtom) - - if (shouldHydrate) { - // Hydration data is newer, queue it for later hydration - existingDehydratedAtoms.push(dehydratedAtom) - } - // If hydration data is older or same age, ignore it - } else { - // This is a new Atom value, safe to hydrate immediately - newDehydratedAtoms.push(dehydratedAtom) - } - } - - if (newDehydratedAtoms.length > 0) { - // It's actually fine to call this with state that already exists - // in the registry, or is older. hydrate() is idempotent. - Hydration.hydrate(registry, newDehydratedAtoms) - } - - if (existingDehydratedAtoms.length > 0) { - return existingDehydratedAtoms - } - } - return - }) - - createEffect(() => { - const queue = hydrationQueue() - if (queue) { - Hydration.hydrate(registry, queue) - } - }) - - return props.children -} diff --git a/packages/solid/effect-atom/src/ssr-utils.ts b/packages/solid/effect-atom/src/ssr-utils.ts deleted file mode 100644 index 358a2be9..00000000 --- a/packages/solid/effect-atom/src/ssr-utils.ts +++ /dev/null @@ -1,331 +0,0 @@ -/** - * @since 1.0.0 - */ - -import * as Atom from "@effect-atom/atom/Atom" -import * as Hydration from "@effect-atom/atom/Hydration" -import * as Registry from "@effect-atom/atom/Registry" -import * as Effect from "effect/Effect" - -// Extend the Window interface to include our hydration flag -declare global { - interface Window { - __ATOM_SOLID_HYDRATED__?: boolean - } -} - -/** - * Helper function to get a key from an atom for debugging/logging purposes - */ -const getAtomKey = (atom: Atom.Atom): string => { - if (Atom.isSerializable(atom)) { - return atom[Atom.SerializableTypeId].key - } - return atom.toString() -} - -/** - * @since 1.0.0 - * @category SSR - */ -export interface SSROptions { - /** - * Timeout for async atom resolution during SSR - * @default 5000 - */ - timeout?: number - - /** - * Whether to include error states in dehydrated data - * @default false - */ - includeErrors?: boolean - - /** - * Custom scheduler for SSR context - */ - scheduler?: (f: () => void) => void -} - -/** - * @since 1.0.0 - * @category SSR - */ -export interface SSRResult { - /** - * Dehydrated atom state for client hydration - */ - dehydratedState: Hydration.DehydratedAtom[] - - /** - * Any errors that occurred during SSR - */ - errors: Array<{ atomKey: string; error: unknown }> - - /** - * Atoms that timed out during SSR - */ - timeouts: string[] -} - -/** - * Create a registry optimized for SSR - * - * @since 1.0.0 - * @category SSR - */ -export const createSSRRegistry = (options: SSROptions = {}): Registry.Registry => { - const { scheduler = (f) => f() } = options - - return Registry.make({ - scheduleTask: scheduler, - defaultIdleTTL: Number.POSITIVE_INFINITY, // Don't cleanup during SSR - }) -} - -/** - * Preload atoms during SSR - * - * @since 1.0.0 - * @category SSR - */ -export const preloadAtoms = ( - registry: Registry.Registry, - atoms: Atom.Atom[], - options: SSROptions = {}, -): Effect.Effect => - Effect.gen(function*() { - const { includeErrors = false, timeout = 5000 } = options - const errors: Array<{ atomKey: string; error: unknown }> = [] - const timeouts: string[] = [] - - // Preload all atoms with timeout - const preloadEffects = atoms.map((atom) => - Effect.gen(function*() { - try { - yield* Effect.sync(() => registry.get(atom)) - } catch (error) { - const atomKey = getAtomKey(atom) - errors.push({ atomKey, error }) - } - }).pipe( - Effect.timeout(`${timeout} millis`), - Effect.catchAll((error) => - Effect.sync(() => { - const atomKey = getAtomKey(atom) - if (error._tag === "TimeoutException") { - timeouts.push(atomKey) - } else { - errors.push({ atomKey, error }) - } - }) - ), - ) - ) - - // Wait for all preloads to complete or timeout - yield* Effect.all(preloadEffects, { concurrency: "unbounded" }) - - // Dehydrate the registry state - const dehydratedStateRaw = Array.from(Hydration.dehydrate(registry)) - const dehydratedStateValues = Hydration.toValues(dehydratedStateRaw) - - // Filter out errors if not including them - const finalDehydratedState = includeErrors - ? dehydratedStateValues - : dehydratedStateValues.filter((atom) => !errors.some((error) => error.atomKey === atom.key)) - - return { - dehydratedState: finalDehydratedState, - errors, - timeouts, - } - }) - -/** - * Render atoms to static values for SSR - * - * @since 1.0.0 - * @category SSR - */ -export const renderAtomsStatic = (registry: Registry.Registry, atoms: Atom.Atom[]): Record => { - const staticValues: Record = {} - - for (const atom of atoms) { - try { - const nodes = registry.getNodes() - const node = nodes.get(atom) - if (node) { - staticValues[getAtomKey(atom)] = registry.get(atom) - } - } catch { - // Ignore errors for static rendering - } - } - - return staticValues -} - -/** - * Create a server-side registry with initial data - * - * @since 1.0.0 - * @category SSR - */ -export const createServerRegistry = ( - initialData?: Record, - options: SSROptions = {}, -): Registry.Registry => { - const registry = createSSRRegistry(options) - - if (initialData) { - // Hydrate with initial data - const dehydratedAtoms = Object.entries(initialData).map(([key, value]) => ({ - key, - value, - dehydratedAt: Date.now(), - })) as unknown as Array - - Hydration.hydrate(registry, dehydratedAtoms) - } - - return registry -} - -/** - * Extract critical atoms that should be preloaded for SSR - * - * @since 1.0.0 - * @category SSR - */ -export const extractCriticalAtoms = (registry: Registry.Registry, criticalKeys: string[]): Atom.Atom[] => { - const nodes = registry.getNodes() - const criticalAtoms: Atom.Atom[] = [] - - for (const [atomOrKey, _node] of nodes.entries()) { - // atomOrKey can be either an Atom or a string (serializable key) - if (typeof atomOrKey === "string") { - // If it's already a string key, check directly - if (criticalKeys.includes(atomOrKey)) { - // We need to get the actual atom from the node - criticalAtoms.push(_node.atom) - } - } else if (criticalKeys.includes(getAtomKey(atomOrKey))) { - // If it's an atom, get its key and check - criticalAtoms.push(atomOrKey) - } - } - - return criticalAtoms -} - -/** - * Serialize dehydrated state for client - * - * @since 1.0.0 - * @category SSR - */ -export const serializeState = (dehydratedState: Hydration.DehydratedAtom[]): string => { - try { - return JSON.stringify(dehydratedState) - } catch { - return "[]" - } -} - -/** - * Deserialize state on client - * - * @since 1.0.0 - * @category SSR - */ -export const deserializeState = (serializedState: string): Hydration.DehydratedAtom[] => { - try { - return JSON.parse(serializedState) - } catch { - return [] - } -} - -/** - * Create a SSR-safe atom that provides fallback values - * - * @since 1.0.0 - * @category SSR - */ -export const createSSRAtom = (serverValue: T, clientAtom: Atom.Atom): Atom.Atom => { - return Atom.make((get) => { - // During SSR, return server value - if (typeof window === "undefined") { - return serverValue - } - - // On client, use the actual atom - return get(clientAtom) - }) -} - -/** - * Check if we're in SSR context - * - * @since 1.0.0 - * @category SSR - */ -export const isSSR = (): boolean => { - return typeof window === "undefined" -} - -/** - * Check if we're in hydration phase - * - * @since 1.0.0 - * @category SSR - */ -export const isHydrating = (): boolean => { - return typeof window !== "undefined" && !window.__ATOM_SOLID_HYDRATED__ -} - -/** - * Mark hydration as complete - * - * @since 1.0.0 - * @category SSR - */ -export const markHydrationComplete = (): void => { - if (typeof window !== "undefined") { - ;(window as any).__ATOM_SOLID_HYDRATED__ = true - } -} - -/** - * SSR-safe effect that only runs on client - * - * @since 1.0.0 - * @category SSR - */ -export const clientOnlyEffect = (effect: Effect.Effect): Effect.Effect => { - return Effect.gen(function*() { - if (isSSR()) { - return null - } - return yield* effect - }) -} - -/** - * Create an atom that behaves differently on server vs client - * - * @since 1.0.0 - * @category SSR - */ -export const createIsomorphicAtom = ( - serverFactory: () => Atom.Atom, - clientFactory: () => Atom.Atom, -): Atom.Atom => { - return Atom.make((get) => { - if (isSSR()) { - return get(serverFactory()) - } - return get(clientFactory()) - }) -} diff --git a/packages/solid/effect-atom/test/index.test.tsx b/packages/solid/effect-atom/test/index.test.tsx index d3ed9c99..72fe2f0c 100644 --- a/packages/solid/effect-atom/test/index.test.tsx +++ b/packages/solid/effect-atom/test/index.test.tsx @@ -1,648 +1,285 @@ -import * as Atom from "@effect-atom/atom/Atom" -import * as Registry from "@effect-atom/atom/Registry" -import { fireEvent, render, screen, waitFor } from "@solidjs/testing-library" -import { Schema } from "effect" -import * as Effect from "effect/Effect" -import { createResource, ErrorBoundary, Suspense } from "solid-js" -import { beforeEach, describe, expect, test, vi } from "vitest" import { - Hydration, - HydrationBoundary, - RegistryProvider, - Result, + RegistryContext, useAtom, + useAtomInitialValues, + useAtomMount, + useAtomRef, + useAtomRefProp, + useAtomRefPropValue, + useAtomRefresh, useAtomSet, useAtomSubscribe, - useAtomSuspense, - useAtomSuspenseResult, useAtomValue, } from "../src/index.js" +import { assert, describe, it } from "vitest" +import * as Atom from "@effect-atom/atom/Atom" +import * as AtomRef from "@effect-atom/atom/AtomRef" +import * as Registry from "@effect-atom/atom/Registry" +import { type Accessor, createComponent, createEffect, createRoot } from "solid-js" describe("atom-solid", () => { - let registry: Registry.Registry - - beforeEach(() => { - registry = Registry.make() - }) - describe("useAtomValue", () => { - test("should read value from simple Atom", () => { + it("reads value from simple Atom", () => { const atom = Atom.make(42) - - function TestComponent() { - const value = useAtomValue(() => atom) - return
    {value()}
    - } - - render(() => ) - - expect(screen.getByTestId("value")).toHaveTextContent("42") + let observed: number | undefined + const dispose = renderAtomValue(atom, (value) => { + observed = value + }) + assert.strictEqual(observed, 42) + dispose() }) - test("should read value with transform function", () => { + it("reads value with transform function", () => { const atom = Atom.make(42) - - function TestComponent() { - const value = useAtomValue( - () => atom, - (x) => x * 2, - ) - return
    {value()}
    - } - - render(() => ) - - expect(screen.getByTestId("value")).toHaveTextContent("84") + let observed: number | undefined + const dispose = renderAtomValue(atom, (value) => { + observed = value + }, { map: (value) => value * 2 }) + assert.strictEqual(observed, 84) + dispose() }) - test("should update when Atom value changes", async () => { + it("updates when Atom value changes", () => { + const registry = Registry.make() const atom = Atom.make("initial") - - function TestComponent() { - const value = useAtomValue(() => atom) - return
    {value()}
    - } - - render(() => ( - - - - )) - - expect(screen.getByTestId("value")).toHaveTextContent("initial") - + let observed: string | undefined + const dispose = renderAtomValue(atom, (value) => { + observed = value + }, { registry }) + assert.strictEqual(observed, "initial") registry.set(atom, "updated") - - await waitFor(() => { - expect(screen.getByTestId("value")).toHaveTextContent("updated") - }) + assert.strictEqual(observed, "updated") + dispose() }) - test("should work like Counter example - with useAtom and useAtomValue", async () => { - const baseAtom = Atom.make(5) - const doubledAtom = Atom.make((get) => get(baseAtom) * 2) - - function TestComponent() { - const [count, setCount] = useAtom(() => baseAtom) - const doubled = useAtomValue(() => doubledAtom) - return ( -
    -
    {count()}
    -
    {doubled()}
    - -
    - ) - } - - render(() => ( - - - - )) - - expect(screen.getByTestId("count")).toHaveTextContent("5") - expect(screen.getByTestId("doubled")).toHaveTextContent("10") - - fireEvent.click(screen.getByTestId("increment")) - - await waitFor(() => { - expect(screen.getByTestId("count")).toHaveTextContent("6") - expect(screen.getByTestId("doubled")).toHaveTextContent("12") - }) - }) - - test("should work with computed Atom", () => { + it("works with computed Atom", () => { const baseAtom = Atom.make(10) const computedAtom = Atom.make((get) => get(baseAtom) * 2) - - function TestComponent() { - const value = useAtomValue(() => computedAtom) - return
    {value()}
    - } - - render(() => ) - - expect(screen.getByTestId("value")).toHaveTextContent("20") + let observed: number | undefined + const dispose = renderAtomValue(computedAtom, (value) => { + observed = value + }) + assert.strictEqual(observed, 20) + dispose() }) }) describe("useAtom", () => { - test("should provide both value and setter", async () => { + it("updates value with setter", () => { const atom = Atom.make(0) - - function TestComponent() { - const [count, setCount] = useAtom(() => atom) - - return ( -
    -
    {count()}
    - -
    - ) - } - - render(() => ) - - expect(screen.getByTestId("value")).toHaveTextContent("0") - - fireEvent.click(screen.getByTestId("increment")) - - await waitFor(() => { - expect(screen.getByTestId("value")).toHaveTextContent("1") - }) - }) - - test("should work with function updater", async () => { - const atom = Atom.make(5) - - function TestComponent() { - const [count, setCount] = useAtom(() => atom) - - return ( -
    -
    {count()}
    - -
    - ) - } - - render(() => ) - - expect(screen.getByTestId("value")).toHaveTextContent("5") - - fireEvent.click(screen.getByTestId("double")) - - await waitFor(() => { - expect(screen.getByTestId("value")).toHaveTextContent("10") + let observed: number | undefined + const dispose = createRoot((dispose) => { + const [value, setValue] = useAtom(atom) + createEffect(() => { + observed = value() + }) + createEffect(() => { + if (value() !== 0) { + return + } + setValue(1) + setValue((current) => current + 1) + }) + return dispose }) + assert.strictEqual(observed, 2) + dispose() }) }) describe("useAtomSet", () => { - test("should provide only setter function", async () => { + it("updates atom value without subscribing", () => { + const registry = Registry.make() const atom = Atom.make(0) - function TestComponent() { - const value = useAtomValue(() => atom) - const setValue = useAtomSet(() => atom) - - return ( -
    -
    {value()}
    - -
    - ) - } - - render(() => ) - - expect(screen.getByTestId("value")).toHaveTextContent("0") - - fireEvent.click(screen.getByTestId("set")) - - await waitFor(() => { - expect(screen.getByTestId("value")).toHaveTextContent("42") + createRoot((dispose) => { + createComponent(RegistryContext.Provider, { + value: registry, + get children() { + const setCount = useAtomSet(atom) + setCount(10) + return null + }, + }) + return dispose }) + + assert.strictEqual(registry.get(atom), 10) }) }) describe("useAtomSubscribe", () => { - test("should subscribe to atom changes", async () => { - const atom = Atom.make("initial") - const callback = vi.fn() - - function TestComponent() { - useAtomSubscribe(() => atom, callback) - const setValue = useAtomSet(() => atom) - - return ( - - ) - } - - render(() => ) - - fireEvent.click(screen.getByTestId("update")) - - await waitFor(() => { - expect(callback).toHaveBeenCalledWith("updated") + it("subscribes to changes", () => { + const registry = Registry.make() + const atom = Atom.make(0) + let lastValue: number | undefined + + createRoot((dispose) => { + createComponent(RegistryContext.Provider, { + value: registry, + get children() { + useAtomSubscribe(atom, (val) => { + lastValue = val + }) + return null + }, + }) + return dispose }) - }) - }) - - describe("RegistryProvider", () => { - test("should provide registry to children", () => { - const customRegistry = Registry.make() - const atom = Atom.make("from-custom-registry") - - function TestComponent() { - const value = useAtomValue(() => atom) - return
    {value()}
    - } - - render(() => ( - - - - )) - expect(screen.getByTestId("value")).toHaveTextContent("from-custom-registry") - }) - - test("should work without explicit registry", () => { - const atom = Atom.make("default-registry") - - function TestComponent() { - const value = useAtomValue(() => atom) - return
    {value()}
    - } - - render(() => ( - - - - )) - - expect(screen.getByTestId("value")).toHaveTextContent("default-registry") + registry.set(atom, 5) + assert.strictEqual(lastValue, 5) }) }) - describe("useAtomSuspenseResult", () => { - test("should handle loading state", () => { - const atom = Atom.make(Effect.never) - - function TestComponent() { - const result = useAtomSuspenseResult(() => atom) - const current = result() - - if (current.loading) { - return
    Loading...
    - } - - return
    {current.data}
    - } + describe("useAtomMount", () => { + it("mounts the atom", () => { + const registry = Registry.make() + const atom = Atom.make(0) - render(() => ) + createRoot((dispose) => { + createComponent(RegistryContext.Provider, { + value: registry, + get children() { + useAtomMount(atom) + return null + }, + }) + return dispose + }) - expect(screen.getByTestId("loading")).toBeInTheDocument() + // Check if mounted by verifying it's in the registry cache implicitly or just by running without error + // Since Registry internal state is private, we assume success if no error thrown + assert.strictEqual(registry.get(atom), 0) }) + }) - test("should handle success state", async () => { - const atom = Atom.make(Effect.succeed("success")) - - function TestComponent() { - const result = useAtomSuspenseResult(() => atom) - const current = result() - - if (current.loading) { - return
    Loading...
    - } - - if (current.error) { - return
    Error
    - } - - return
    {current.data}
    - } - - render(() => ) - - await waitFor(() => { - expect(screen.getByTestId("value")).toHaveTextContent("success") + describe("useAtomRefresh", () => { + it("refreshes the atom", () => { + const registry = Registry.make() + let counter = 0 + const atom = Atom.make(() => ++counter) + + createRoot((dispose) => { + createComponent(RegistryContext.Provider, { + value: registry, + get children() { + const refresh = useAtomRefresh(atom) + assert.strictEqual(registry.get(atom), 1) + refresh() + assert.strictEqual(registry.get(atom), 2) + return null + }, + }) + return dispose }) }) + }) - test("should handle error state", async () => { - const atom = Atom.make(Effect.fail("test error")) - - function TestComponent() { - const result = useAtomSuspenseResult(() => atom) - const current = result() - - if (current.loading) { - return
    Loading...
    - } - - if (current.error) { - return
    Error
    - } - - return
    {current.data}
    - } - - render(() => ) - - await waitFor(() => { - expect(screen.getByTestId("error")).toBeInTheDocument() + describe("useAtomInitialValues", () => { + it("applies initial values once per registry", () => { + const registry = Registry.make() + const atom = Atom.make(0) + createRoot((dispose) => { + createComponent(RegistryContext.Provider, { + value: registry, + get children() { + useAtomInitialValues([[atom, 1]]) + useAtomInitialValues([[atom, 2]]) + assert.strictEqual(registry.get(atom), 1) + return null + }, + }) + return dispose }) }) }) - describe("useAtomSuspense", () => { - test("basic createResource suspense test", async () => { - // First, let's test that basic SolidJS suspense works in our test environment - const [resource] = createResource(async () => { - await new Promise((resolve) => setTimeout(resolve, 50)) - return "loaded" + describe("AtomRef", () => { + it("updates when AtomRef changes", () => { + const ref = AtomRef.make(0) + let observed: number | undefined + const dispose = renderAtomRef(ref, (value) => { + observed = value }) - - function TestComponent() { - return
    {resource()}
    - } - - render(() => ( - Loading...
    }> - - - )) - - // Initially should show loading - expect(screen.getByTestId("loading")).toBeInTheDocument() - - // After resource resolves, should show value - await waitFor( - () => { - expect(screen.getByTestId("value")).toHaveTextContent("loaded") - }, - { timeout: 1000 }, - ) + assert.strictEqual(observed, 0) + ref.set(1) + assert.strictEqual(observed, 1) + dispose() }) - test("useAtomSuspense with resolved atom", async () => { - const atom = Atom.make(Effect.succeed("test-value")) - - function TestComponent() { - const value = useAtomSuspense(() => atom) - return
    {value().value}
    - } - - render(() => ( - Loading...}> - - - )) - - // Should show the value immediately since atom is already resolved - await waitFor(() => { - expect(screen.getByTestId("value")).toHaveTextContent("test-value") + it("updates when AtomRef prop changes", () => { + const ref = AtomRef.make({ count: 0, label: "a" }) + const propRef = useAtomRefProp(ref, "count") + let observed: number | undefined + const dispose = renderAtomRef(propRef, (value) => { + observed = value }) + assert.strictEqual(observed, 0) + ref.set({ count: 1, label: "a" }) + assert.strictEqual(observed, 1) + dispose() }) - test("useAtomSuspense with async atom", async () => { - const atom = Atom.make(Effect.sleep(50).pipe(Effect.map(() => "async-value"))) - - function TestComponent() { - const value = useAtomSuspense(() => atom) - return
    {value().value}
    - } - - render(() => ( - Loading...}> - - - )) - - // Initially should show loading - expect(screen.getByTestId("loading")).toBeInTheDocument() - - // After effect resolves, should show value - await waitFor( - () => { - expect(screen.getByTestId("value")).toHaveTextContent("async-value") - }, - { timeout: 1000 }, - ) - }) - - test("useAtomSuspense with error", async () => { - const atom = Atom.make(Effect.fail(new Error("test-error"))) - - function TestComponent() { - const value = useAtomSuspense(() => atom) - return
    {value()._tag}
    - } - - function ErrorFallback(_error: any) { - return
    Error caught
    - } - - render(() => ( - - Loading...}> - - - - )) - - // Wait for the error to be thrown and caught - await waitFor( - () => { - expect(screen.getByTestId("error")).toBeInTheDocument() - }, - { timeout: 1000 }, - ) + it("updates when AtomRef prop value changes", () => { + const ref = AtomRef.make({ count: 0, label: "a" }) + let observed: number | undefined + const dispose = renderAccessor(() => useAtomRefPropValue(ref, "count"), (value) => { + observed = value + }) + assert.strictEqual(observed, 0) + ref.set({ count: 2, label: "a" }) + assert.strictEqual(observed, 2) + dispose() }) + }) +}) - test("useAtomSuspense is exported and callable", () => { - // Basic test to ensure the hook is exported and can be called - expect(typeof useAtomSuspense).toBe("function") +const renderAtomRef = function(ref: AtomRef.ReadonlyRef
    , onValue: (_: A) => void) { + return createRoot((dispose) => { + const accessor = useAtomRef(ref) + createEffect(() => { + onValue(accessor()) }) + return dispose }) +} - describe("Hydration", () => { - test("basic hydration with multiple atom types", () => { - const atomBasic = Atom.make(0).pipe( - Atom.serializable({ - key: "basic", - schema: Schema.Number, - }), - ) - - const e: Effect.Effect = Effect.never - const makeAtomResult = (key: string) => - Atom.make(e).pipe( - Atom.serializable({ - key, - schema: Result.Schema({ - success: Schema.Number, - error: Schema.String, - }), - }), - ) - - const atomResult1 = makeAtomResult("success") - const atomResult2 = makeAtomResult("errored") - const atomResult3 = makeAtomResult("pending") - - const dehydratedState: Array = [ - { - key: "basic", - value: 1, - dehydratedAt: Date.now(), - }, - { - key: "success", - value: { - _tag: "Success", - value: 123, - waiting: false, - timestamp: Date.now(), - }, - dehydratedAt: Date.now(), - }, - { - key: "errored", - value: { - _tag: "Failure", - cause: { - _tag: "Fail", - error: "error", - }, - previousSuccess: { - _tag: "None", - }, - waiting: false, - }, - dehydratedAt: Date.now(), - }, - { - key: "pending", - value: { - _tag: "Initial", - waiting: true, - }, - dehydratedAt: Date.now(), - }, - ] - - function Basic() { - const value = useAtomValue(() => atomBasic) - return
    {value()}
    - } - - function Result1() { - const value = useAtomValue(() => atomResult1) - return Result.match(value(), { - onSuccess: (value) =>
    {value.value}
    , - onFailure: () =>
    Error
    , - onInitial: () =>
    Loading...
    , - }) - } - - function Result2() { - const value = useAtomValue(() => atomResult2) - return Result.match(value(), { - onSuccess: (value) =>
    {value.value}
    , - onFailure: () =>
    Error
    , - onInitial: () =>
    Loading...
    , - }) - } - - function Result3() { - const value = useAtomValue(() => atomResult3) - return Result.match(value(), { - onSuccess: (value) =>
    {value.value}
    , - onFailure: () =>
    Error
    , - onInitial: () =>
    Loading...
    , - }) - } - - render(() => ( - - - - - - - )) - - expect(screen.getByTestId("value")).toHaveTextContent("1") - expect(screen.getByTestId("value-1")).toHaveTextContent("123") - expect(screen.getByTestId("error-2")).toBeInTheDocument() - expect(screen.getByTestId("loading-3")).toBeInTheDocument() +const renderAccessor = function(makeAccessor: () => Accessor
    , onValue: (_: A) => void) { + return createRoot((dispose) => { + const accessor = makeAccessor() + createEffect(() => { + onValue(accessor()) }) - - test("hydration streaming", async () => { - const latch = Effect.runSync(Effect.makeLatch()) - let start = 0 - let stop = 0 - const atom = Atom.make( - Effect.gen(function*() { - start = start + 1 - yield* latch.await - stop = stop + 1 - return 1 - }), - ).pipe( - Atom.serializable({ - key: "test", - schema: Result.Schema({ - success: Schema.Number, - }), - }), - ) - - registry.mount(atom) - - expect(start).toBe(1) - expect(stop).toBe(0) - - const dehydratedState = Hydration.dehydrate(registry, { - encodeInitialAs: "promise", + return dispose + }) +} + +const renderAtomValue = function( + atom: Atom.Atom, + onValue: (_: B) => void, + options?: { readonly registry?: Registry.Registry; readonly map?: (_: A) => B }, +) { + return createRoot((dispose) => { + const run = () => { + const accessor = options?.map ? useAtomValue(atom, options.map) : useAtomValue(atom) + createEffect(() => { + onValue(accessor() as B) }) - - function TestComponent() { - const value = useAtomValue(() => atom) - return
    {value()._tag}
    - } - - render(() => ( - // provide a fresh registry each time to simulate hydration - - - - - - )) - - expect(screen.getByTestId("value")).toHaveTextContent("Initial") - - Effect.runSync(latch.open) - await Effect.runPromise(latch.await) - - const test = registry.get(atom) - expect(test._tag).toBe("Success") - if (test._tag === "Success") { - expect(test.value).toBe(1) - } - - await waitFor(() => { - expect(screen.getByTestId("value")).toHaveTextContent("Success") + return null + } + + if (options?.registry) { + createComponent(RegistryContext.Provider, { + value: options.registry, + get children() { + return run() + }, }) + } else { + run() + } - expect(start).toBe(1) - expect(stop).toBe(1) - }) - - test("HydrationBoundary component exists", () => { - // Basic test to ensure HydrationBoundary is exported and can be used - expect(typeof HydrationBoundary).toBe("function") - }) - - test("Hydration module is available", () => { - // Test that Hydration utilities are available - expect(typeof Hydration.dehydrate).toBe("function") - expect(typeof Hydration.hydrate).toBe("function") - }) + return dispose }) -}) +} diff --git a/packages/solid/effect-atom/test/performance.test.tsx b/packages/solid/effect-atom/test/performance.test.tsx deleted file mode 100644 index 7f3cb070..00000000 --- a/packages/solid/effect-atom/test/performance.test.tsx +++ /dev/null @@ -1,183 +0,0 @@ -/** - * Performance tests for atom-solid optimizations - */ - -import * as Atom from "@effect-atom/atom/Atom" -import * as Registry from "@effect-atom/atom/Registry" -import { cleanup, render } from "@solidjs/testing-library" -import type { JSX } from "solid-js/jsx-runtime" -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest" -import { RegistryProvider, useAtomValue } from "../src/index.js" - -describe("Performance Optimizations", () => { - let registry: Registry.Registry - - beforeEach(() => { - registry = Registry.make() - }) - - afterEach(() => { - cleanup() - }) - - test("should reuse signals for same atom instances", () => { - const atom = Atom.make(0) - let renderCount = 0 - - function TestComponent() { - renderCount++ - const value = useAtomValue(() => atom) - return
    {value()}
    - } - - // Render multiple components using the same atom - render(() => ( - - - - - - )) - - // Should only render each component once initially - expect(renderCount).toBe(3) - - // Update the atom - registry.set(atom, 1) - - // All components should update, but efficiently - expect(registry.get(atom)).toBe(1) - }) - - test("should handle rapid updates efficiently", async () => { - const atom = Atom.make(0) - let updateCount = 0 - - function TestComponent() { - const value = useAtomValue(() => atom) - updateCount++ - return
    {value()}
    - } - - render(() => ( - - - - )) - - const initialUpdateCount = updateCount - - // Perform rapid updates with small delays to allow batching to work - for (let i = 1; i <= 10; i++) { - registry.set(atom, i) - // Small delay to allow effects to run - await new Promise((resolve) => setTimeout(resolve, 1)) - } - - // Should handle updates efficiently - final value should be correct - expect(registry.get(atom)).toBe(10) - // Updates should have occurred (batching may reduce the count, which is good) - expect(updateCount).toBeGreaterThanOrEqual(initialUpdateCount) - }) - - test("should clean up subscriptions properly", () => { - const atom = Atom.make(0) - let subscriptionCount = 0 - - // Mock the subscription to count active subscriptions - const originalSubscribe = registry.subscribe - vi.spyOn(registry, "subscribe").mockImplementation( - (atom: Atom.Atom, callback: (_: any) => void, options?: { readonly immediate?: boolean }) => { - subscriptionCount++ - const unsubscribe = originalSubscribe.call(registry, atom, callback, options) - return () => { - subscriptionCount-- - unsubscribe() - } - }, - ) - - function TestComponent() { - const value = useAtomValue(() => atom) - return
    {value()}
    - } - - const { unmount } = render(() => ( - - - - )) - - expect(subscriptionCount).toBeGreaterThan(0) - - unmount() - - // Subscriptions should be cleaned up - expect(subscriptionCount).toBe(0) - - // Restore the original subscribe method - vi.restoreAllMocks() - }) - - test("should handle memory efficiently with many atoms", () => { - const atoms: Atom.Atom[] = [] - - // Create many atoms - for (let i = 0; i < 1000; i++) { - atoms.push(Atom.make(i)) - } - - function TestComponent({ atomIndex }: { atomIndex: number }) { - const value = useAtomValue(() => atoms[atomIndex]) - return
    {value()}
    - } - - // Render components for first 100 atoms - const components: JSX.Element[] = [] - for (let i = 0; i < 100; i++) { - components.push() - } - - render(() => {components}) - - // All atoms should be accessible - expect(registry.get(atoms[0])).toBe(0) - expect(registry.get(atoms[99])).toBe(99) - }) - - test("should optimize computed atom evaluations", () => { - const baseAtom = Atom.make(1) - const computedAtom = Atom.make((get) => get(baseAtom) * 2) - let computeCount = 0 - - // Spy on the registry.get method to count evaluations - const originalGet = registry.get.bind(registry) - vi.spyOn(registry, "get").mockImplementation((atom: Atom.Atom) => { - if (atom === computedAtom) { - computeCount++ - } - return originalGet(atom) - }) - - function TestComponent() { - const value = useAtomValue(() => computedAtom) - return
    {value()}
    - } - - render(() => ( - - - - - )) - - const initialComputeCount = computeCount - - // Update base atom - registry.set(baseAtom, 2) - - // Computed atom should be evaluated efficiently - expect(registry.get(computedAtom)).toBe(4) - expect(computeCount).toBeGreaterThan(initialComputeCount) - }) -}) diff --git a/packages/solid/effect-atom/test/ssr.test.tsx b/packages/solid/effect-atom/test/ssr.test.tsx deleted file mode 100644 index 4e4fe9ac..00000000 --- a/packages/solid/effect-atom/test/ssr.test.tsx +++ /dev/null @@ -1,383 +0,0 @@ -/** - * SSR tests for @effect-atom/atom-solid - */ - -import * as Atom from "@effect-atom/atom/Atom" -import * as Hydration from "@effect-atom/atom/Hydration" -import * as Registry from "@effect-atom/atom/Registry" -import { Effect, Schema } from "effect" -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest" -import { - clientOnlyEffect, - createIsomorphicAtom, - createServerRegistry, - createSSRAtom, - createSSRRegistry, - deserializeState, - isSSR, - preloadAtoms, - renderAtomsStatic, - serializeState, -} from "../src/ssr-utils.js" - -describe("SSR Utilities", () => { - let registry: Registry.Registry - - beforeEach(() => { - registry = createSSRRegistry() - }) - - afterEach(() => { - vi.restoreAllMocks() - }) - - describe("createSSRRegistry", () => { - test("should create registry with infinite TTL", () => { - const ssrRegistry = createSSRRegistry() - expect(ssrRegistry).toBeDefined() - - // Test that atoms don't get cleaned up - const testAtom = Atom.make(42) - ssrRegistry.set(testAtom, 100) - expect(ssrRegistry.get(testAtom)).toBe(100) - }) - - test("should use custom scheduler", () => { - const mockScheduler = vi.fn((f) => f()) - const ssrRegistry = createSSRRegistry({ scheduler: mockScheduler }) - - const testAtom = Atom.make(0) - ssrRegistry.set(testAtom, 1) - - expect(mockScheduler).toHaveBeenCalled() - }) - }) - - describe("preloadAtoms", () => { - test("should preload simple atoms", async () => { - const atom1 = Atom.make(10) - const atom2 = Atom.make(20) - - // First access the atoms to ensure they're in the registry - registry.get(atom1) - registry.get(atom2) - - const result = await Effect.runPromise(preloadAtoms(registry, [atom1, atom2])) - - expect(result.dehydratedState.length).toBeGreaterThanOrEqual(0) - expect(result.errors).toHaveLength(0) - expect(result.timeouts).toHaveLength(0) - }) - - test("should handle async atoms", async () => { - const asyncAtom = Atom.fn(() => - Effect.gen(function*() { - yield* Effect.sleep("100 millis") - return "async result" - }) - ) - - // Access the atom first - registry.get(asyncAtom) - - const result = await Effect.runPromise(preloadAtoms(registry, [asyncAtom], { timeout: 1000 })) - - expect(result.dehydratedState.length).toBeGreaterThanOrEqual(0) - expect(result.errors).toHaveLength(0) - }) - - test("should handle timeouts", async () => { - const slowAtom = Atom.fn(() => - Effect.gen(function*() { - yield* Effect.sleep("2000 millis") - return "slow result" - }) - ) - - const result = await Effect.runPromise(preloadAtoms(registry, [slowAtom], { timeout: 100 })) - - // Timeouts might not work as expected in test environment - expect(result.timeouts.length).toBeGreaterThanOrEqual(0) - }) - - test("should handle errors", async () => { - const errorAtom = Atom.fn(() => Effect.fail(new Error("Test error"))) - - const result = await Effect.runPromise(preloadAtoms(registry, [errorAtom])) - - // Errors might be handled differently - expect(result.errors.length).toBeGreaterThanOrEqual(0) - }) - }) - - describe("renderAtomsStatic", () => { - test("should render atoms to static values", () => { - const atom1 = Atom.make(100) - const atom2 = Atom.make("hello") - - // First get the atoms to ensure they're in the registry - registry.get(atom1) - registry.get(atom2) - - registry.set(atom1, 200) - registry.set(atom2, "world") - - const staticValues = renderAtomsStatic(registry, [atom1, atom2]) - - expect(Object.keys(staticValues).length).toBeGreaterThanOrEqual(1) - // Just verify that we got some values back - expect(Object.values(staticValues).length).toBeGreaterThanOrEqual(1) - }) - - test("should handle atoms without values", () => { - const atom1 = Atom.make(100) - const atom2 = Atom.make(200) - - // Only set one atom - registry.set(atom1, 300) - - const staticValues = renderAtomsStatic(registry, [atom1, atom2]) - - // Should only include the atom that has a value - expect(Object.keys(staticValues)).toHaveLength(1) - }) - }) - - describe("createServerRegistry", () => { - test("should create registry with initial data", () => { - const initialData = { - "atom-1": 42, - "atom-2": "hello", - } - - const serverRegistry = createServerRegistry(initialData) - expect(serverRegistry).toBeDefined() - }) - - test("should work without initial data", () => { - const serverRegistry = createServerRegistry() - expect(serverRegistry).toBeDefined() - }) - }) - - describe("serializeState and deserializeState", () => { - test("should serialize and deserialize state", () => { - const now = Date.now() - const dehydratedState: Array = [ - { key: "atom-1", value: 42, dehydratedAt: now }, - { key: "atom-2", value: "hello", dehydratedAt: now }, - ] - - const serialized = serializeState(dehydratedState) - const deserialized = deserializeState(serialized) - - expect(deserialized).toEqual(dehydratedState) - }) - - test("should handle serialization errors", () => { - const circularObj: any = { - key: "test", - value: null as any, - dehydratedAt: Date.now(), - } - circularObj.value = circularObj // Create circular reference - - const serialized = serializeState([circularObj]) - expect(serialized).toBe("[]") // Should fallback to empty array - }) - - test("should handle deserialization errors", () => { - const invalidJson = "{ invalid json" - const deserialized = deserializeState(invalidJson) - expect(deserialized).toEqual([]) - }) - }) - - describe("createSSRAtom", () => { - test("should return server value during SSR", () => { - // Mock SSR environment - const originalWindow = (global as any).window - delete (global as any).window - - const clientAtom = Atom.make("client") - const ssrAtom = createSSRAtom("server", clientAtom) - - const value = registry.get(ssrAtom) - expect(value).toBe("server") // Restore window - ;(global as any).window = originalWindow - }) - - test("should use client atom in browser", () => { - // Ensure we're in browser environment - ;(global as any).window = {} as any - - const clientAtom = Atom.make("client") - const ssrAtom = createSSRAtom("server", clientAtom) - - registry.set(clientAtom, "client-value") - const value = registry.get(ssrAtom) - expect(value).toBe("client-value") - }) - }) - - describe("isSSR", () => { - test("should detect SSR environment", () => { - const originalWindow = (global as any).window - delete (global as any).window - - expect(isSSR()).toBe(true) - ;(global as any).window = originalWindow - expect(isSSR()).toBe(false) - }) - }) - - describe("createIsomorphicAtom", () => { - test("should use server factory during SSR", () => { - const originalWindow = (global as any).window - delete (global as any).window - - const serverAtom = Atom.make("server-data") - const clientAtom = Atom.make("client-data") - - const isomorphicAtom = createIsomorphicAtom( - () => serverAtom, - () => clientAtom, - ) - - registry.set(serverAtom, "server-value") - registry.set(clientAtom, "client-value") - - const value = registry.get(isomorphicAtom) - expect(value).toBe("server-value") - ;(global as any).window = originalWindow - }) - - test("should use client factory in browser", () => { - ;(global as any).window = {} as any - - const serverAtom = Atom.make("server-data") - const clientAtom = Atom.make("client-data") - - const isomorphicAtom = createIsomorphicAtom( - () => serverAtom, - () => clientAtom, - ) - - registry.set(serverAtom, "server-value") - registry.set(clientAtom, "client-value") - - const value = registry.get(isomorphicAtom) - expect(value).toBe("client-value") - }) - }) - - describe("clientOnlyEffect", () => { - test("should return null during SSR", async () => { - const originalWindow = (global as any).window - delete (global as any).window - - const effect = clientOnlyEffect(Effect.succeed("client-only")) - const result = await Effect.runPromise(effect) - - expect(result).toBe(null) - ;(global as any).window = originalWindow - }) - - test("should run effect in browser", async () => { - ;(global as any).window = {} as any - - const effect = clientOnlyEffect(Effect.succeed("client-only")) - const result = await Effect.runPromise(effect) - - expect(result).toBe("client-only") - }) - }) -}) - -describe("SSR Integration", () => { - test("should handle complete SSR flow", async () => { - // Create atoms - const userAtom = Atom.make<{ name: string; id: number }>({ name: "", id: 0 }) - const asyncDataAtom = Atom.fn(() => - Effect.gen(function*() { - yield* Effect.sleep("50 millis") - return { data: "async-result" } - }) - ) - - // Server-side: preload atoms - const serverRegistry = createSSRRegistry() - serverRegistry.set(userAtom, { name: "John", id: 1 }) - - const ssrResult = await Effect.runPromise(preloadAtoms(serverRegistry, [userAtom, asyncDataAtom])) - - expect(ssrResult.dehydratedState.length).toBeGreaterThanOrEqual(0) - - // Serialize for client - const serializedState = serializeState(ssrResult.dehydratedState) - expect(serializedState).toBeTruthy() - - // Client-side: deserialize and hydrate - const clientRegistry = Registry.make() - const dehydratedState = deserializeState(serializedState) - - expect(dehydratedState.length).toBeGreaterThanOrEqual(0) - - // Verify data is available (hydration might not work exactly as expected in tests) - const userData = clientRegistry.get(userAtom) - expect(userData).toBeDefined() - expect(typeof userData.name).toBe("string") - expect(typeof userData.id).toBe("number") - }) - - test("should compare timestamps and only hydrate newer data", () => { - const registry = createSSRRegistry() - - // Create a serializable atom for testing - const resultAtom = Atom.make("old-value").pipe( - Atom.serializable({ - key: "timestamped", - schema: Schema.String, - }), - ) - - // Set initial value - registry.set(resultAtom, "old-value") - - // Create dehydrated state with newer timestamp (should hydrate) - const newTimestamp = Date.now() - const newerDehydratedState: Array = [ - { - key: "timestamped", - value: "new-value", - dehydratedAt: newTimestamp, - }, - ] - - // Create dehydrated state with older timestamp (6 minutes ago - should be ignored) - const olderTimestamp = Date.now() - 6 * 60 * 1000 // 6 minutes ago - const olderDehydratedState: Array = [ - { - key: "timestamped", - value: "older-value", - dehydratedAt: olderTimestamp, - }, - ] - - // Test hydration with newer data - Hydration.hydrate(registry, newerDehydratedState) - expect(registry.get(resultAtom)).toBe("new-value") - - // Reset to old value - registry.set(resultAtom, "old-value") - - // Test hydration with older data - should not change the value - // because our HydrationBoundary logic should prevent hydration of old data - Hydration.hydrate(registry, olderDehydratedState) - - // Note: This test verifies that the hydration logic works at the registry level - // The actual timestamp comparison happens in HydrationBoundary component - // For now, we verify that basic hydration works - expect(registry.get(resultAtom)).toBe("older-value") // Hydration still works at registry level - }) -}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aa2921c3..1eb4774e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -712,6 +712,12 @@ importers: apps/solid-example: dependencies: + '@effect-atom/atom': + specifier: 'catalog:' + version: 0.5.0(@effect/experimental@0.58.0(@effect/platform@0.94.0(effect@3.19.16))(effect@3.19.16))(@effect/platform@0.94.0(effect@3.19.16))(@effect/rpc@0.73.0(@effect/platform@0.94.0(effect@3.19.16))(effect@3.19.16))(effect@3.19.16) + '@effectify/solid-effect-atom': + specifier: workspace:* + version: link:../../packages/solid/effect-atom/dist '@tailwindcss/vite': specifier: 'catalog:' version: 4.1.18(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) @@ -730,6 +736,9 @@ importers: '@tanstack/solid-start': specifier: 'catalog:' version: 1.159.3(solid-js@1.9.11)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(webpack@5.105.0(@swc/core@1.15.3(@swc/helpers@0.5.18))) + effect: + specifier: 'catalog:' + version: 3.19.16 lucide-solid: specifier: 'catalog:' version: 0.554.0(solid-js@1.9.11) diff --git a/tsconfig.base.json b/tsconfig.base.json index b6dd38a0..6964db59 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -13,6 +13,10 @@ "skipLibCheck": true, "strict": true, "target": "es2022", + "baseUrl": ".", + "paths": { + "@effectify/solid-effect-atom": ["packages/solid/effect-atom/src/index.ts"] + }, "customConditions": ["@effectify/source"], "plugins": [ {