97 lines
2.4 KiB
TypeScript
97 lines
2.4 KiB
TypeScript
import {useSyncExternalStore} from "react";
|
||
|
||
type Unsub = () => void;
|
||
|
||
|
||
/**
|
||
* A simple reactive state container that holds a value of type `T` that provides methods to get, set, and subscribe.
|
||
*/
|
||
export type Cell<T> = {
|
||
/**
|
||
* Returns the current value stored in the cell.
|
||
*/
|
||
get: () => T;
|
||
/**
|
||
* Updates the cell's value, pass either a direct value or an updater function.
|
||
*
|
||
* @example
|
||
* ```ts
|
||
* count.set(5);
|
||
* count.set(prev => prev + 1);
|
||
* ```
|
||
*/
|
||
set: (next: T | ((prev: T) => T)) => void;
|
||
|
||
/**
|
||
* Subscribe to changes in the cell's value, meaning the provided callback is called whenever the value changes.
|
||
* Returns an unsubscribe function.
|
||
*
|
||
* @example
|
||
* ```ts
|
||
* const unsubscribe = count.subscribe(() => console.log(count.get()));
|
||
* // later:
|
||
* unsubscribe();
|
||
* ```
|
||
*/
|
||
subscribe: (callback: () => void) => Unsub;
|
||
};
|
||
|
||
/**
|
||
* Creates a new reactive state container (`Cell`) with an initial value.
|
||
*
|
||
* This function allows you to store and mutate state outside of React,
|
||
* while still supporting subscriptions for reactivity.
|
||
*
|
||
* @param initial - The initial value for the cell.
|
||
* @returns A Cell object with `get`, `set`, and `subscribe` methods.
|
||
*
|
||
* @example
|
||
* ```ts
|
||
* const count = cell(0);
|
||
* count.set(10);
|
||
* console.log(count.get()); // 10
|
||
* ```
|
||
*/
|
||
export function cell<T>(initial: T): Cell<T> {
|
||
let value = initial;
|
||
const listeners = new Set<() => void>();
|
||
return {
|
||
get: () => value,
|
||
set: (next) => {
|
||
value = typeof next === "function" ? (next as (v: T) => T)(value) : next;
|
||
for (const l of listeners) l();
|
||
},
|
||
subscribe: (callback) => {
|
||
listeners.add(callback);
|
||
return () => listeners.delete(callback);
|
||
},
|
||
};
|
||
}
|
||
|
||
/**
|
||
* React hook that subscribes a component to a Cell.
|
||
*
|
||
* Automatically re-renders the component whenever the Cell's value changes.
|
||
* Uses React’s built-in `useSyncExternalStore` for correct subscription behavior.
|
||
*
|
||
* @param c - The cell to subscribe to.
|
||
* @returns The current value of the cell.
|
||
*
|
||
* @example
|
||
* ```tsx
|
||
* const count = cell(0);
|
||
*
|
||
* function Counter() {
|
||
* const value = useCell(count);
|
||
* return (
|
||
* <button onClick={() => count.set(v => v + 1)}>
|
||
* Count: {value}
|
||
* </button>
|
||
* );
|
||
* }
|
||
* ```
|
||
*/
|
||
export function useCell<T>(c: Cell<T>) {
|
||
return useSyncExternalStore(c.subscribe, c.get, c.get);
|
||
}
|