Skip to content

React — Foundational Design Rationale (FDR)

Version: 1.0 Status: Normative Purpose: Document the "Why" behind every constitutional decision in @manifesto-ai/react


Overview

This document records the foundational design decisions that shape @manifesto-ai/react.

Each FDR entry follows the format:

  • Decision: What was decided
  • Context: Why this decision was needed
  • Rationale: The reasoning behind the choice
  • Alternatives Rejected: Other options considered and why rejected
  • Consequences: What this decision enables and constrains

FDR-R001: Factory Pattern (createManifestoApp)

Decision

The primary API is a factory function createManifestoApp(domain, options) that returns a complete app object with Provider and hooks.

typescript
const App = createManifestoApp(TodoDomain, { initialState: { todos: [] } });

// Returns:
// App.Provider - Context provider
// App.useValue - Select state
// App.useDispatch - Get dispatch
// App.useActions - Get typed actions

Context

Users need a simple way to integrate Manifesto into React applications. The integration requires:

  • Bridge creation and lifecycle management
  • Context setup
  • Type-safe hooks
  • Action dispatcher generation

Rationale

Zero-config setup with full type safety

ConcernWhy Factory Pattern
Type inferenceFactory captures domain types at call site, flowing to all hooks
EncapsulationBridge lifecycle managed internally, no leaky abstractions
Developer experienceSingle import, single call, everything works
TestingEasy to create test instances with different initial state

Alternatives Rejected

AlternativeWhy Rejected
Global hooksNo type safety, can't have multiple domains
HOC patternVerbose, poor TypeScript inference
Render propsClunky, out of fashion
Redux-style connectToo much boilerplate, weak typing

Consequences

Enables:

  • One-liner setup for new apps
  • Full type inference from Zod schemas
  • Easy testing with different configurations
  • Multiple domains in same app (separate factories)

Constrains:

  • Factory must be called at module level
  • Can't dynamically change domain after creation

Requires:

  • Domain must be defined before creating app
  • Initial state must match schema

FDR-R002: Selector-Based Subscription (useValue)

Decision

useValue takes a selector function and only re-renders when the selected value changes.

typescript
// Only re-renders when todos changes
const todos = App.useValue(s => s.todos);

// Only re-renders when filter changes
const filter = App.useValue(s => s.filter);

Context

React re-renders can be expensive. Manifesto snapshots contain all state, and we need fine-grained subscriptions to avoid unnecessary re-renders.

Rationale

Performance through precision

ConcernWhy Selector Pattern
PerformanceOnly re-render when relevant data changes
ComposabilitySelectors can derive values
FamiliaritySame pattern as Reselect, Zustand
Type safetySelector return type is inferred

Alternatives Rejected

AlternativeWhy Rejected
Full snapshot subscriptionEvery component re-renders on any change
Path-based subscriptionStrings lose type safety
Proxy-based trackingComplex, magic behavior, debugging difficulty
Atom-based (Jotai/Recoil)Different mental model, requires atom definition

Consequences

Enables:

  • Optimal re-render behavior
  • Derived value computation in selectors
  • Easy optimization (memoized selectors)

Constrains:

  • Selectors should be pure functions
  • New objects in selectors cause extra re-renders

Requires:

  • Users understand selector identity rules
  • Stable selector references (useCallback if inline)

FDR-R003: Typed Action Dispatchers (useActions)

Decision

useActions returns an object with type-safe functions for each domain action.

typescript
const { add, toggle, remove } = App.useActions();

add({ title: "Buy milk" });      // Type-safe input
toggle({ id: "123" });           // Type-safe input

Context

Domain actions have typed inputs (from Zod schemas). Users need a way to dispatch actions with full type checking.

Rationale

Developer experience and safety

ConcernWhy Typed Dispatchers
Type safetyInput validation at compile time
AutocompleteIDE shows available actions and their inputs
RefactoringRename action, all usages update
DiscoverabilityDestructuring shows available actions

Alternatives Rejected

AlternativeWhy Rejected
String-based dispatchNo type safety, typos possible
Generic dispatch onlyLoses action-specific typing
Action creatorsExtra boilerplate, separate from hooks
Proxy-based actionsMagic behavior, poor debugging

Consequences

Enables:

  • Compile-time validation of action inputs
  • IDE autocomplete for actions
  • Safe refactoring

Constrains:

  • Actions must be known at domain definition time
  • Dynamic actions not directly supported

Requires:

  • Domain must define actions with input types
  • Builder must export ActionRef types

FDR-R004: Bridge Lifecycle in Provider

Decision

The Provider component creates the Bridge on mount and disposes it on unmount.

typescript
function Provider({ children }) {
  const bridge = useMemo(() => createBridge(...), []);

  useEffect(() => {
    return () => bridge.dispose();
  }, [bridge]);

  return <BridgeContext.Provider value={{ bridge }}>{children}</BridgeContext.Provider>;
}

Context

Bridge instances hold subscriptions, timers, and other resources. These must be properly cleaned up to avoid memory leaks.

Rationale

Predictable resource management

ConcernWhy Provider Manages Lifecycle
Memory safetyResources always cleaned up
SimplicityUsers don't manage lifecycle
React idiomFollows useEffect cleanup pattern
EncapsulationBridge details hidden from user

Alternatives Rejected

AlternativeWhy Rejected
User manages BridgeEasy to forget cleanup, memory leaks
Global singletonCan't have multiple instances, testing hard
Ref-based storageMore complex, same result

Consequences

Enables:

  • Automatic resource cleanup
  • Multiple instances (testing, multiple domains)
  • React StrictMode compatibility

Constrains:

  • Bridge lifecycle tied to Provider mount/unmount
  • Can't share Bridge across Provider instances

FDR-R005: Low-Level API for Advanced Use Cases

Decision

Provide low-level BridgeProvider and hooks (useBridge, useSnapshot) alongside the factory API.

typescript
// Low-level usage
const bridge = createBridge({ ... });

<BridgeProvider bridge={bridge}>
  <App />
</BridgeProvider>

// In component
const bridge = useBridge();
const snapshot = useSnapshot();

Context

Some users need custom Bridge setup (custom World, special configuration). The factory API should not be the only option.

Rationale

Flexibility without complexity

ConcernWhy Low-Level API
Custom setupUsers can configure Bridge however needed
TestingEasy to inject mock Bridge
Gradual adoptionStart low-level, migrate to factory
Framework buildingBuild higher-level abstractions

Alternatives Rejected

AlternativeWhy Rejected
Factory onlyToo limiting for advanced users
Config explosionFactory options become unwieldy
Escape hatchesAd-hoc solutions are messy

Consequences

Enables:

  • Custom Bridge configuration
  • Easy testing with mocks
  • Building custom abstractions

Constrains:

  • Users must understand Bridge lifecycle
  • Low-level API has less type safety

Requires:

  • Clear documentation of both APIs
  • Examples for common patterns

Summary Table

FDRDecisionKey Principle
R001Factory patternZero-config with full types
R002Selector-based subscriptionPerformance through precision
R003Typed action dispatchersDeveloper experience and safety
R004Provider manages lifecyclePredictable resource management
R005Low-level API availableFlexibility without complexity

Cross-Reference

SPEC SectionRelevant FDR
§5.1 createManifestoAppFDR-R001
§5.3.1 useValueFDR-R002
§5.3.3 useActionsFDR-R003
§5.2 ProviderFDR-R004
§5.4 Low-Level APIFDR-R005
Other FDRRelationship
Bridge FDR-B001Bridge design informs React bindings
Builder FDRType inference flows from Builder to React

End of React FDR