repka
v0.1.36
Published
A simple and powerful state manager for React with selective re-renders
Maintainers
Readme
Repka
This is a simple yet powerful state manager for React. It leverages plain JavaScript classes (or objects) as stores, providing high performance with selective re-renders out of the box.
Forget about selectors and complex boilerplate. With Repka, you interact with your state as if it were a plain object, and the library ensures your components update efficiently.
✨ Key Features
- Class or Object Stores: Define your state and logic in a class or a plain object.
- Direct Mutations: Modify state with simple assignments (
store.myValue = 'new value'). No reducers or setters are needed. - High Performance: Components re-render only when the specific data they use changes.
- Boilerplate-Free TypeScript: Strong typing out of the box with automatic type inference. No duplicate interfaces needed.
- Composable: Nests
repkastores within each other for granular, decoupled state. - Automatic Cleanup: Subscriptions are automatically cleaned up when components unmount, preventing memory leaks.
💾 Installation
# npm
npm install repka🚀 Getting Started
1. Define Your Store
Create a plain JavaScript class or object. Repka uses type inference, so you don't need to create separate interfaces.
// store.ts
import { repka } from "repka";
// --- Option 1: Using a Class (Recommended for complex logic) ---
class SimpleStore {
str = "initial string";
num = 123;
reset() {
this.str = "initial string";
this.num = 123;
}
}
// 2. Create and export your reactive store
// No <ISimpleStore> needed! The type is inferred automatically.
export const simpleStore = repka(new SimpleStore());
// --- Option 2: Using a Plain Object ---
export const anotherStore = repka({
count: 0,
increment() {
this.count++;
},
});
// The type of simpleStore is now:
// SimpleStore & IRepkaCallable<SimpleStore>2. Use in Your React Component
You can now import simpleStore and use it. There are two primary ways to "listen" to changes.
// MyComponent.jsx
import React from 'react';
import { simpleStore } from './store';
// 1. Read data from the store
<p>String: {simpleStore.str}</p>
// 2. Change data via direct assignment
<button onClick={() => simpleStore.str = 'new string!'}>
Change String
</button>
// 3. Call a method from the store
<button onClick={() => simpleStore.reset()}>
Reset
</button>📖 Usage in React: The Two Methods
Repka provides two ways to subscribe to state.
Method 1: The HOC Wrapper (Recommended Way)
This is the safest and recommended way to use Repka.
Wrap your component with the store instance itself. The store acts as a Higher-Order Component (HOC). This method reliably tracks all properties your component accesses during its render, even if they are inside conditional logic (if, &&, ?:).
// MyComponent.jsx
import { simpleStore } from "./store";
// Wrap the component with the store
export const MyComponent = simpleStore(({ shouldShow }) => {
// ✅ SAFE: We can now use conditional logic
let value = "default";
if (shouldShow) {
value = simpleStore.str;
}
return (
<div>
<p>Value: {value}</p>
<button onClick={() => (simpleStore.str = "new string!")}>
Change String
</button>
</div>
);
});Method 2: Direct Property Access (Advanced Shortcut)
You can also access store properties (store.prop) directly in your component's render body. Under the hood, this acts like a React Hook (useSyncExternalStore).
const StringDisplay = () => {
// This access subscribes the component to 'str'
return <p>String: {simpleStore.str}</p>;
};⛔️ CRITICAL WARNING: Respect the Rules of Hooks
Because direct access (store.prop) is a hook call, you MUST follow the React Rules of Hooks.
NEVER access store properties conditionally, in loops, or in event handlers (if you expect reactivity).
❌ This will BREAK your app:
const MyComponent = ({ shouldShow }) => {
let value = "default";
if (shouldShow) {
// 🚨 WRONG! Calling a hook (store.prop) conditionally.
// This will cause an "Invalid hook call" error.
value = simpleStore.str;
}
return <div>{value}</div>;
};Our Recommendation:
- HOC (
repka(Component)): Use this 99% of the time. It's safe, works with any logic, and is compatible withReact.memo.- Direct Access (
store.prop): Use this only for simple components where properties are always accessed unconditionally at the top level.
⚡ Performance: Selective Re-renders
Repka's performance magic is that it tracks which property is used by which component.
If you change store.num, only components that use store.num will re-render. Components that only use store.str will be skipped.
import { simpleStore } from "./store";
// This component ONLY depends on `simpleStore.str`
const StringDisplay = simpleStore(() => {
// Using the HOC
console.log("Render StringDisplay");
return <p>String: {simpleStore.str}</p>;
});
// This component ONLY depends on `simpleStore.num`
const NumberDisplay = simpleStore(() => {
// Using the HOC
console.log("Render NumberDisplay");
return <p>Number: {simpleStore.num}</p>;
});
const App = () => (
<>
<StringDisplay />
<NumberDisplay />
<button onClick={() => (simpleStore.str = "new value")}>
Change Only String
</button>
</>
);When you click the button, you will only see "Render StringDisplay" in the console. NumberDisplay will not re-render because its data did not change.
🧩 Composing Stores
You can safely nest repka stores inside each other. The reactivity will be tracked across stores automatically.
This is different from the "shallow reactivity" limitation. While plain objects aren't tracked deeply, other repka instances are.
// stores.ts
import { repka } from "repka";
export const childStore = repka({
text: "Hello",
});
export const parentStore = repka({
child: childStore,
// You can even swap them out
setNewChild() {
this.child = repka({ text: "World" });
},
});// MyComponent.jsx
import { parentStore } from "./stores";
// This component tracks `parentStore.child` AND `parentStore.child.text`
export const MyComponent = parentStore(() => {
return (
<div>
<p>{parentStore.child.text}</p>
{/* This will trigger a re-render */}
<button onClick={() => (parentStore.child.text = "New Text!")}>
Change Child Text
</button>
{/* This will also trigger a re-render */}
<button onClick={() => parentStore.setNewChild()}>Set New Child</button>
</div>
);
});When you update childStore.text directly, MyComponent will correctly re-render because its HOC (Reaction) is subscribed to changes in both parentStore and childStore.
🧐 Reactivity Outside React: watch
You can use the watch function to react to changes outside of a React component (e.g., for logging or async logic).
import { repka, watch } from "repka";
const state = repka({ foo: 0 });
const logChanges = async () => {
console.log('Waiting for "foo" to change...');
const newValue = await watch(state, "foo");
console.log(`"foo" changed! New value: ${newValue}`);
// You can call this in a loop or recursively
// logChanges();
};
// Start the watcher
logChanges();
// Somewhere in your app...
setTimeout(() => {
state.foo = 1;
}, 2000);
/*
Console Output:
Waiting for "foo" to change...
"foo" changed! New value: 1
*/💡 How it Works
Repka uses a Proxy-based observable pattern with a dual-subscription mechanism:
HOC Wrapper (
repka(Component)): This is the recommended, MobX-like approach. It wraps your component in aReaction(a special observer class). During the render (reaction.track(...)), anystore.propaccess is intercepted by theProxy'sgethandler. This handler reports the dependency (e.g.,foo) to theReaction. Whenstore.foo = 'new'is called, the store notifies allReactionobjects subscribed tofoo, which then trigger aforceUpdatein your component.Direct Access (
store.prop): This is an advanced shortcut. When theProxy'sgethandler detects it's not inside aReaction.track()call, it instead invokes a React hook:useSyncExternalStore. This creates a granular subscription only to that specific property. This is why it's subject to the Rules of Hooks.Error Resilience for Hook-Based Access:
When using direct property access (store.prop), Repka proactively handles React hook rule violations through a sophisticated error mitigation system:- Spam Hash Precomputation:
During library initialization, Repka intentionally triggers common React hook errors (like "Invalid hook call") in isolated test components. It captures the error message, normalizes it (removing prefixes like "Error:"), and computes a 32-bit hash of the first 200 characters. ThisKNOWN_SPAM_HASHis stored for both server (SSR) and client environments. - Safe Error Handling in Getters:
InsimpleReactProvider(the getter handler), when an error occurs during hook execution:- The error message is normalized and hashed
- The hash is compared against
KNOWN_SPAM_HASH - If matched: The error is treated as non-critical "development spam". Repka safely falls back to returning the current raw property value, preventing crashes while still showing React's warning in console.
- If unmatched: Repka throws a loud
Repka CRITICAL ERRORcontaining:- The rendering component's name
- Known vs. actual error hashes for diagnostics
- Full original error details
- Clear explanation about preventing "zombie components"
- Zombie Component Prevention:
For non-spam errors (e.g., "Rendered more hooks than during previous render"), Repka intentionally crashes with a detailed error rather than allowing corrupted UI states. This fails fast to prevent subtle data corruption issues that are extremely hard to debug. - Context Detection:
The getter first checks if it's executing within React's render cycle using internal React flags (__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED). If not in a render context, it bypasses hooks entirely and returns raw values.
This mechanism ensures:
- 🛡️ Development warnings don't crash production apps
- 🚨 Critical errors fail loudly with actionable diagnostics
- ⚡ Zero performance overhead for successful hook executions
- 🌐 Consistent behavior across SSR and client environments
- Spam Hash Precomputation:
⛔️ Known Limitations
Repka is designed for simplicity, which comes with trade-offs you must be aware of:
Shallow Reactivity (for Plain Objects):
Repkauses a shallowProxy. It only tracks top-level properties of your store. Mutating a plain nested object (e.g.,store.myObject.foo = 'bar') will not trigger a re-render.- Solution: You must re-assign the object:
store.myObject = { ...store.myObject, foo: 'bar' }. - Exception: This limitation does not apply to nesting other
repkastores. See "Composing Stores" above.
- Solution: You must re-assign the object:
Dynamic HOC Dependencies: The HOC wrapper (
repka(Component)) only tracks dependencies that are accessed during the current render. If a property is behind conditional logic (e.g.,if (show)), it will only become a dependency aftershowbecomestrueand the component re-renders.React Compiler: work in progress... ---
📚 API Reference
repka(sourceObject)
The main function to create a reactive store.
sourceObject: T: An instance of your store class (e.g.,new MyStore()) or a plain object.- Returns: A reactive
RepkaStore<T>proxy. This object can be used directly for property access, as a HOC, and for hooks.
watch(store, propertyKey)
An async function to react to state changes outside of a React component.
store: The reactive store instance created byrepka.propertyKey: string: The string name of the property to watch.- Returns: A
Promisethat resolves with the new value as soon as the specified property changes.
