
ReactJS
Beyond basic event handling, production applications require patterns for composing events, custom event systems, and complex event flows. Understand how to design scalable event architectures.
Complex event handling often requires composing multiple handlers. Higher-order functions create reusable event handler factories that encapsulate cross-cutting concerns like logging, validation, and error handling.
The composition pattern allows chaining handlers, preventing default behavior conditionally, and maintaining single responsibility. Each handler does one thing well and can be composed with others.
// Event composition pattern: create specialized handlers from primitives
interface EventComposer {
compose: (...handlers: ((e: any) => void)[]) => (e: any) => void;
withLogging: (handler: (e: any) => void) => (e: any) => void;
withValidation: (
handler: (e: any) => void,
validator: (e: any) => boolean
) => (e: any) => void;
}
const eventComposer: EventComposer = {
// Compose multiple handlers to run in sequence
compose:
(...handlers) =>
(e) => {
handlers.forEach((h) => h(e));
},
// Add logging to any handler
withLogging: (handler) => (e) => {
console.log("Event:", e.type, "Target:", e.target);
handler(e);
},
// Add validation gate
withValidation: (handler, validator) => (e) => {
if (validator(e)) {
handler(e);
}
},
};
// Example: composed form handler with validation and logging
function AdvancedForm() {
const handleFormValidation = (e: React.FormEvent) => {
const form = e.currentTarget as HTMLFormElement;
const data = new FormData(form);
return Array.from(data.entries()).every(([key, value]) => value);
};
const handleFormSubmit = (e: React.FormEvent) => {
console.log("Form submitted successfully");
};
const handleFormError = (e: React.FormEvent) => {
console.error("Validation failed");
};
// Compose handlers: first validate, then submit or error
const validatedSubmit = eventComposer.withValidation(
eventComposer.withLogging(handleFormSubmit),
handleFormValidation
);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!handleFormValidation(e)) {
handleFormError(e);
} else {
validatedSubmit(e);
}
};
return (
<form onSubmit={handleSubmit}>
<input name="email" required />
<input name="password" required />
<button type="submit">Submit</button>
</form>
);
}<details>
<summary>📚 More Examples</summary>
// Example 2: Chain-of-responsibility pattern for event handling
interface EventHandler {
handle(e: React.MouseEvent): boolean; // Return true if handled
setNext(handler: EventHandler): void;
}
class ClickHandler implements EventHandler {
private next: EventHandler | null = null;
handle(e: React.MouseEvent): boolean {
if (e.button === 0) {
console.log("Left click handled");
return true;
}
return this.next ? this.next.handle(e) : false;
}
setNext(handler: EventHandler) {
this.next = handler;
}
}
class RightClickHandler implements EventHandler {
private next: EventHandler | null = null;
handle(e: React.MouseEvent): boolean {
if (e.button === 2) {
console.log("Right click handled");
return true;
}
return this.next ? this.next.handle(e) : false;
}
setNext(handler: EventHandler) {
this.next = handler;
}
}
// Example 3: Decorator pattern for adding behavior
function withPreventDefault(
handler: (e: React.FormEvent) => void
): (e: React.FormEvent) => void {
return (e: React.FormEvent) => {
e.preventDefault();
handler(e);
};
}
function withStopPropagation(
handler: (e: React.MouseEvent) => void
): (e: React.MouseEvent) => void {
return (e: React.MouseEvent) => {
e.stopPropagation();
handler(e);
};
}
function withErrorBoundary(
handler: (e: React.SyntheticEvent) => void
): (e: React.SyntheticEvent) => void {
return (e: React.SyntheticEvent) => {
try {
handler(e);
} catch (error) {
console.error("Handler error:", error);
}
};
}
// Decorate handler with multiple behaviors
const robustSubmit = withPreventDefault(
withErrorBoundary(
(e: React.FormEvent) => {
console.log("Form submitted");
}
)
);</details>
Building applications with many components often requires a global event system for communication without prop drilling. The Pub-Sub (publish-subscribe) pattern decouples event producers from consumers.
// Global event bus using Pub-Sub pattern
interface Subscription {
unsubscribe: () => void;
}
interface EventBus {
on<T>(event: string, handler: (data: T) => void): Subscription;
emit<T>(event: string, data: T): void;
once<T>(event: string, handler: (data: T) => void): Subscription;
}
function createEventBus(): EventBus {
const handlers = new Map<string, Set<Function>>();
return {
on<T>(event: string, handler: (data: T) => void): Subscription {
if (!handlers.has(event)) {
handlers.set(event, new Set());
}
handlers.get(event)!.add(handler);
return {
unsubscribe: () => {
handlers.get(event)?.delete(handler);
},
};
},
emit<T>(event: string, data: T): void {
handlers.get(event)?.forEach((handler) => {
try {
(handler as (data: T) => void)(data);
} catch (error) {
console.error(`Error in event handler for ${event}:`, error);
}
});
},
once<T>(event: string, handler: (data: T) => void): Subscription {
const wrappedHandler = (data: T) => {
handler(data);
subscription.unsubscribe();
};
const subscription = this.on<T>(event, wrappedHandler);
return subscription;
},
};
}
// Usage: Global event bus for app communication
const eventBus = createEventBus();
interface AuthEvent {
userId: string;
timestamp: number;
}
// Publisher: Authentication module
function LoginButton() {
const handleLogin = () => {
eventBus.emit<AuthEvent>("auth:login", {
userId: "user123",
timestamp: Date.now(),
});
};
return <button onClick={handleLogin}>Login</button>;
}
// Subscriber: Notification module
function NotificationCenter() {
useEffect(() => {
const subscription = eventBus.on<AuthEvent>("auth:login", (data) => {
console.log(`User ${data.userId} logged in`);
});
return () => subscription.unsubscribe();
}, []);
return null;
}
// Subscriber: Analytics module
function Analytics() {
useEffect(() => {
const loginSub = eventBus.on<AuthEvent>("auth:login", (data) => {
console.log("Track: user login", data);
});
const logoutSub = eventBus.on<AuthEvent>("auth:logout", (data) => {
console.log("Track: user logout", data);
});
return () => {
loginSub.unsubscribe();
logoutSub.unsubscribe();
};
}, []);
return null;
}<details>
<summary>📚 More Examples</summary>
// Example 2: Typed event bus using generics
interface EventMap {
"user:created": { userId: string; email: string };
"user:deleted": { userId: string };
"post:published": { postId: string; title: string };
}
interface TypedEventBus {
on<E extends keyof EventMap>(
event: E,
handler: (data: EventMap[E]) => void
): Subscription;
emit<E extends keyof EventMap>(event: E, data: EventMap[E]): void;
}
function createTypedEventBus(): TypedEventBus {
const handlers = new Map<string, Set<Function>>();
return {
on<E extends keyof EventMap>(
event: E,
handler: (data: EventMap[E]) => void
): Subscription {
if (!handlers.has(event as string)) {
handlers.set(event as string, new Set());
}
handlers.get(event as string)!.add(handler);
return {
unsubscribe: () => {
handlers.get(event as string)?.delete(handler);
},
};
},
emit<E extends keyof EventMap>(event: E, data: EventMap[E]): void {
handlers.get(event as string)?.forEach((handler) => {
try {
handler(data);
} catch (error) {
console.error(`Error in ${String(event)} handler:`, error);
}
});
},
};
}
// Type-safe usage
const bus = createTypedEventBus();
bus.on("user:created", (data) => {
// data is typed as { userId: string; email: string }
console.log("User created:", data.email);
});
bus.emit("user:created", {
userId: "123",
email: "user@example.com",
});
// This would be a TypeScript error:
// bus.emit("user:created", { userId: "123" }); // Missing email</details>
Event handlers need careful management of context and binding, especially in class components or when using callbacks. Modern React solutions use hooks to manage event handler dependencies.
// Context management in event handlers
interface User {
id: string;
name: string;
}
function UserActions() {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(false);
// useCallback ensures handler identity is stable
const handleSaveUser = useCallback(
async (updatedUser: User) => {
setLoading(true);
try {
const response = await fetch(`/api/users/${user?.id}`, {
method: "PUT",
body: JSON.stringify(updatedUser),
});
const result = await response.json();
setUser(result);
} catch (error) {
console.error("Failed to save user:", error);
} finally {
setLoading(false);
}
},
[user?.id]
);
const handleDeleteUser = useCallback(async () => {
if (!user) return;
setLoading(true);
try {
await fetch(`/api/users/${user.id}`, { method: "DELETE" });
setUser(null);
} catch (error) {
console.error("Failed to delete user:", error);
} finally {
setLoading(false);
}
}, [user?.id]);
return (
<div>
{user && (
<>
<UserProfile user={user} onSave={handleSaveUser} />
<button onClick={handleDeleteUser} disabled={loading}>
{loading ? "Deleting..." : "Delete User"}
</button>
</>
)}
</div>
);
}Understanding event flow (bubbling vs capturing phases) enables sophisticated event handling patterns. Capturing phase listeners execute first, allowing intervention before bubbling.
// Event capturing for modal/tooltip positioning
function PositionedTooltip() {
const [position, setPosition] = useState({ x: 0, y: 0 });
const [visible, setVisible] = useState(false);
// Capture phase handler to detect all clicks
const handleCaptureClick = useCallback(
(e: React.MouseEvent) => {
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
setPosition({
x: e.clientX - rect.left,
y: e.clientY - rect.top,
});
},
[]
);
return (
<div
onClickCapture={handleCaptureClick}
onClick={() => setVisible(!visible)}
style={{ position: "relative" }}
>
<button>Hover me</button>
{visible && (
<div
style={{
position: "absolute",
left: `${position.x}px`,
top: `${position.y}px`,
}}
>
Tooltip
</div>
)}
</div>
);
}Ready to practice? Click Events (Advanced) | Challenges
Resources
Ojasa Mirai
Master AI-powered development skills through structured learning, real projects, and verified credentials. Whether you're upskilling your team or launching your career, we deliver the skills companies actually need.
Learn Deep • Build Real • Verify Skills • Launch Forward