remesh
repository·main·Indexed 20 days ago
https://github.com/remesh-js/remeshA 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).
What's inside remesh
- remesh-vue is the official Vue adapter for the remesh framework. It allows you to integrate remesh domains and state management directly into Vue applications.
Use remesh-react as a React adapter for remesh
mainTheremesh-reactpackage provides the official React adapter forremesh. It allows you to integrateremeshdomains and logic directly into React components, enabling a reactive programming model within the React ecosystem.What is a Remesh Domain?
mainA 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:
- Domain States: The internal data you want to store. States are private and cannot be accessed directly from outside the domain.
- Domain Queries: The only way to read state. Queries can return a state or derive new values from existing states.
- Domain Commands: The only way to update state. Commands can update state, emit events, or trigger other commands.
- Domain Events: Identifiers for things that have happened within the domain.
- 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, andeventshould be exposed in the domain's return object.statemust remain private to the domain.Create a custom module to reuse logic across domains
mainCustom 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(thedomainargument in a domain'simpl) and an options object, then returns aRemesh.moduleobject.Inside the module, you define your logic using the provided
domaincontext and then wrap it inRemesh.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, }, } }, })Create a custom module for reusing logic
mainTo reuse logic across multiple domains, define a function that accepts a
RemeshDomainContext(thedomainargument in a domain'simpl) and returns aRemesh.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 }, } }, })Configure IDE for Vue 3 + TypeScript development
mainFor the best development experience with this template (Vue 3, TypeScript, and Vite), it is recommended to use VS Code with the Volar extension installed.Use Remesh in a React component
mainTo use Remesh in React, install
remesh-react. Wrap your application in<RemeshRoot>. Inside components, use hooks likeuseRemeshDomain,useRemeshQuery,useRemeshEvent, anduseRemeshSendto 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> )Fetch async resources in a domain using AsyncModule
mainUse
AsyncModulefromremesh/modules/asyncto 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, }, } }, })Define a domain command
mainCommands are used to perform actions and trigger state updates or events. Use
domain.commandwithin theimplfunction. The implementation function receives agetmethod to read state.Note: To indicate that a command performs no operations (no state updates or event emissions), return
nullor 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 }, }) }, })Subscribe to events or queries in a domain-effect
mainA
domain.effectis used to perform side effects in response to changes in the domain. You can subscribe to events usingfromEvent(Event())and to queries usingfromQuery(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 { /* ... */ } }, })Manage domain lifecycle with RemeshScope
mainBy default,
remeshautomatically 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 theRemeshScopecomponent fromremesh-reactand pass the desired domains to itsdomainsprop.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> }Install remesh and rxjs
mainTo use Remesh, you must install both the
remeshcore package andrxjsas a peer dependency.Using npm:
npm install --save remesh rxjsUsing yarn:
yarn add remesh rxjsnpm install --save remesh rxjs