
Security News
Critical Security Vulnerability in React Server Components
React disclosed a CVSS 10.0 RCE in React Server Components and is advising users to upgrade affected packages and frameworks to patched versions now.
@tcn/state
Advanced tools
A lightweight, type-safe state management library for TypeScript applications.
npm install @tcn/state
# or
yarn add @tcn/state
# or
pnpm add @tcn/state
// CounterPresenter.ts
class CounterPresenter {
private _countSignal: Signal<number>;
get countBroadcast() {
return this._countSignal.broadcast;
}
constructor() {
this._countSignal = new Signal<number>(0);
}
increment() {
this._countSignal.transform(count => count + 1);
}
decrement() {
this._countSignal.transform(count => count - 1);
}
dispose() {
this._countSignal.dispose();
}
}
// Counter.tsx
function Counter({ presenter }: { presenter: CounterPresenter }) {
const count = useSignalValue(presenter.countBroadcast);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => presenter.increment()}>Increment</button>
<button onClick={() => presenter.decrement()}>Decrement</button>
</div>
);
}
The library provides two main classes for state management:
Signal: Base class for reactive state management
transformRunner: Extends Signal for handling async operations
Signals are designed to be encapsulated within classes, providing controlled access to state through readonly interfaces.
class TodoListPresenter {
private _todosSignal: Signal<Todo[]>;
private _completedTodosSignal: Signal<number>;
get todosBroadcast() {
return this._todosSignal.broadcast;
}
get completedCountBroadcast() {
return this._completedTodosSignal.broadcast;
}
constructor() {
this._todosSignal = new Signal<Todo[]>([]);
this._completedTodosSignal = new Signal<number>(0);
this._todosSignal.subscribe(todos => {
this._completedTodosSignal.set(
todos.filter(todo => todo.completed).length
);
});
}
dispose() {
this._todosSignal.dispose();
this._completedTodosSignal.dispose();
}
}
Runners provide a powerful way to manage asynchronous operations with built-in state management.
class DataServicePresenter {
private _dataRunner: Runner<Data>;
get dataBroadcast() {
return this._dataRunner.broadcast;
}
constructor() {
this._dataRunner = new Runner<Data>(null);
}
async fetchData() {
await this._dataRunner.execute(async () => {
const response = await fetch('/api/data');
return await response.json();
});
}
dispose() {
this._dataRunner.dispose();
}
}
Root Presenter Pattern (Recommended)
class AppPresenter {
readonly userPresenter: UserPresenter;
constructor() {
this.userPresenter = new UserPresenter();
}
dispose() {
this.userPresenter.dispose();
}
}
Local State Pattern (For isolated components)
function MyComponent() {
const [presenter] = useState(() => new MyPresenter());
useEffect(() => {
return () => presenter.dispose();
}, [presenter]);
return <div>...</div>;
}
useSignalValue<T>(broadcast: IBroadcast<T>): TuseRunnerStatus<T>(broadcast: IRunnerBroadcast<T>): StatususeRunnerProgress<T>(broadcast: IRunnerBroadcast<T>): numberuseRunnerError<T>(broadcast: IRunnerBroadcast<T>): Error | nullset(value: T): voidtransform(cb: (val: T) => T): voidsubscribe(callback: (value: T) => void): ISubscriptiondispose(): voidexecute(action: () => Promise<T>): Promisedispatch(action: () => Promise<T>): Promiseretry(): Promisereset(): voidsetProgress(progress: number): voidsetFeedback(feedback: string): voidsetError(error: Error | null): voiddispose(): voidMemory Management
dispose() on signals and runners when they're no longer needed, but not necessary because Signals subscriptions are WeakRefsPerformance
transform for memory-efficient updatesType Safety
import { Signal, Runner } from '@tcn/state';
class StockPricePresenter {
private _priceSignal: Signal<number>;
private _updateRunner: Runner<void>;
private _ws: WebSocket | null;
private _symbol: string;
get priceBroadcast() {
return this._priceSignal.broadcast;
}
get updateRunnerBroadcast() {
return this._updateRunner.broadcast;
}
constructor(symbol: string) {
this._symbol = symbol;
this._priceSignal = new Signal<number>(0);
this._updateRunner = new Runner<void>();
this._ws = null;
}
async initialize() {
try {
this._ws = new WebSocket(`wss://api.example.com/stock/${this._symbol}`);
// Handle WebSocket connection
this._ws.onopen = () => {
console.log('WebSocket connected');
};
// Handle WebSocket messages
this._ws.onmessage = (event) => {
const price = JSON.parse(event.data).price;
this._priceSignal.set(price);
};
// Handle WebSocket errors
this._ws.onerror = (error) => {
console.error('WebSocket error:', error);
this._updateRunner.setError(new Error('WebSocket connection failed'));
};
// Handle WebSocket closure
this._ws.onclose = () => {
console.log('WebSocket disconnected');
};
return true;
} catch (error) {
console.error('Failed to initialize WebSocket:', error);
this._updateRunner.setError(new Error('Failed to initialize WebSocket connection'));
return false;
}
}
async refresh() {
await this._updateRunner.dispatch(async () => {
const response = await fetch(`/api/stock/${this._symbol}`);
const data = await response.json();
this._priceSignal.set(data.price);
});
}
dispose() {
this._ws?.close();
this._priceSignal.dispose();
this._updateRunner.dispose();
}
}
// Usage in React component
function StockPriceView({ presenter }: { presenter: StockPricePresenter }) {
const price = useSignalValue(presenter.priceBroadcast);
const status = useRunnerStatus(presenter.updateRunnerBroadcast);
const error = useRunnerError(presenter.updateRunnerBroadcast);
useEffect(() => {
// Initialize WebSocket connection when component mounts
presenter.initialize();
// Cleanup when component unmounts
return () => {
presenter.dispose();
};
}, []);
if (status === 'ERROR') {
return (
<div>
<p>Error: {error?.message}</p>
<button onClick={() => presenter.initialize()}>Retry Connection</button>
</div>
);
}
return (
<div>
<h2>Stock Price: ${price}</h2>
<button onClick={() => presenter.refresh()}>Refresh Price</button>
</div>
);
}
// AppPresenter.ts
class AppPresenter {
// Pattern 1: Readonly property for permanent presenters
// - Used when the child presenter is always needed
// - The child presenter is created once and lives as long as the parent
// - Access is direct and type-safe
readonly toolbarPresenter: ToolbarPresenter;
// Pattern 2: Signal for dynamic presenters
// - Used when the child presenter may come and go
// - The child presenter can be created and disposed on demand
// - Access requires checking for null
private _sidebarSignal: Signal<SidebarPresenter | null>;
get sidebarBroadcast() {
return this._sidebarSignal.broadcast;
}
constructor() {
// Pattern 1: Initialize permanent presenters in constructor
this.toolbarPresenter = new ToolbarPresenter();
// Pattern 2: Initialize signal with null for dynamic presenters
this._sidebarSignal = new Signal<SidebarPresenter | null>(null);
}
toggleSidebar() {
if (this._sidebarSignal.get() === null) {
// Pattern 2: Create new presenter when needed
this._sidebarSignal.set(new SidebarPresenter());
} else {
// Pattern 2: Clean up and remove presenter when no longer needed
this._sidebarSignal.get()?.dispose();
this._sidebarSignal.set(null);
}
}
dispose() {
// Pattern 1: Clean up permanent presenters
this.toolbarPresenter.dispose();
// Pattern 2: Clean up dynamic presenters if they exist
this._sidebarSignal.get()?.dispose();
this._sidebarSignal.dispose();
}
}
// App.tsx
function App() {
const [appPresenter] = useState(() => new AppPresenter());
const sidebarPresenter = useSignalValue(appPresenter.sidebarBroadcast);
useEffect(() => {
return () => appPresenter.dispose();
}, [appPresenter]);
return (
<div className="app">
{/* Pattern 1: Direct access to permanent presenter */}
<Toolbar presenter={appPresenter.toolbarPresenter} />
<div className="content">
<button onClick={() => appPresenter.toggleSidebar()}>
{sidebarPresenter ? 'Hide Sidebar' : 'Show Sidebar'}
</button>
{/* Pattern 2: Conditional rendering based on presenter existence */}
{sidebarPresenter && (
<Sidebar presenter={sidebarPresenter} />
)}
</div>
</div>
);
}
The library supports two main patterns for composing presenters:
class ParentPresenter {
// Child presenter is always available
readonly childPresenter: ChildPresenter;
constructor() {
this.childPresenter = new ChildPresenter();
}
}
Use this pattern when:
class ParentPresenter {
private _childSignal: Signal<ChildPresenter | null>;
get childBroadcast() {
return this._childSignal.broadcast;
}
constructor() {
this._childSignal = new Signal<ChildPresenter | null>(null);
}
toggleChild() {
if (this._childSignal.get() === null) {
this._childSignal.set(new ChildPresenter());
} else {
this._childSignal.get()?.dispose();
this._childSignal.set(null);
}
}
}
Use this pattern when:
Use Permanent Presenters when:
Use Dynamic Presenters when:
Memory Management:
Type Safety:
Component Integration:
useSignalValue to subscribe to dynamic presenter signalsFAQs
State management for complex business logic.
The npm package @tcn/state receives a total of 3,560 weekly downloads. As such, @tcn/state popularity was classified as popular.
We found that @tcn/state demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 6 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
React disclosed a CVSS 10.0 RCE in React Server Components and is advising users to upgrade affected packages and frameworks to patched versions now.

Research
/Security News
We spotted a wave of auto-generated “elf-*” npm packages published every two minutes from new accounts, with simple malware variants and early takedowns underway.

Security News
TypeScript 6.0 will be the last JavaScript-based major release, as the project shifts to the TypeScript 7 native toolchain with major build speedups.