remesh

repository·main·Indexed 20 days ago

https://github.com/remesh-js/remesh

A CQRS-based Domain-Driven Design (DDD) framework for large-scale TypeScript/JavaScript applications. It provides a modular, reactive, and predictable way to manage complex business logic independently of the view layer using domains consisting of states, queries, commands, events, and effects. Official adapters are available for React (remesh-react) and Vue (remesh-vue).

Tokens
37.6K
Snippets
100
Records
120
Agent score
69%

What's inside remesh

  1. What is a Remesh Domain?

    main

    A Domain is the fundamental unit of business logic in Remesh, acting similarly to a component but for logic rather than UI. It encapsulates all related business state and behavior.

    A domain consists of five primary resources:

    1. Domain States: The internal data you want to store. States are private and cannot be accessed directly from outside the domain.
    2. Domain Queries: The only way to read state. Queries can return a state or derive new values from existing states.
    3. Domain Commands: The only way to update state. Commands can update state, emit events, or trigger other commands.
    4. Domain Events: Identifiers for things that have happened within the domain.
    5. Domain Effects: Observables (using RxJS) that handle side effects and can trigger commands or events.

    Key Constraint: To ensure predictability and prevent invalid updates, only query, command, and event should be exposed in the domain's return object. state must remain private to the domain.

  2. Create a custom module to reuse logic across domains

    main

    Custom modules allow you to encapsulate state, queries, commands, and events that can be reused in multiple domains. A custom module is typically a function that accepts a RemeshDomainContext (the domain argument in a domain's impl) and an options object, then returns a Remesh.module object.

    Inside the module, you define your logic using the provided domain context and then wrap it in Remesh.module({ query, command, event }) to make it compatible with Remesh domains.

    import { Remesh, RemeshDomainContext, Capitalize } from 'Remesh'
    
    export type TextModuleOptions = {
      name: Capitalize
      default?: string
    }
    
    export const TextModule = (domain: RemeshDomainContext, options: TextModuleOptions) => {
      const TextState = domain.state({
        name: `${options.name}.TextState`,
        default: options.default ?? '',
      })
    
      const TextQuery = domain.query({
        name: `${options.name}.TextQuery`,
        impl: ({ get }) => get(TextState()),
      })
    
      const SetTextCommand = domain.command({
        name: `${options.name}.SetTextCommand`,
        impl: ({}, current: string) => {
          return TextState().new(current)
        },
      })
    
      return Remesh.module({
        query: { TextQuery },
        command: { SetTextCommand },
      })
    }
    
    // Usage in a domain
    const MyDomain = Remesh.domain({
      name: 'MyDomain',
      impl: (domain) => {
        const Text = TextModule(domain, {
          name: 'Text',
          default: 'Hello, world!',
        })
    
        return {
          command: {
            SetTextCommand: Text.command.SetTextCommand,
          },
        }
      },
    })
  3. Create a custom module for reusing logic

    main

    To reuse logic across multiple domains, define a function that accepts a RemeshDomainContext (the domain argument in a domain's impl) and returns a Remesh.module. This allows you to encapsulate states, queries, commands, and events that can be instantiated in any domain.

    import { Remesh, RemeshDomainContext, Capitalize } from 'Remesh'
    
    export type TextModuleOptions = {
      name: Capitalize
      default?: string
    }
    
    export const TextModule = (domain: RemeshDomainContext, options: TextModuleOptions) => {
      const TextState = domain.state({
        name: `${options.name}.TextState`,
        default: options.default ?? '',
      })
    
      const TextQuery = domain.query({
        name: `${options.name}.TextQuery`,
        impl: ({ get }) => get(TextState()),
      })
    
      const SetTextCommand = domain.command({
        name: `${options.name}.SetTextCommand`,
        impl: ({}, current: string) => {
          return TextState().new(current)
        },
      })
    
      return Remesh.module({
        query: { TextQuery },
        command: { SetTextCommand },
      })
    }
    
    // Usage in a domain
    const MyDomain = Remesh.domain({
      name: 'MyDomain',
      impl: (domain) => {
        const Text = TextModule(domain, { name: 'Text', default: 'Hello!' })
        return {
          command: { SetTextCommand: Text.command.SetTextCommand },
        }
      },
    })
  4. Use Remesh in a React component

    main

    To use Remesh in React, install remesh-react. Wrap your application in <RemeshRoot>. Inside components, use hooks like useRemeshDomain, useRemeshQuery, useRemeshEvent, and useRemeshSend to interact with your domains.

    import React from 'react'
    import ReactDOM from 'react-dom/client'
    import { RemeshRoot, useRemeshDomain, useRemeshQuery, useRemeshEvent, useRemeshSend } from 'remesh-react'
    
    const YourComponent = () => {
      const send = useRemeshSend()
      const domain = useRemeshDomain(YourDomain())
      const data = useRemeshQuery(domain.query.YourQuery(queryArg))
    
      const handleClick = () => {
        send(domain.command.YourCommand(commandArg))
      }
    
      useRemeshEvent(domain.event.YourEvent, (event) => {
        // do something
      })
    
      return <></>
    }
    
    const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
    
    root.render(
      <RemeshRoot>
        <YourComponent />
      </RemeshRoot>
    )
  5. Fetch async resources in a domain using AsyncModule

    main

    Use AsyncModule from remesh/modules/async to handle asynchronous operations like API calls within a domain. It provides built-in handling for success, failure, loading, cancellation, and changes.

    import { Remesh } from 'remesh'
    import { AsyncModule } from 'remesh/modules/async'
    
    const YourDomain = Remesh.domain({
      name: 'YourDomain',
      impl: (domain) => {
        const YourAsyncTask = AsyncModule(domain, {
          name: 'YourAsyncTask',
          load: async ({ get }, arg: number) => {
            const response = await fetch('/path/to/api?arg=' + arg)
            return await response.json()
          },
          onSuccess: ({ get }, json, arg) => {
            return MySuccessCommand(json)
          },
          onFailed: ({ get }, error, arg) => {
            return MyFailedCommand(error.message)
          },
          onLoading: ({ get }, arg) => {
            return MyLoadingCommand()
          },
          onCanceled: ({ get }, arg) => {
            return MyCanceledCommand()
          },
          onChanged: ({ get }, asyncState, arg) => {
            return MyChangedCommand()
          },
        })
    
        return {
          command: {
            LoadCommand: YourAsyncTask.command.LoadCommand,
            CancelCommand: YourAsyncTask.command.CancelCommand,
            ReloadCommand: YourAsyncTask.command.ReloadCommand,
          },
          event: {
            SuccessEvent: YourAsyncTask.event.SuccessEvent,
            FailedEvent: YourAsyncTask.event.FailedEvent,
            LoadingEvent: YourAsyncTask.event.LoadingEvent,
            CanceledEvent: YourAsyncTask.event.CanceledEvent,
            ChangedEvent: YourAsyncTask.event.ChangedEvent,
          },
        }
      },
    })
  6. Define a domain command

    main

    Commands are used to perform actions and trigger state updates or events. Use domain.command within the impl function. The implementation function receives a get method to read state.

    Note: To indicate that a command performs no operations (no state updates or event emissions), return null or an empty array [].

    import { Remesh } from 'remesh'
    
    const YourDomain = Remesh.domain({
      name: 'YourDomain',
      impl: (domain) => {
        const YourCommand = domain.command({
          name: 'YourCommand',
          impl: ({ get }) => {
            // do something
          },
        })
      },
    })
  7. Subscribe to events or queries in a domain-effect

    main

    A domain.effect is used to perform side effects in response to changes in the domain. You can subscribe to events using fromEvent(Event()) and to queries using fromQuery(Query()). Both return RxJS Observables.

    • fromEvent(Event()): Emits whenever the specified event is emitted.
    • fromQuery(Query()): Emits whenever the specified query is re-computed (i.e., its underlying state changes).
    import { Remesh } from 'Remesh'
    import { merge } from 'rxjs'
    import { map } from 'rxjs/operators'
    
    const YourDomain = Remesh.domain({
      name: 'YourDomain',
      impl: (domain) => {
        const YourQuery = domain.query({ /* ... */ })
        const YourEvent = domain.event({ /* ... */ })
    
        domain.effect({
          name: 'YourEffect',
          impl: ({ get, fromEvent, fromQuery }) => {
            const event$ = fromEvent(YourEvent())
            const query$ = fromQuery(YourQuery())
    
            return merge(event$, query$).pipe(
              map(() => [ACommand(), BCommand()])
            )
          },
        })
    
        return { /* ... */ }
      },
    })
  8. Manage domain lifecycle with RemeshScope

    main

    By default, remesh automatically garbage collects domains that no longer have active subscribers. To prevent this and keep domain resources (and their state) alive even when components are unmounted, wrap your components in the RemeshScope component from remesh-react and pass the desired domains to its domains prop.

    import { RemeshScope } from 'remesh-react'
    
    const App = (props) => {
      // Even if component A is destroyed, the domain in TestScopeDomain() won't be collected
      return <RemeshScope domains={[TestScopeDomain()]}>{props.show && <A />}</RemeshScope>
    }
  9. Install remesh and rxjs

    main

    To use Remesh, you must install both the remesh core package and rxjs as a peer dependency.

    Using npm:

    npm install --save remesh rxjs

    Using yarn:

    yarn add remesh rxjs
    npm install --save remesh rxjs