@renewx/core
v0.3.1
Published
Lightweight state management library with validation support
Downloads
29
Maintainers
Readme
@renewx/core
RenewX is a lightweight, high-performance state management library for TypeScript applications. It offers robust features like transactions and validation support, ensuring data integrity in your projects.
Features
- 🚀 High Performance: Optimized for speed, perfect for both simple and complex data structures.
- ❄️ Immutable State: All states are wrapped with a Freeze type, preventing unintended mutations.
- ✅ Built-in Validation: Maintain data integrity with custom validation logic.
- 💯 Transaction Support: Implement MVCC, Snapshot Isolation, and Optimistic Concurrency Control for safe concurrent modifications.
- 🪶 Lightweight: Efficient solution keeping your project slim.
- ♻️ Zero Dependencies: Self-contained library for reliable performance.
- 📚 TypeScript Native: Full type safety and excellent developer experience.
Installation
Install RenewX using npm:
npm install @renewx/core
Or using yarn:
yarn add @renewx/core
Quick Start
Let's create a simple todo list application to demonstrate the basic usage of RenewX, including the setup
and creator
functions.
import {
store,
action,
adapter,
watch,
setup,
creator,
HasReadOnlyStoreWith,
} from "@renewx/core";
// Define the Todo interface
interface Todo {
id: number;
text: string;
completed: boolean;
}
// Define the Todos interface
interface TodoList extends HasReadOnlyStoreWith<Todo[]> {
add: (text: string) => void;
toggle: (id: number) => void;
getIncomplete: () => Todo[];
}
// Use creator to encapsulate todo list logic
const createTodoList = creator((cleaner): TodoList => {
const todoList = store<Todo[]>([], "TODO_LIST");
const add = action(
todoList,
(state, text: string) =>
state.concat({ id: Date.now(), text, completed: false }),
"add",
);
const toggle = action(
todoList,
(state, id: number) =>
state.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo,
),
"toggle",
);
const incompleteTodos = adapter(
todoList,
(state) => state.filter((todo) => !todo.completed),
"INCOMPLETE_TODOS",
);
cleaner.add(incompleteTodos);
return {
store: todoList.readOnly,
add,
toggle,
getIncomplete: () => incompleteTodos.get(),
};
});
// Use setup to create a reusable watch setup
const setupTodoWatch = setup((cleaner, todoList: TodoList) =>
cleaner.add(
watch(todoList.store, (state) => {
console.log(`Total todos: ${state.length}`);
console.log(`Incomplete todos: ${todoList.getIncomplete().length}`);
}),
),
);
// Usage
const todoList = createTodoList();
const todoWatcher = setupTodoWatch(todoList);
todoList.add("Learn RenewX");
todoList.add("Build an awesome app");
todoList.toggle(todoList.store.get()[0].id);
console.log(todoList.getIncomplete());
// At some point, when cleaning up
todoWatcher.off();
todoList.off();
This example demonstrates the core concepts of RenewX: stores, actions, adapters, watchers, as well as the setup
and creator
functions. As we progress through the documentation, we'll explore these concepts in more depth.
Core Concepts
Store
A store is the foundation of state management in RenewX. It holds your application's data and ensures its immutability.
import { store } from "@renewx/core";
const counter = store(0, "COUNTER");
console.log(counter.get()); // 0
counter.set(5);
console.log(counter.get()); // 5
Action
Actions are functions that modify the state in a store. They provide a way to encapsulate state changes and make them reusable.
import { action } from "@renewx/core";
const increment = action(
counter,
(state, amount: number) => state + amount,
"increment",
);
increment(3);
console.log(counter.get()); // 8
Adapter
Adapters allow you to derive computed values from one or more stores. They automatically update when their dependent stores change.
import { adapter } from "@renewx/core";
const isEven = adapter(counter, (count) => count % 2 === 0, "IS_EVEN");
console.log(isEven.get()); // false
increment(2);
console.log(isEven.get()); // true
Watch
The watch
function allows you to observe changes in stores or adapters and react to them.
import { watch } from "@renewx/core";
watch(counter, (count) => {
console.log("Counter value:", count);
});
increment(5); // Console: Counter value: 13
Advanced Concepts
Setup Function
The setup
function allows you to create reusable watch setups. This is particularly useful when you want to create a named initialization for watchers that can be easily reused and cleaned up.
import { Store, store, watch, setup } from "@renewx/core";
interface UserProfile {
name: string;
age: number;
}
interface UserActivity {
lastLogin: Date;
loginCount: number;
}
const userProfile = store<UserProfile>(
{ name: "Alice", age: 30 },
"USER_PROFILE",
);
const userActivity = store<UserActivity>(
{ lastLogin: new Date(), loginCount: 0 },
"USER_ACTIVITY",
);
const setupUserMonitor = setup(
(cleaner, profile: Store<UserProfile>, activity: Store<UserActivity>) =>
cleaner.add(
watch(profile, ({ name, age }) => {
console.log(`Profile updated: ${name}, ${age} years old`);
}),
watch(activity, ({ lastLogin, loginCount }) => {
console.log(
`Last login: ${lastLogin.toLocaleString()}, Total logins: ${loginCount}`,
);
}),
),
);
// Usage
const userMonitor = setupUserMonitor(userProfile, userActivity);
// Update the profile and activity
userProfile.set({ name: "Alice", age: 31 });
userActivity.set({ lastLogin: new Date(), loginCount: 5 });
// Later, when cleaning up
userMonitor.off();
In this example, setupUserMonitor
creates two watchers: one for the user profile and another for user activity. The setup
function allows us to bundle these watchers together, making it easy to initialize and clean up the monitoring in one go.
Creator Function
The creator
function is used to encapsulate the initialization logic for a store and its related functionality. This keeps all the logic related to a particular entity in one place, making your code more organized and easier to maintain.
import {
store,
action,
watch,
creator,
adapter,
HasReadOnlyStoreWith,
} from "@renewx/core";
interface Stock {
symbol: string;
price: number;
quantity: number;
}
interface Portfolio extends HasReadOnlyStoreWith<Stock[]> {
addStock: (symbol: string, price: number, quantity: number) => void;
updatePrice: (symbol: string, newPrice: number) => void;
}
const createPortfolio = creator((cleaner): Portfolio => {
const stocks = store<Stock[]>([], "STOCKS");
const addStock = action(
stocks,
(state, symbol: string, price: number, quantity: number) =>
state.concat({ symbol, price, quantity }),
"add_stock",
);
const updatePrice = action(
stocks,
(state, symbol: string, newPrice: number) =>
state.map((stock) =>
stock.symbol === symbol ? { ...stock, price: newPrice } : stock,
),
"update_price",
);
return {
store: stocks.readOnly,
addStock,
updatePrice,
};
});
const createPortfolioMonitor = creator((cleaner, portfolio: Portfolio) => {
const totalValue = adapter(
portfolio.store,
(stocks) =>
stocks.reduce((total, stock) => total + stock.price * stock.quantity, 0),
"TOTAL_VALUE",
);
cleaner.add(
watch(totalValue, (value) => {
console.log(`Current portfolio value: $${value.toFixed(2)}`);
}),
);
return {
store: totalValue.readOnly,
};
});
// Usage
const portfolio = createPortfolio();
const portfolioMonitor = createPortfolioMonitor(portfolio);
portfolio.addStock("AAPL", 150.25, 10);
portfolio.addStock("GOOGL", 2750.5, 5);
portfolio.updatePrice("AAPL", 155.75);
// Later, when cleaning up
portfolioMonitor.off();
portfolio.off();
In this example, createPortfolio
encapsulates all the logic related to managing a stock portfolio, including the store and actions. The createPortfolioMonitor
function creates a separate entity responsible for monitoring the portfolio's total value. The creator
function ensures that all cleanup logic is handled automatically when off
is called.
These advanced concepts, setup
and creator
, provide powerful tools for structuring your application's state management in a clean and maintainable way. They allow you to create reusable patterns and encapsulate related functionality, leading to more organized and easier to understand code.
Advanced Usage
Nested Watchers
RenewX allows you to create nested watchers, which can be useful for complex reactive scenarios.
import { store, action, watch } from "@renewx/core";
const user = store({ name: "John", isOnline: false }, "USER");
const messages = store([], "MESSAGES");
const setOnlineStatus = action(
user,
({ name }, isOnline: boolean) => ({ name, isOnline }),
"set_online_status",
);
watch(user, (userData) => {
console.log(
`User ${userData.name} status changed to ${userData.isOnline ? "online" : "offline"}`,
);
// Nested watcher
if (userData.isOnline) {
return watch(messages, (messages) => {
console.log(`New message count: ${messages.length}`);
});
}
});
// Usage
setOnlineStatus(true);
messages.set(["Hello!"]);
setOnlineStatus(false);
Working with Event Emitters
RenewX can be seamlessly integrated with traditional event emitters. Here's an example of how to use watch
with DOM events:
import { store, watch } from "@renewx/core";
const windowSize = store(
{ width: window.innerWidth, height: window.innerHeight },
"WINDOW_SIZE",
);
watch(windowSize, () => {
console.log("Window resized");
const handleResize = () => {
windowSize.set({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener("resize", handleResize);
// Return an unsubscribe function
return () => {
window.removeEventListener("resize", handleResize);
};
});
// The watcher will log "Window resized" and update the windowSize store whenever the window is resized
Validation
RenewX provides a way to validate state changes before they're applied to the store.
import { store } from "@renewx/core";
const age = store(0, "AGE");
age.validator((oldValue, newValue) => {
return newValue >= 0 && newValue <= 120;
});
age.set(25); // Valid, state is updated
console.log(age.get()); // 25
age.set(-5); // Invalid, state remains unchanged
console.log(age.get()); // 25
Transactions
For complex state updates, especially those involving asynchronous operations, RenewX provides transaction support.
import { store, tx } from "@renewx/core";
const balance = store(1000, "BALANCE");
const transactionLog = store<string[]>([], "TRANSACTION_LOG");
const transferMoney = tx(
[balance, transactionLog],
async ([balanceTx, logTx], amount: number) => {
const currentBalance = balanceTx.get();
if (currentBalance < amount) {
throw new Error("Insufficient funds");
}
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1000));
balanceTx.set(currentBalance - amount);
logTx.set(logTx.get().concat([`Transferred $${amount}`]));
return { newBalance: balanceTx.get() };
},
);
// Usage
transferMoney(500)
.then((result) =>
console.log("Transfer successful. New balance:", result.newBalance),
)
.catch((error) => console.error("Transfer failed:", error.message));
This transaction ensures that both the balance update and the transaction log update occur atomically. If any part of the transaction fails, all changes are rolled back.
Grouping Actions
For stores with multiple related actions, you can use the actions
function to group them together.
import { store, actions } from "@renewx/core";
const counter = store(0, "COUNTER");
const counterActions = actions(counter, {
increment: (state, amount: number) => state + amount,
decrement: (state, amount: number) => state - amount,
reset: () => 0,
});
// Usage
counterActions.increment(5);
console.log(counter.get()); // 5
counterActions.decrement(2);
console.log(counter.get()); // 3
counterActions.reset();
console.log(counter.get()); // 0
Cleaner Utility
RenewX provides a cleaner
utility for managing disposable resources. This is particularly useful for grouping multiple unsubscribe functions or off
methods, making resource cleanup more manageable.
import { cleaner } from "@renewx/core";
const resourceCleaner = cleaner();
// Adding disposables
const unsubscribe1 = resourceCleaner.add(() => console.log("Resource 1 cleaned"));
const unsubscribe2 = resourceCleaner.add(() => console.log("Resource 2 cleaned"));
// Removing a specific disposable
resourceCleaner.remove(unsubscribe1);
// Cleaning up all remaining resources
resourceCleaner.off();
The cleaner
function accepts any number of disposable items. A disposable can be:
- An object with an
off
method - A function that performs cleanup when called
undefined
ornull
(which are ignored)
You can add disposables at any time using the add
method, remove specific disposables with the remove
method, and clean up all resources at once with the off
method.
Here's an example of using cleaner
with watchers:
import { store, watch, cleaner } from "@renewx/core";
const userStore = store({ name: "John", age: 30 }, "USER");
const taskStore = store([], "TASKS");
const appCleaner = cleaner();
appCleaner.add(
watch(userStore, (user) => console.log("User updated:", user)),
watch(taskStore, (tasks) => console.log("Tasks updated:", tasks))
);
// Later, when you want to clean up all watchers:
appCleaner.off();
Using the cleaner
utility helps prevent memory leaks by ensuring all resources are properly disposed of when they're no longer needed.
Read-Only Store Access
In some cases, you might want to provide read-only access to a store. RenewX offers a readOnly
property for this purpose.
import { store, watch } from "@renewx/core";
const data = store({ value: 0 }, "DATA");
// Read-only access
const readOnlyData = data.readOnly;
watch(readOnlyData, (data) => {
console.log("Data changed:", data.value);
});
// This works
data.set({ value: 5 });
// This would cause a TypeScript error
// readOnlyData.set({ value: 10 });
Performance Optimization with Batch
When creating multiple adapters or watchers, you can use the batch
function to optimize performance.
import { store, adapter, batch } from "@renewx/core";
const userStore = store({ name: "John", age: 30 }, "USER");
batch.stores.start();
const name = adapter(userStore, (state) => state.name, "NAME");
const age = adapter(userStore, (state) => state.age, "AGE");
const fullInfo = adapter(
userStore,
(state) => `${state.name} (${state.age})`,
"FULL_INFO",
);
batch.stores.end();
console.log(name.get()); // "John"
console.log(age.get()); // 30
console.log(fullInfo.get()); // "John (30)"
Using batch
can significantly improve performance when creating multiple adapters or watchers simultaneously.
Best Practices
Use Immutable Updates: Always create new objects or arrays when updating state, rather than mutating existing ones.
Keep Stores Focused: Each store should represent a cohesive piece of state. Don't try to put your entire application state in a single store.
Leverage Adapters: Use adapters to compute derived state instead of storing computed values directly.
Unsubscribe from Watchers: Always unsubscribe from watchers when they're no longer needed to prevent memory leaks.
Use Meaningful Names: Give your stores, actions, and adapters clear, descriptive names that reflect their purpose.
Organize Related Logic: Use the
creator
function to group related stores, actions, and adapters together.Reuse Watch Logic: Utilize the
setup
function to create reusable watch setups for common patterns in your application.Be Careful with
.unsafe()
: The.unsafe()
method bypasses the Freeze type. Use it sparingly and only when absolutely necessary.Validate Early: Use validators to ensure your state always remains in a valid form.
Use Transactions for Complex Updates: When you need to update multiple stores atomically or perform asynchronous operations, use transactions.
Conclusion
RenewX provides a powerful yet flexible approach to state management in TypeScript applications. By leveraging its core concepts of stores, actions, adapters, and watchers, you can build robust and reactive applications with ease. The advanced features like setup
and creator
functions, along with transaction support and performance optimizations, allow you to tackle even the most complex state management scenarios.
Remember, the key to effective state management is to start simple and add complexity only as needed. As you become more familiar with RenewX, you'll discover how its features can help you create clean, maintainable, and efficient state management solutions for your applications.
Happy coding with RenewX!