@deft-plus/reactivity@0.1.0Built and signed on GitHub ActionsBuilt and signed on GitHub Actions
🎯 Seamlessly manage reactive state with signals, enabling automatic updates and fine-grained reactivity across your application.
🎯 Reactivity
The @deft-plus/reactivity module combines the power of reactive signals and stores to manage both individual reactive values and global state in your application. With this module, you can create reactive values (signals), manage derived values, and encapsulate state in stores, all with minimal boilerplate and a functional programming style.
Installation
Install the @deft-plus/reactivity package using deno:
deno add @deft-plus/reactivity
Or using npm:
npx jsr add @deft-plus/reactivity
Signals
Signals provide a way to create reactive values that automatically notify consumers when their value changes. They allow for fine-grained reactivity and lazy evaluation, making them ideal for performance-sensitive applications.
Basics
A signal is a function (() => T) that returns its current value. It doesn't trigger side effects but may recompute values lazily.
Reactive Contexts: When a signal is accessed in a reactive context, it registers as a dependency. The context updates when any signal it depends on changes.
Writable Signals
Use signal() to create a WritableSignal. Writable signals allow updating their value using:
.set(value): Update the signal's value..update(func): Update using a function..mutate(func): Modify the current value directly.
Example:
import { signal } from '@deft-plus/reactivity'; const counter = signal(0); counter.set(2); counter.update((count) => count + 1); counter.mutate((list) => list.push({ title: 'New Task' }));
Equality Comparator: Optionally, provide a function to compare new and old values to prevent unnecessary updates.
Read-Only Signals
Create a read-only signal with signal(value).readonly(). It restricts mutation and only allows access to the value.
Example:
const readonlyCounter = signal(0).readonly();
Memoized Signals
Use memoSignal() to create memoized signals that automatically update based on dependencies.
Example:
import { memoSignal } from '@deft-plus/reactivity'; const isEven = memoSignal(() => counter() % 2 === 0);
Memoized signals can be configured with an equality comparator to prevent unnecessary updates and an subscribe hook to run when the value changes.
Signal Conversion
toSignal() creates signals that represent asynchronous values. The signal resolves to a promise and exposes a status property.
Example:
import { toSignal } from '@deft-plus/reactivity'; // Function parameter. const data = toSignal(async () => { const response = await fetch('https://api.example.com/data'); return response.json(); }); // Promise parameter. const data = toSignal( fetch('https://api.example.com/data') .then((response) => response.json()), );
Event Signals
You can also use events to trigger signal updates. Just dispatch an event with the signal's name and the new value.
Example:
import { signal } from '@deft-plus/reactivity'; using counter = signal(0, { name: 'counter', allowEvents: true }); dispatchEvent(new CustomEvent('counter', { detail: 1 })); console.log(counter()); // 1
This allows you to update signals from event listeners, making it easy to integrate with other parts of your application. It also uses the Dispose pattern to clean up event listeners when the signal is disposed. You can also use the onDispose hook to run cleanup code when the signal is disposed.
Example:
import { signal } from '@deft-plus/reactivity'; { using counter = signal(0, { name: 'counter', allowEvents: true, onDispose: () => { console.log('Disposed'); }, }); dispatchEvent(new CustomEvent('counter', { detail: 1 })); console.log(counter()); // 1 } // Logs: "Disposed"
Effects
effect() registers side effects that run whenever the signals they depend on change. Effects can be cleaned up by returning a cleanup function.
Example:
import { effect, signal } from '@deft-plus/reactivity'; const counter = signal(0); effect(() => { console.log('Counter:', counter()); return () => console.log('Cleanup'); }); // Run effect immediately. effect.initial(() => { console.log('Counter:', counter()); });
Untracked Signals
Use signal.untracked() to access a signal's value without registering it as a dependency in a reactive context.
Stores
Stores allow you to manage multiple reactive values in a cohesive, encapsulated structure. They are ideal for managing global or complex state, ensuring atomic updates and immutability.
Why Use Stores?
- Encapsulation: Keep state and logic organized.
- Atomic Updates: Prevent unintended side effects.
- Derived Values: Easily compute values based on existing state.
- Immutability: Ensure state cannot be directly mutated, reducing bugs.
Creating a Store
A store is an object containing signals and actions. It leverages signals internally to manage and update state reactively.
Example:
import { store } from '@deft-plus/reactivity'; type CounterStore = { count: number; increment: () => void; decrement: () => void; reset: () => void; }; const counterStore = store<CounterStore>(({ get }) => ({ count: 0, increment: () => get().count.update((count) => count + 1), decrement: () => get().count.update((count) => count - 1), reset: () => get().count.set(0), })); console.log(counterStore.count()); // 0 counterStore.increment(); console.log(counterStore.count()); // 1
Working with Derived Values
Stores can include derived values that depend on other signals. These derived values are memoized and update automatically.
Example:
import { store } from '@deft-plus/reactivity'; type CounterStore = { count: number; double: number; increment: () => void; }; const counterStore = store<CounterStore>(({ get }) => ({ count: 0, double: { value: () => get().count() * 2, }, increment: () => get().count.update((count) => count + 1), })); console.log(counterStore.double()); // 0 counterStore.increment(); console.log(counterStore.double()); // 2
Accessing Store State
The get function provides access to the store's internal signals and actions. Use it to retrieve the current state and trigger updates.
Note:
getensures non-nullable access to the store's state, avoiding constant null checks.
Summary
@deft-plus/reactivity unifies signals and stores, providing a flexible, efficient way to manage both individual reactive values and global state. With signals, you get fine-grained reactivity and lazy evaluation. With stores, you encapsulate state and logic, making it easy to maintain complex state while ensuring immutability and performance.
Add Package
deno add jsr:@deft-plus/reactivity
Import symbol
import * as reactivity from "@deft-plus/reactivity";
Import directly with a jsr specifier
import * as reactivity from "jsr:@deft-plus/reactivity";
Add Package
pnpm i jsr:@deft-plus/reactivity
pnpm dlx jsr add @deft-plus/reactivity
Import symbol
import * as reactivity from "@deft-plus/reactivity";
Add Package
yarn add jsr:@deft-plus/reactivity
yarn dlx jsr add @deft-plus/reactivity
Import symbol
import * as reactivity from "@deft-plus/reactivity";
Add Package
vlt install jsr:@deft-plus/reactivity
Import symbol
import * as reactivity from "@deft-plus/reactivity";
Add Package
npx jsr add @deft-plus/reactivity
Import symbol
import * as reactivity from "@deft-plus/reactivity";
Add Package
bunx jsr add @deft-plus/reactivity
Import symbol
import * as reactivity from "@deft-plus/reactivity";