Store & Dependency Injection
Managing global services and cross-module dependencies using the ValueStore pattern.
In complex applications (like our e-commerce demo), you often need to share separate objects (like "controllers" or "helpers") (business logic classes) across multiple modules without creating hard dependencies (cyclic imports).
JopiJS provides a lightweight Dependency Injection (DI) system via the valueStore.
Why use ValueStore?
- Decoupling Modules: A module can get an object instance, without knowing which module provide it.
- Singleton Management: Ensures a single instance of an object exists for the app lifecycle.
- Lazy Loading: Objects can be instantiated only when first requested.
- Testing: Makes it easy to mock objects during tests.
1. Registering an Object
Typically, a module (the "Provider") registers its objects during the UI Initialization phase. This is done in uiInit.tsx.
We use uiApp.valueStore.addValueProvider(key, builder) to register a factory function.
Example: mod_cart/uiInit.tsx
import { JopiUiApplication } from "jopijs/ui";
import CartController from "@/lib/shop.ui.cartController";
export default function (uiApp: JopiUiApplication) {
// Register the "shop.ui.cartController" object.
// The arrow function will only be called ONCE, the first time someone needs it.
uiApp.valueStore.addValueProvider("shop.ui.cartController", () => {
return new CartController(uiApp.valueStore);
});
}Note: The key string "shop.ui.cartController" acts as the interface contract. It effectively says "I provide an object that matches the Cart Controller structure".
2. Consuming an Object
Any other module (the "Consumer") can retrieve the object using valueStore.getValue(key).
Example: A UI Component in mod_template
import { useJopiUi } from "jopijs/ui";
import type CartController from "@/lib/shop.ui.cartController";
// Note: checking if CartController is only a type import is best practice
// to avoid bundling the actual class if not needed.
export function CartWidget() {
const uiApp = useJopiUi();
// Retrieve the object instance
const cart = uiApp.valueStore.getValue<CartController>("shop.ui.cartController");
if (!cart) {
return null; // Handle case where the module is not installed
}
// Use the object
return <div>{cart.itemCount} items</div>;
}3. Best Practices
Logic in uiInit.tsx
Always put your object registration in uiInit.tsx. This file is guaranteed to run before any React component is mounted on the client side, and before rendering on the server side.
Use Type-Only Imports for Contracts
When consuming an object from another module, try to import only the Type (interface or class type) to keep the hard dependency minimal.
import type { CartController } from "my-other-module";Uniqueness of Keys
Use a namespaced convention for your keys to avoid collisions:
myModule.myObjectshop.cartauth.provider
Advanced: Reactive State Management
JopiJS offers a powerful hook useStoreValue that acts as a Global Store.
Unlike complex state management libraries (Redux, etc.), it is designed to be terribly simple: just use a key to share data responsively across your app.
Why it's useful:
- Zero Boilerplate: No need to wrap your app in contexts or create complex store definitions.
- Global Persistence: Data remains available even if all components using it are unmounted.
- Decoupled State: Share state between completely unrelated modules (e.g.
mod_cartupdates state read bymod_header).
useStoreValue<T>(key)
This hook returns a tuple [value, setValue] and automatically triggers a re-render of your component when the underlying value changes in the store. It can effectively be used as a global store.
Example: Reactive usage in mod_cart
import { useStoreValue } from "jopijs/ui";
import type CartController from "@/lib/shop.ui.cartController";
export function useCartHook() {
// 1. Get the value and a setter
// 2. Component will re-render if "shop.ui.cartController" changes
const [controller, setController] = useStoreValue<CartController>("shop.ui.cartController");
if (!controller) return null;
return <div>Cart ready: {controller.isReady ? 'Yes' : 'No'}</div>;
}Since the valueStore is global, replacing the value via setController (or by using uiApp.valueStore.setValue) will update all components using this hook across the application.