Events & Listeners
Learn how to implement decoupled communication between modules using JopiJS's event system.
Overview
In a modular application, modules shouldn't call each other directly (which creates "tight coupling"). Instead, they should communicate via Events. One module "emits" a message, and any other module can "listen" and react to it.
In this guide, you will learn how to:
- Define a new event type.
- Create static listeners that run automatically.
- Listen for events inside React components.
- Emit events with custom data.
1. Static Listeners & Discoverability
In JopiJS, events are not just lines of code; they are part of your project structure. To define an event or a listener, you create folders in the @alias/events/ directory.
Why this approach? (Maintainability)
By using the file system as your event registry, your application becomes self-documenting. You can immediately see:
- Which events exist by looking at the folder names.
- Who is listening by looking at the module sub-folders.
This eliminates the "hidden logic" problem common in traditional event emitters.
How to Create a Listener
Create a sub-folder inside an event folder using a numeric prefix for ordering. Any module can add a listener to any event simply by replicating the event's folder path.
Structure Example:
src/
├── mod_cart/
│ └── @alias/events/cart.product_added/ # The Event Name
│ └── (empty)
└── mod_analytics/
└── @alias/events/cart.product_added/
└── 100_log_event/ # 100 = Priority/Order
└── index.ts # The LogicStatic Listener Examples
Example A: Background Logging
export default function(user: any) {
console.log(`[Analytics] User logged in: ${user.email}`);
}Example B: UI Feedback (Toasts)
import { toast } from "@/lib/toast-system";
export default function({ productName }: any) {
toast.success(`${productName} added to your cart!`);
}Example C: Chain Reactions
import eventEmailSend from "@/events/email.send";
export default function(order: any) {
// Trigger another event as a consequence
eventEmailSend.send({ to: order.email, subject: "Success!" });
}SSR Compatibility:
Static listeners run in both the Browser and the Server (SSR). If you use browser-only APIs (like window), wrap them in a check or use useStaticEffect.
2. Emitting an Event
To send a message, you import the event from the @/events/ virtual path and call the .send() method.
import eventProductAdded from "@/events/cart.product_added";
export function AddToCart({ productId }) {
const handleClick = () => {
// Broadcast the news!
// You can send any object as data.
eventProductAdded.send({ id: productId, timestamp: Date.now() });
};
return <button onClick={handleClick}>Add to Cart</button>;
}3. Synchronous Nature & Async Flows
JopiJS events are synchronous by design. When you call .send(), all registered listeners are executed immediately one after another. They are intended to be triggers (fire-and-forget signals) rather than heavy workers.
Handling Asynchronous Tasks
If a listener needs to perform an asynchronous operation (like fetching data or a timeout), it should initiate the task and return instantly.
The recommended strategy to notify the application when the work is finished is to emit a second event with the suffix .done.
The "Done" Pattern:
- Initial Event:
document.save(The trigger) - Listener: Starts the saving process.
- Completion Event:
document.save.done(Broadcasts the result)
// Inside a listener (Static)
import eventSaveDone from "@/events/document.save.done";
export default function(data) {
// Start async work (e.g., API call)
saveToDatabase(data).then(() => {
// When finished, emit the "done" event
eventSaveDone.send({ status: "success" });
});
}Why synchronous? This approach keeps your UI highly responsive and prevents "hanging" events. It forces a clear execution flow where components only care about signals, not implementation details.
4. Listening in React Components
Sometime you want a UI component to update instantly when an event occurs without using a complex global state manager (like Redux or Zustand).
JopiJS provides the reactListener method, which handles the subscription and cleanup (unmount) automatically for you.
import eventProductAdded from "@/events/cart.product_added";
import { useState } from "react";
export default function CartBadge() {
const [count, setCount] = useState(0);
// This listener is automatically mounted/unmounted
// with the component.
eventProductAdded.reactListener((data) => {
setCount(prev => prev + 1);
console.log("Added product details:", data);
});
return <div className="badge">Items: {count}</div>;
}5. Event Context (this)
By default, events do not have a specific this context. However, you can bind a context to an event, which can then be accessed inside your listeners.
Setting Context for a Single Event
You can set the this value for a specific event using .setThisValue().
import eventProductAdded from "@/events/cart.product_added";
// In your initialization logic
eventProductAdded.setThisValue(valueForThis);Inside the listeners for eventProductAdded, this will now refer to valueForThis.
Setting a Global Default Context
You can also set a default this value for all events that don't have a specific one assigned. This is particularly useful for making the main application instance available to all event listeners.
import { setEventsThisValue } from "jopijs/ui";
// Typically in your UI initialization file (uiInit.tsx)
setEventsThisValue(valueForThis);Initialization Example (uiInit.tsx)
Here is a complete example showing how to configure the event context during the module initialization phase.
import { JopiUiApplication, setEventsThisValue } from "jopijs/ui";
import { EventPriority } from "jopi-toolkit/jk_events";
export default function(uiApp: JopiUiApplication) {
// 1. Set the global context for all static events
// This allows listeners to access the 'uiApp' instance via 'this'
setEventsThisValue(uiApp);
// 2. Other initialization logic...
uiApp.addUiInitializer(EventPriority.normal, () => {
console.log("UI Application initialized and event context set.");
});
}Server-Side Note:
This configuration is ignored on the server-side. It ensures that the this context is properly set for client-side interactions, while having no effect during Server-Side Rendering (SSR).
Best Practices
- Numeric Gaps: Use prefixes like
100_,200_instead of001_,002_to allow other modules to insert themselves in between. - Naming Matters: Name your listener folders descriptively (e.g.,
100_sync_to_localstorage) so other developers understand the impact of your code.