React integration for Braided - Bridge your stateful systems to React without giving up lifecycle control.
React observes your system. React doesn't own it.
Braided is a minimal, type-safe library for declarative system composition with dependency-aware lifecycle management. Braided React provides the bridge to use those systems seamlessly in React applications.
Braided is a minimal (~250 lines), type-safe library for declarative system composition with dependency-aware lifecycle management. It lets you define stateful resources (databases, WebSockets, caches, etc.) with explicit dependencies, and handles starting/stopping them in the correct order.
Think of it as dependency injection + lifecycle management for JavaScript, inspired by Clojure's Integrant.
Modern React apps often need to manage complex, long-lived resources that don't fit neatly into the React component lifecycle:
- WebSockets & Real-time Feeds (Chat, Multiplayer Games)
- Audio/Video Contexts (WebRTC, Music Apps)
- Complex API Clients (Authentication, Retries, Caching)
- Game Loops & Simulations
- Background Tasks (Sync, Polling, Timers)
Managing these inside useEffect often leads to "dependency hell," double-initialization in StrictMode, and race conditions.
Braided React solves this by letting you define your system outside React using Braided, and then bridging it into React as a fully-typed dependency injection layer. Your resources outlive React's mount/unmount cycles and you decide when/how to stop them.
- 🔌 Direct Closure Access: System lives in module scope, React observes directly
- 🛡️ Lifecycle Safety: Resources survive remounts and StrictMode
- 🎯 Type Safety: Fully inferred types from your system config to your hooks
- 🧩 Observer Pattern: React components observe the system; they don't drive it
- ⚡ React Primitives: Integrates with Suspense and ErrorBoundary
- 🧪 Testing Friendly: Optional Context for dependency injection in tests
- 📦 Minimal: Thin wrapper around React hooks and Context (for testing)
npm install braided-react braidedRequirements:
react >= 18.0.0(peer dependency)braided >= 0.0.4(peer dependency)
Note: You need both libraries. Braided defines your system, Braided React bridges it to React.
// system.tsimport{defineResource}from"braided";import{createSystemManager,createSystemHooks}from"braided-react";// A simple counter resourceconstcounterResource=defineResource({start: ()=>{letcount=0;constlisteners=newSet<()=>void>();return{subscribe: (listener: ()=>void)=>{listeners.add(listener);return()=>listeners.delete(listener);},getSnapshot: ()=>count,increment: ()=>{count++;listeners.forEach((l)=>l());},};},halt: ()=>{},});// A logger resource that depends on counterconstloggerResource=defineResource({dependencies: ["counter"],start: ({ counter })=>({logCount: ()=>console.log(`Count: ${counter.getSnapshot()}`),}),halt: ()=>{},});// System configurationexportconstsystemConfig={counter: counterResource,logger: loggerResource,};// Create manager and hooks ONCEexportconstmanager=createSystemManager(systemConfig);exportconst{ useSystem, useResource, SystemProvider }=createSystemHooks(manager);// App.tsximport{Suspense}from"react";import{useResource}from"./system";functionApp(){return(<Suspensefallback={<div>Startingsystem...</div>}><Counter/></Suspense>);}functionCounter(){constcounter=useResource("counter");// Suspends automatically!constcount=useSyncExternalStore(counter.subscribe,counter.getSnapshot);return(<div><p>Count: {count}</p><buttononClick={()=>counter.increment()}>Increment</button></div>);}That's it! The system starts automatically when useResource is first called, and React suspends until it's ready.
Braided React embraces a dimensional model:
- Z-axis (Closure Space): Where your system lives independently
- X-Y Plane (React Tree): Where your components render
- Hooks: Windows between these dimensions
Your system is a module singleton in closure space. React components observe it through hooks. This separation gives you:
- Lifecycle independence - System outlives React mounts/unmounts
- No prop drilling - Direct access from any component
- Testing flexibility - Context override for dependency injection
// system.tsexportconstmanager=createSystemManager(config);exportconst{ useSystem }=createSystemHooks(manager);// App.tsx<Suspensefallback={<Loading/>}><ErrorBoundaryFallbackComponent={ErrorScreen}><App/></ErrorBoundary></Suspense>;- ✅ Minimal boilerplate
- ✅ Automatic loading (Suspense)
- ✅ Automatic errors (ErrorBoundary)
- ✅ Direct closure access (fast)
// App.tsximport{useSystemStatus}from"./system";functionApp(){const{ isIdle, isLoading, startSystem }=useSystemStatus();if(isIdle){return<WelcomeScreenonStart={startSystem}/>;}if(isLoading){return<LoadingScreen/>;}return<ChatRoom/>;}Use when you need:
- Welcome screen before startup
- Defer startup until user action
- Custom loading/error UI
// Component.test.tsximport{SystemProvider}from"./system";// Same hooks as production!import{startSystem}from"braided";test("component works",async()=>{// Start system with mock resourcesconstmockConfig={
...config,api: mockApiResource,// defineResource with vi.fn() inside};const{ system }=awaitstartSystem(mockConfig);render(<SystemProvidersystem={system}><Component/></SystemProvider>);// Test...awaithaltSystem(mockConfig,system);});Benefits:
- ✅ Real lifecycle (resources start/halt properly)
- ✅ Easy mocking (just define mock resources)
- ✅ Mix and match (swap only what you need)
- ✅ Type-safe (same config shape)
Creates a manager for idempotent system startup.
constmanager=createSystemManager(systemConfig);// Methods:manager.getSystem();// Promise<StartedSystem> - Start or get systemmanager.destroySystem();// Promise<void> - Halt and resetmanager.getCurrentSystem();// StartedSystem | null - Sync checkmanager.getStartupErrors();// Map<string, Error> | nullmanager.isStarted();// booleanmanager.config;// TConfig - Exposed for inspectionCreates typed hooks for a system. Always pass the manager.
const{ useSystem, useResource, useSystemStatus, SystemProvider }=createSystemHooks(manager);Returns:
useSystem()- Get entire system (suspends until ready)useResource(id)- Get single resource (suspends until ready)useSystemStatus()- Manual control (doesn't suspend)SystemProvider- Context override for testing
Hook to access the entire started system.
functionComponent(){constsystem=useSystem();// Suspends automatically!// system.counter, system.logger, etc.}Behavior:
- Checks Context first (if
SystemProviderin tree) - Falls back to manager
- Suspends (throws Promise) while starting
- Throws Error if startup failed
- Returns system once ready
Hook to access a single resource with full type inference.
functionComponent(){constcounter=useResource("counter");// Fully typed!counter.increment();}Hook for manual startup control. Does not suspend.
functionComponent(){const{ isIdle, isLoading, isReady, isError, system, errors, startSystem }=useSystemStatus();if(isIdle)return<buttononClick={startSystem}>Start</button>;if(isLoading)return<div>Loading...</div>;if(isError)return<div>Error: {errors}</div>;return<div>Ready!</div>;}Context provider for dependency injection (testing).
<SystemProvidersystem={mockSystem}><Component/></SystemProvider>Important:braided-react is a lifecycle management and dependency injection library, not a state management library.
When you call useResource('counter'), you get the instance of the counter. If properties change, your component will not re-render automatically.
React 18's useSyncExternalStore is perfect for subscribing to external state:
constcounterResource=defineResource({start: ()=>{letcount=0;constlisteners=newSet<()=>void>();return{// For useSyncExternalStoresubscribe: (listener: ()=>void)=>{listeners.add(listener);return()=>listeners.delete(listener);},getSnapshot: ()=>count,// Public APIincrement: ()=>{count++;listeners.forEach((l)=>l());},};},});// In component:functionCounter(){constcounter=useResource("counter");constcount=useSyncExternalStore(counter.subscribe,counter.getSnapshot);return<buttononClick={()=>counter.increment()}>{count}</button>;}You can also use Zustand stores as resources:
constchatStoreResource=defineResource({start: ()=>create((set)=>({messages: [],addMessage: (msg)=>set((state)=>({messages: [...state.messages,msg]})),})),halt: ()=>{},});// In component:functionChat(){constuseStore=useResource("chatStore");constmessages=useStore((state)=>state.messages);return(<div>{messages.map((m)=>(<divkey={m}>{m}</div>))}</div>);}We provide 4 complete examples demonstrating different integration patterns:
1. Basic - useSyncExternalStore ⭐ Start Here
Modern React 18 integration using useSyncExternalStore API for automatic reactivity.
cd examples/basic
npm install && npm run devBest for: Modern React apps, learning the recommended pattern
Zustand stores managed as Braided resources for centralized state management.
cd examples/lazy-start
npm install && npm run devBest for: Apps with complex state management, multiple coordinated stores
Resources communicating through an event bus for loose coupling.
cd examples/singleton-manager
npm install && npm run devBest for: Complex systems, event-driven architectures
4. Outliving React 🔥
System running even when React is unmounted.
cd examples/outliving-react
npm install && npm run devBest for: Music players, WebSocket apps, background sync, game engines
See examples/README.md for detailed comparison.
import{SystemProvider}from"./system";import{startSystem,haltSystem}from"braided";describe("ChatRoom",()=>{test("sends messages",async()=>{// Define mock resourceconstmockTransport=defineResource({start: ()=>({send: vi.fn(),receive: vi.fn(),}),halt: ()=>{},});// Create test configconsttestConfig={
...productionConfig,transport: mockTransport,// Swap just one resource};// Start system with mockconst{ system }=awaitstartSystem(testConfig);render(<SystemProvidersystem={system}><ChatRoom/></SystemProvider>);// Test...fireEvent.click(screen.getByText("Send"));expect(system.transport.send).toHaveBeenCalled();// CleanupawaithaltSystem(testConfig,system);});});test("displays count",()=>{constmockSystem={counter: {count: 42,increment: vi.fn()},}asStartedSystem<typeofconfig>;render(<SystemProvidersystem={mockSystem}><Counter/></SystemProvider>);expect(screen.getByText("42")).toBeInTheDocument();});LazySystemBridgeremoved - Use<Suspense>+useSystemoruseSystemStatuscreateSystemHooksrequires manager - Pass manager as parameterSystemBridgerenamed toSystemProvider- Clearer purpose
const{ SystemBridge, useSystem }=createSystemHooks<typeofconfig>();constmanager=createSystemManager(config);<LazySystemBridgemanager={manager}SystemBridge={SystemBridge}><App/></LazySystemBridge>;constmanager=createSystemManager(config);const{ useSystem }=createSystemHooks(manager);<Suspensefallback={<Loading/>}><App/></Suspense>;constmanager=createSystemManager(config);const{ useSystemStatus }=createSystemHooks(manager);functionApp(){const{ isIdle, isLoading, startSystem }=useSystemStatus();if(isIdle)return<WelcomeScreenonStart={startSystem}/>;if(isLoading)return<LoadingScreen/>;return<ChatRoom/>;}See CHANGELOG.md for detailed migration guide.
Braided React follows the same philosophy as Braided:
- Simple over easy - Minimal API that composes well
- Explicit over implicit - No magic, no scanning, just data
- Data over code - Systems are declared as data structures
- Testable by default - No global state, easy to mock
- Type-safe - Full TypeScript support with inference
- React observes, doesn't own - System lifecycle is independent
React components are observers of your system. They watch for changes and re-render when needed. But they don't control the system's lifecycle. This separation of concerns leads to:
- Simpler components - Just observe and render
- Easier testing - Mock the system, not React
- Better performance - System lives outside React's render cycle
- More flexibility - System can be used outside React
- Braided - The core system composition library
- Braided React - React integration (this library)
ISC