𧬠Fine-grained Reactivity
JODS uses a signal-based reactive system to provide efficient, fine-grained updates to subscribers. This optimization ensures that subscribers are only notified when the specific data they depend on changes.
π€ How It Worksβ
Unlike many state management libraries that notify all subscribers whenever any part of the state changes, JODS tracks which properties each subscriber actually uses and only triggers updates when those specific properties change.
π‘ Signal-based Architectureβ
The store implementation uses these key concepts:
- Signals: Each property in your store is backed by a signal (a read/write pair of functions)
- Dependency Tracking: JODS automatically tracks which properties are accessed during a subscriber function
- Fine-grained Notifications: Updates only trigger for subscribers that depend on changed properties
π Benefitsβ
This approach provides several advantages:
- β Reduced Re-renders: Components only re-render when data they actually use changes
- β‘ Better Performance: Fewer wasted update cycles, especially in larger applications
- πͺ Automatic Optimization: No manual selector functions needed - dependencies are tracked automatically
π Subscription Behaviorβ
When you subscribe to a store, JODS uses an automatic dependency tracking mechanism:
- JODS runs your subscriber function once immediately to track which properties are accessed
- It remembers which properties were accessed and only notifies the subscriber when those specific properties change
- If your subscriber doesn't access any properties, it becomes a global subscriber that gets notified on any change
π― Dynamic Dependenciesβ
Dependencies are re-tracked each time your subscriber function runs. This means if your subscriber accesses different properties based on the current state, JODS will update the tracked dependencies automatically.
// This subscriber's dependencies change based on the value of showDetails
store.subscribe((state) => {
console.log("Always shows:", state.title);
if (state.showDetails) {
console.log("Only when details shown:", state.description);
}
});
ποΈ Unsubscribe Behaviorβ
The subscribe
method returns an unsubscribe function that you can call to stop receiving updates:
const unsubscribe = store.subscribe((state) => {
console.log("Count:", state.count);
});
// Later, when you want to stop receiving updates
unsubscribe();
When you call the unsubscribe function:
- The subscriber is immediately removed from the notification list
- All signal subscriptions associated with this subscriber are properly cleaned up
- The subscriber will never be called again when properties change
- Memory usage is optimized by removing all references to the subscriber
This clean unsubscribe behavior ensures your application doesn't experience memory leaks or unexpected behavior when components are unmounted or subscriptions are no longer needed.
π‘ Examplesβ
π’ Basic Exampleβ
import { store } from "jods";
const appState = store({
user: { name: "Burt" },
theme: "light",
counter: 0,
});
// This subscriber only depends on counter
appState.subscribe((state) => {
console.log("Counter:", state.counter);
});
// This subscriber only depends on theme
appState.subscribe((state) => {
console.log("Theme:", state.theme);
});
// Updating counter only notifies the first subscriber
appState.counter++;
// Updating theme only notifies the second subscriber
appState.theme = "dark";
βοΈ React Component Exampleβ
import { store } from "jods";
import { useJods } from "jods/react";
// Create a store with multiple sections
const appStore = store({
user: { name: "Burt", role: "admin" },
settings: { theme: "light", notifications: true },
todos: [{ text: "Buy milk", done: false }],
});
// UserProfile component only re-renders when user data changes
function UserProfile() {
const state = useJods(appStore);
console.log("UserProfile render");
return <div>User: {state.user.name}</div>;
}
// SettingsPanel component only re-renders when settings change
function SettingsPanel() {
const state = useJods(appStore);
console.log("SettingsPanel render");
return (
<div>
<div>Theme: {state.settings.theme}</div>
<button
onClick={() =>
(appStore.settings.theme =
state.settings.theme === "light" ? "dark" : "light")
}
>
Toggle Theme
</button>
</div>
);
}
// TodoList component only re-renders when todos change
function TodoList() {
const state = useJods(appStore);
console.log("TodoList render");
return (
<div>
<h3>Todos:</h3>
<ul>
{state.todos.map((todo, i) => (
<li key={i}>{todo.text}</li>
))}
</ul>
<button
onClick={() =>
appStore.todos.push({
text: "New todo",
done: false,
})
}
>
Add Todo
</button>
</div>
);
}
π οΈ Technical Implementationβ
Under the hood, JODS implements this optimization using a combination of JavaScript's Proxy and a dependency tracking system:
- When a subscriber runs for the first time, JODS tracks which properties are accessed
- These dependencies are stored in a map for that specific subscriber
- When properties are updated, JODS only notifies subscribers that depend on those properties
This approach is similar to the reactivity systems in modern frameworks like Vue 3 and SolidJS, but it's packaged in a framework-agnostic way that feels like working with plain JavaScript objects.
βοΈ Comparison with Other Approachesβ
Approach | Update Strategy | Performance |
---|---|---|
Redux | Notify all subscribers, components must use selectors | Requires manual optimization |
Zustand | Store-wide updates, selector functions needed | Requires manual optimization |
Signals (SolidJS, Preact) | Property-level granularity | Automatic fine-grained updates |
JODS | Property-level granularity with automatic tracking | Automatic fine-grained updates |
π Best Practicesβ
To get the most out of JODS's fine-grained reactivity:
- Keep subscriber functions focused: Access only the properties you need
- Structure your state logically: Group related data together
- Use computed values: These automatically track their dependencies too
π³ Deep Nesting and Arraysβ
JODS's fine-grained tracking works with deeply nested properties and arrays. When you access a nested property or array element, JODS tracks that specific path as a dependency.
const store = createStore({
nested: {
deeply: {
value: 42,
},
},
items: [1, 2, 3],
});
// This only tracks nested.deeply.value as a dependency
store.subscribe((state) => console.log(state.nested.deeply.value));
// This only tracks items as a dependency
store.subscribe((state) => console.log(state.items.length));