A type-safe event broker for frontend applications with first-class TypeScript and React support.
- Why EventFlow?
- Features
- Installation
- Quick Start
- Core API
- React Integration
- Advanced Usage
- Error Handling
- Middleware System
- TypeScript Usage
- Best Practices
- Examples
- Contributing
- TODO
- License
Modern frontend applications often need to handle complex state management and communication between components that aren't directly connected. While solutions like Redux or global state management tools work well for application state, they can be overkill for simple event-based communication or introduce unnecessary complexity.
EventFlow solves several common challenges in frontend development:
- Decoupled Communication: Enable components, modules, and services to communicate without direct dependencies or prop drilling.
- Type Safety: Unlike traditional event emitters, EventFlow ensures your events and their payloads are fully typed.
- Cross-Cutting Concerns: Handle application-wide concerns like logging, analytics, or error tracking through the middleware system.
- Framework Independence: While providing first-class React support, the core package works with any JavaScript environment.
- Predictable Error Handling: Configure how your application behaves when events fail to emit or process.
- 🎯 Type-safe: Full TypeScript support with type inference
- ⚡ Lightweight: Zero dependencies for core functionality
- 🔌 Middleware Support: Extensible through middleware system
- 🛡️ Error Handling: Configurable error policies
- ⚛️ React Integration: Ready-to-use React hooks and context
- 🎠Framework Agnostic: Core package works anywhere
- 📦 Tree-shakeable: Only bundle what you use
- 🔍 DevTools: Built-in logger middleware for debugging
# Using npm
npm install @thesmilingsloth/eventflow-core
npm install @thesmilingsloth/eventflow-react # Optional React integration
# Using yarn
yarn add @thesmilingsloth/eventflow-core
yarn add @thesmilingsloth/eventflow-react # Optional React integration
# Using pnpm
pnpm add @thesmilingsloth/eventflow-core
pnpm add @thesmilingsloth/eventflow-react # Optional React integrationimport { createEventBroker } from "@thesmilingsloth/eventflow-core";
interface UserEvents {
"user:login": {
userId: string;
timestamp: number;
};
}
interface AppEvents {
"app:notification": {
message: string;
type: "success" | "error";
};
"app:themeChange": "light" | "dark";
}
// Define your events
interface MyEvents extends UserEvents, AppEvents {}
// Create broker
const broker = createEventBroker<MyEvents>();
// Subscribe to events
const unsubscribe = broker.on("user:login", (data) => {
console.log(`User ${data.userId} logged in`);
});
// Emit events
broker.emit("user:login", {
userId: "123",
timestamp: Date.now(),
});
// Cleanup
unsubscribe();import {
EventBrokerProvider,
useEventListener,
useEventEmitter,
useEventState,
} from "@thesmilingsloth/eventflow-react";
import { createEventBroker } from "@thesmilingsloth/eventflow-core";
const broker = createEventBroker<MyEvents>();
// Provider setup
function App() {
return (
<EventBrokerProvider broker={broker}>
<LoginButton />
<ThemeToggle />
<NotificationListener />
</EventBrokerProvider>
);
}
// Emit events
function LoginButton() {
const emitLogin = useEventEmitter("user:login");
return (
<button
onClick={() =>
emitLogin({
userId: "123",
timestamp: Date.now(),
})
}
>
Login
</button>
);
}
// Listen to events
function NotificationListener() {
useEventListener("app:notification", (data) => {
alert(data.message);
});
return null;
}
// Manage state with events
function ThemeToggle() {
const [theme, setTheme] = useEventState("app:themeChange", "light");
return (
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
Toggle Theme
</button>
);
}Creates a new event broker instance with optional configuration.
interface EventBrokerOptions<T extends EventMap> {
logger?: boolean;
maxListeners?: number;
errorPolicy?: ErrorPolicy<T>;
middlewares?: Middleware<T>[];
}
const broker = createEventBroker<MyEvents>({
logger: true, // Enable console logging
maxListeners: 10, // Max listeners per event
errorPolicy: {
onListenerError: "continue",
onEmitError: "stop",
onMiddlewareError: (error, event) => {
// Custom error handling
},
},
});// Subscribe to events
const unsubscribe = broker.on("eventName", (data) => {});
// One-time subscription
broker.once("eventName", (data) => {});
// Emit events
broker.emit("eventName", eventData);
// Unsubscribe
broker.off("eventName", listener);
// Clear all subscriptions
broker.clear();
// Add middleware
const removeMiddleware = broker.use(middleware);Subscribe to events in components.
useEventListener("eventName", (data) => {
// Handle event
});Get a typed emit function.
const emit = useEventEmitter("eventName");
emit(eventData); // Type-safe event emissionManage state through events.
const [state, setState] = useEventState("statefulEvent", initialState);Subscribe to an event once.
useEventOnce("eventName", (data) => {
// Handles event once
});const analyticsMiddleware: Middleware<MyEvents> = (next) => (event) => {
// Before event
trackEvent(event.name, event.data);
// Process event
next(event);
// After event
console.log("Event processed:", event.name);
};
broker.use(analyticsMiddleware);const broker = createEventBroker<MyEvents>({
errorPolicy: {
// Continue on listener errors
onListenerError: "continue",
// Stop on emit errors
onEmitError: "stop",
// Custom middleware error handling
onMiddlewareError: (error, event) => {
reportError(error);
console.error(`Error in middleware for ${event.name}:`, error);
},
},
});interface MyEvents {
// Simple events
"user:logout": void;
// Events with data
"user:login": {
username: string;
timestamp: number;
};
// Union types
"app:notification": {
type: "success" | "error" | "info";
message: string;
};
// Generic events
"data:loaded": {
data: T;
source: string;
};
}// Domain-specific event interfaces
interface UserEvents {
"user:created": { id: string; email: string };
"user:updated": { id: string; changes: Partial<User> };
"user:deleted": { id: string };
}
interface AuthEvents {
"auth:login": { email: string; password: string };
"auth:logout": void;
"auth:sessionExpired": { reason: string };
}
interface NotificationEvents {
"notification:show": { message: string; type: "success" | "error" | "info" };
"notification:clear": void;
}
// Compose your application events
type AppEvents = UserEvents & AuthEvents & NotificationEvents;
const broker = createEventBroker<AppEvents>();// Middleware composition
const withTiming = (next: MiddlewareNext) => (event: Event) => {
const start = performance.now();
next(event);
const duration = performance.now() - start;
console.log(`Event ${event.name} took ${duration}ms`);
};
const withRetry =
(attempts: number) => (next: MiddlewareNext) => async (event: Event) => {
for (let i = 0; i < attempts; i++) {
try {
await next(event);
return;
} catch (error) {
if (i === attempts - 1) throw error;
console.warn(`Retrying event ${event.name}, attempt ${i + 1}`);
}
}
};
broker.use(withTiming);
broker.use(withRetry(3));// Create domain-specific brokers
const userBroker = broker.namespace<UserEvents>("user");
const authBroker = broker.namespace<AuthEvents>("auth");
// Type-safe event handling for specific domains
userBroker.on("created", (user) => {
// Handle user creation
});
authBroker.on("sessionExpired", (data) => {
// Handle session expiration
});-
Memory Leaks
Problem: Event listeners not being cleaned up Solution: Always use the unsubscribe function or useEventListener hook -
Type Inference Issues
Problem: TypeScript not inferring event types correctly Solution: Ensure your event map interface follows the correct structure -
Middleware Order
Problem: Middleware not executing in expected order Solution: Middleware executes in the order they're added. Order them from generic to specific
Contributions are welcome!
# Clone the repo
git clone https://github.com/thesmilingsloth/eventflow.git
# Install dependencies
pnpm install
# Build packages
pnpm build- We use Biome for formatting and linting
- Follow the existing code style
- Write meaningful commit messages
- Keep PRs focused and atomic
interface FormEvents {
"form:submit": { formId: string; data: Record<string, unknown> };
"form:validate": { formId: string; field: string; value: unknown };
"form:error": { formId: string; errors: Record<string, string[]> };
"form:success": { formId: string; data: Record<string, unknown> };
}
// Form validation middleware
const validationMiddleware: Middleware<FormEvents> =
(next) => async (event) => {
if (event.name === "form:submit") {
const errors = await validateForm(event.data);
if (Object.keys(errors).length > 0) {
broker.emit("form:error", { formId: event.data.formId, errors });
return;
}
}
next(event);
};interface AnalyticsEvents {
"analytics:pageView": { path: string; title: string };
"analytics:event": { category: string; action: string; label?: string };
"analytics:timing": { category: string; variable: string; value: number };
}
// Analytics middleware
const analyticsMiddleware: Middleware<AnalyticsEvents> = (next) => (event) => {
// Send to analytics service
if (event.name.startsWith("analytics:")) {
sendToAnalytics(event.name, event.data);
}
next(event);
};MIT