@razaman2/data-manager
v3.3.5
Published
A highly robust, reactive, and path-based data management container for TypeScript and JavaScript. Built on top of `@razaman2/object-manager`, it provides a seamless way to manipulate deeply nested object states using string paths, while emitting fine-gra
Readme
@razaman2/data-manager
A highly robust, reactive, and path-based data management container for TypeScript and JavaScript. Built on top of @razaman2/object-manager, it provides a seamless way to manipulate deeply nested object states using string paths, while emitting fine-grained events for granular reactivity.
Features
- Dot-Notation Paths: Access or update deep nested properties without manual traversal (e.g.,
user.profile.settings.theme). - Reactive Event Emitters: Granular pub/sub hooks into specific state paths whenever they mutate.
- Smart Merging: Intelligently handles
defaultDataversus currentdata, prioritizing structural stability. - Ignored Paths: Protect specific keys or paths from being accessed or modified via regex, strings, or functions.
- TypeScript Ready: Full generic
<T>support allows strict type enforcing on your state container.
Installation
npm install @razaman2/data-manager @razaman2/object-manager @razaman2/event-emitterCore Concepts
The DataManager works by managing a source of truth data object, but it also respects a defaultData payload. When the manager initializes, it deep-merges defaultData and the provided data. If state is ever wiped via replaceData(), it falls back exactly to the defaultData structure natively.
import DataManager from "@razaman2/data-manager";
const manager = new DataManager({
defaultData: { count: 0, theme: "light" }, // Fallback state
data: { theme: "dark" } // User's active state
});
// Merged result: { count: 0, theme: "dark" }TypeScript Usage
You can pass a custom type interface T into DataManager<T> to strictly type the underlying object graph.
interface AppState {
user: {
id: number;
role: "admin" | "user";
};
preferences: Record<string, boolean>;
}
// Initializing strictly
const store = new DataManager<AppState>({
data: {
user: { id: 1, role: "admin" },
preferences: { notifications: true }
}
});Detailed API Reference
getData(path?: string | number, alternative?: any): any
Fetches data from the state tree.
path: A dot-notation string indicating the deep path to fetch. Omitting it returns the entire state object.alternative: A fallback value if the target path isundefined.
store.getData(); // Full object
store.getData("user.role"); // "admin"
store.getData("preferences.newsletter", false); // returns false (alternative fallback)setData(path: string | number, value: any): this
setData(value: any): this
Sets or deeply updates the object state. If updating via an object, it correctly patches only the paths provided.
// Path-based assignment
store.setData("preferences.newsletter", true);
store.setData("user.roles.0", "superadmin");
// Deep Object Patching
store.setData({
user: { id: 2 }
}); // Doesn't destroy `user.role`, just patches `id`replaceData(additions?: any, removals?: Set<string | number> | Array<string | number>): this
Completely replaces the internal data tree, reverting first to defaultData and then assigning the new additions.
additions: The new data object to cleanly adopt.removals: An Array or Set of specific dot-notation paths to rip out before applying new data.
// Resets to defaultData, completely destroying current state
store.replaceData();
// Replace state entirely with new data
store.replaceData({ user: { id: 99, role: "user" } });
// Selectively remove specific paths while keeping the rest intact
store.replaceData(null, ["user.preferences"]); setIgnoredPath(match: string | RegExp | ((path: string) => boolean)): this
Blocks the underlying object-manager from traversing into or mutating paths that match the specified constraints.
// Block by Exact Match
store.setIgnoredPath("user.id");
// Block by Regex (protects anything inside preferences)
store.setIgnoredPath(/^preferences\..*/);
// Block by evaluation function
store.setIgnoredPath((path) => path.includes("secret"));Advanced Reactivity: Event Hooks
DataManager automatically emits changes whenever parts of the object are modified, making it perfectly suited as a global state manager for UI frameworks. To enable this, pass an @razaman2/event-emitter into the configuration.
It triggers two distinct events:
localWrite: Triggers whenever any state is set.localWrite.<path>: Triggers for every granular path that is updated.
import EventEmitter from "@razaman2/event-emitter";
const notifications = new EventEmitter();
const manager = new DataManager({
notifications,
data: { title: "Hello", views: 0 }
});
// Hook into specific property
notifications.on("localWrite.views", (newViews, oldViews, instance) => {
console.log(`Views updated from ${oldViews} to ${newViews}`);
});
manager.setData("views", 1);
// Console: "Views updated from 0 to 1"Internal Logging
If you want to trace state mutations during development, simply pass the logging: true flag in the DataClient config.
const debugManager = new DataManager({
logging: true,
data: { app: "running" }
});Every setData execution will result in a visual console log tracing the previous and modified states.
Cookbook / Real-World Examples
Example 1: Redux-Style State Container
You can comfortably use DataManager similarly to Redux, where a singular payload triggers updates.
const store = new DataManager({
data: { items: [], total: 0 }
});
const dispatchAction = (action: { type: string, payload: any }) => {
switch (action.type) {
case "ADD_ITEM":
// Fetch existing
const currentItems = store.getData("items");
// Set individually using paths
store.setData("items", [...currentItems, action.payload]);
store.setData("total", store.getData("total") + action.payload.price);
break;
}
};
dispatchAction({ type: "ADD_ITEM", payload: { name: "Apple", price: 5 } });Example 2: API Payload Settings Merge
A fantastic use-case of DataManager is resolving partial settings fetched from a remote server without destroying local fallbacks.
const localDefaults = {
volume: 100,
graphics: {
resolution: "1920x1080",
vsync: true,
advanced: { shadows: "high" }
}
};
const userSettings = new DataManager({
defaultData: localDefaults,
});
// We fetch completely new settings from an API
const apiSettingsPayload = {
graphics: { resolution: "2560x1440" }
// Notice how they omitted volume, vsync, and advanced properties.
};
// Reset the data completely (to defaults), and then deeply patch the API settings into it
userSettings.replaceData().setData(apiSettingsPayload);
console.log(userSettings.getData());
// Results in:
// {
// volume: 100,
// graphics: { resolution: "2560x1440", vsync: true, advanced: { shadows: "high" } }
// }Example 3: Form Inputs and Bulk Patching
When dealing with deep forms in a React or Vue component, it solves state tree clashing gracefully.
const formState = new DataManager({
data: {
user: { firstName: "", lastName: "" },
notifications: false
}
});
// Single input change handles paths implicitly
formState.setData("user.firstName", "Ainsley");
// Or dispatch an entire bulk patch seamlessly
formState.setData({
user: { lastName: "Clarke" },
notifications: true
});
// The result contains the fully bound tree merged together natively.