A Composable Architecture library for Svelte 5, inspired by The Composable Architecture (TCA) from Swift/iOS.
Status: ✅ Production-ready with 420+ tests
(state, action, deps) => [newState, effect]$state, $derived)npm install @composable-svelte/core
# or
pnpm add @composable-svelte/core
Note: Package is not yet published to npm. Clone the repo to use it.
import { createStore, Effect } from '@composable-svelte/core';
// 1. Define your state
interface CounterState {
count: number;
isLoading: boolean;
}
// 2. Define your actions
type CounterAction =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'incrementAsync' }
| { type: 'incrementCompleted' };
// 3. Create a reducer
const counterReducer = (
state: CounterState,
action: CounterAction,
deps: {}
): [CounterState, Effect<CounterAction>] => {
switch (action.type) {
case 'increment':
return [{ ...state, count: state.count + 1 }, Effect.none()];
case 'decrement':
return [{ ...state, count: state.count - 1 }, Effect.none()];
case 'incrementAsync':
return [
{ ...state, isLoading: true },
Effect.run(async (dispatch) => {
await new Promise(resolve => setTimeout(resolve, 1000));
dispatch({ type: 'incrementCompleted' });
})
];
case 'incrementCompleted':
return [
{ ...state, count: state.count + 1, isLoading: false },
Effect.none()
];
}
};
// 4. Create the store
const store = createStore({
initialState: { count: 0, isLoading: false },
reducer: counterReducer,
dependencies: {}
});
<script lang="ts">
import { store } from './counter-store';
</script>
<div>
<h1>Count: {store.state.count}</h1>
<button onclick={() => store.dispatch({ type: 'increment' })}>
+
</button>
<button onclick={() => store.dispatch({ type: 'decrement' })}>
-
</button>
<button
onclick={() => store.dispatch({ type: 'incrementAsync' })}
disabled={store.state.isLoading}
>
Async +
</button>
</div>
The store holds your app state and provides:
state: Reactive Svelte 5 runedispatch(action): Send actions to the reducersubscribe(listener): Listen to state changesconst store = createStore({
initialState,
reducer,
dependencies
});
// Access state (reactive)
console.log(store.state.count);
// Dispatch actions
store.dispatch({ type: 'increment' });
Pure functions that take state + action and return new state + effects:
const reducer = (state, action, deps) => {
// Return [newState, effect]
return [newState, Effect.none()];
};
Declarative side effects as data structures:
// Run async work
Effect.run(async (dispatch) => {
const data = await fetch('/api/data');
dispatch({ type: 'dataLoaded', data });
});
// Fire and forget
Effect.fireAndForget(async () => {
await analytics.track('button_clicked');
});
// Batch multiple effects
Effect.batch(
Effect.run(/* ... */),
Effect.fireAndForget(/* ... */)
);
// No effect
Effect.none();
Nest reducers to build complex features:
import { scope, combineReducers } from '@composable-svelte/core';
// Compose child reducer into parent
const appReducer = scope(
// Lens: extract child state
(state) => state.counter,
// Update: set child state
(state, counter) => ({ ...state, counter }),
// Extract child action
(action) => action.type === 'counter' ? action.action : null,
// Embed child action
(childAction) => ({ type: 'counter', action: childAction }),
// Child reducer
counterReducer
);
State-driven navigation with tree-based destinations:
import { ifLet, PresentationAction } from '@composable-svelte/core';
import { Modal } from '@composable-svelte/core/navigation-components';
interface AppState {
destination: AddItemState | null;
}
// In reducer
case 'addButtonTapped':
return [
{ ...state, destination: { title: '', description: '' } },
Effect.none()
];
// In component
{#if scopedStore}
<Modal store={scopedStore}>
<AddItemForm />
</Modal>
{/if}
Use TestStore for exhaustive action testing:
import { createTestStore } from '@composable-svelte/core';
const store = createTestStore({
initialState: { count: 0 },
reducer: counterReducer
});
// Test action
await store.send({ type: 'increment' }, (state) => {
expect(state.count).toBe(1);
});
// Test async effects
await store.send({ type: 'incrementAsync' }, (state) => {
expect(state.isLoading).toBe(true);
});
await store.receive({ type: 'incrementCompleted' }, (state) => {
expect(state.count).toBe(1);
expect(state.isLoading).toBe(false);
});
import { createLiveAPI } from '@composable-svelte/core/api';
const api = createLiveAPI({
baseURL: 'https://api.example.com',
interceptors: {
request: async (config) => {
config.headers.Authorization = `Bearer ${token}`;
return config;
}
}
});
// In reducer
Effect.run(async (dispatch) => {
const result = await deps.api.get('/users');
if (result.ok) {
dispatch({ type: 'usersLoaded', users: result.data });
}
});
import { createLiveWebSocket } from '@composable-svelte/core/websocket';
const ws = createLiveWebSocket({
url: 'wss://api.example.com/ws',
reconnect: {
enabled: true,
maxAttempts: 5,
delayMs: 1000
},
heartbeat: {
enabled: true,
intervalMs: 30000
}
});
// In reducer
Effect.run(async (dispatch) => {
ws.on('message', (data) => {
dispatch({ type: 'messageReceived', data });
});
await ws.connect();
});
import {
createSystemClock,
createLocalStorage
} from '@composable-svelte/core/dependencies';
const dependencies = {
clock: createSystemClock(),
storage: createLocalStorage<User>({
prefix: 'app:',
validator: isUser
})
};
// In reducer
deps.storage.setItem('user', currentUser);
const timestamp = deps.clock.now();
Explore working examples in the examples/ directory:
# Run styleguide
cd examples/styleguide
pnpm install
pnpm dev
Inspired by The Composable Architecture (TCA):
| TCA (Swift) | Composable Svelte |
|---|---|
@Reducer macro |
Manual reducer functions |
@Presents macro |
destination: T | null field |
Scope |
scope() / ifLet() operators |
@Dependency(\.dismiss) |
deps.dismiss() |
TestStore |
TestStore (similar API) |
| SwiftUI views | Svelte components |
# Install dependencies
pnpm install
# Run tests
pnpm test
# Type check
pnpm typecheck
# Run examples
cd examples/styleguide
pnpm dev
Completed Phases:
Test Coverage: 420+ tests across all modules
This project follows a specification-first approach. See CLAUDE.md for contributor guidelines.
MIT
Heavily inspired by The Composable Architecture by Point-Free. Adapted for Svelte 5 and TypeScript with love.