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:

  1. Which events exist by looking at the folder names.
  2. 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 Logic

Static Listener Examples

Example A: Background Logging

src/mod_analytics/@alias/events/user.login/100_log/index.ts
export default function(user: any) {
    console.log(`[Analytics] User logged in: ${user.email}`);
}

Example B: UI Feedback (Toasts)

src/mod_ui_kit/@alias/events/cart.added/200_notify/index.tsx
import { toast } from "@/lib/toast-system";

export default function({ productName }: any) {
    toast.success(`${productName} added to your cart!`);
}

Example C: Chain Reactions

src/mod_logic/@alias/events/order.placed/500_process/index.ts
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.

src/mod_cart/ui/AddToCartButton.tsx
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:

  1. Initial Event: document.save (The trigger)
  2. Listener: Starts the saving process.
  3. Completion Event: document.save.done (Broadcasts the result)
Recommended Async Strategy
// 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.

src/mod_layout/ui/CartBadge.tsx
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.

src/mod_main/uiInit.tsx
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

  1. Numeric Gaps: Use prefixes like 100_, 200_ instead of 001_, 002_ to allow other modules to insert themselves in between.
  2. Naming Matters: Name your listener folders descriptively (e.g., 100_sync_to_localstorage) so other developers understand the impact of your code.