From 1211a3a092f35aa2e7f8c825b080e39dd85ac7bd Mon Sep 17 00:00:00 2001 From: Ada Lundhe Date: Tue, 27 Feb 2024 14:25:10 -0600 Subject: [PATCH] AL: add sync base as well as subscribe functions --- README.md | 297 ++++++++++++++++++++++++++++++++++++++++++++++ examples/async.ts | 4 +- package.json | 2 +- src/atom.ts | 80 ++++++++++++- src/index.ts | 4 +- src/store.ts | 73 +++++++++++- tsconfig.json | 7 ++ 7 files changed, 451 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index dc99d50..ef414df 100644 --- a/README.md +++ b/README.md @@ -579,6 +579,303 @@ To access `get()`, just specify it as an argument in addition to `set()`!
+---- +### Async Usage Without React 🤖 + +If you need to use Delta in asynchronous code like React Server Components or Server Actions or independently of React, you can do so via `createAsync()`, `asyncAtom()`, `createBase()`, and `atomBase()`. + +Let's start with `createAsync()` - as before we'll create an interface to represent our store's state and actions: + + +```ts +// async.ts + +import { createAsync } from "delta-state"; + +interface CounterStore { + count: number; + updateCount: (next: number) => void; +} + +const asyncCounter = async () => { + +} + + +asyncCounter() +``` + +Next we'll instantiate an async store instace via `createAsync()`: + + +```ts +// async.ts + +... + +const asyncCounter = async () => { + + // The call to createAsync() returns a Promise so + // we'll need to await it if we want to get the + // function to use our store. + const asyncStore = await createAsync( + + // We pass an async function to create our store + async (set, get) => ({ + count: 0, + updateCount: (next: number) => + set({ + count: next + get().count, + }) + }) + ); +} + +asyncCounter() +``` + +There are a few important differences! First, when creating async stores, the function you pass must be async. Next, you must await the Promise returned by the call to `createAsync()` +to access the function to use your store. To continue our example: + +```ts +// async.ts + +... + +const asyncCounter = async () => { + + const asyncStore = await createAsync( + async (set, get) => ({ + count: 0, + updateCount: (next: number) => + set({ + count: next + get().count, + }) + }) + ); + + const { + count, + updateCount, + + // By default async stores return two additional + // helper functions, get and set, which + // function exactly like calling get() and set() + // inside an action. + get, + set + } = asyncStore((state) => state); + +} + +asyncCounter() +``` +As with normal stores, we return both our value and action. However, +unlike async stores, our value does not automatically update when an action or `set()` is called. In order +to access the updated store value after calling an action you must call the `get()` method, which (like when calling `get()` inside an action) returns the entire store: + +```ts +// async.ts + +... + +const asyncCounter = async () => { + + const asyncStore = await createAsync( + async (set, get) => ({ + count: 0, + updateCount: (next: number) => + set({ + count: next + get().count, + }) + }) + ); + + const { + // Let's skip our count here since it + // won't be updated + updateCount, + get + } = asyncStore((state) => state); + + updateCount(1) + + // This is how we get our updated count. + const { + count + } = get() + +} + +asyncCounter() +``` + +We can also update the store by calling `set()`, which works exactly as it does when you call it inside an action: + +```ts +// async.ts + +... + +const asyncCounter = async () => { + + const asyncStore = await createAsync( + async (set, get) => ({ + count: 0, + updateCount: (next: number) => + set({ + count: next + get().count, + }) + }) + ); + + const { + set + } = asyncStore((state) => state); + + // Our count will now be 10. + set({ + count: 10 + }) + +} + +asyncCounter() +``` +If you need to access store state changes reactively (i.e. whenever an action or `set()` mutates store state), you can call `subscribe()`, which takes a callback and optional comparator function: + +```ts +// async.ts + +... + +const asyncCounter = async () => { + + const asyncStore = await createAsync( + async (set, get) => ({ + count: 0, + updateCount: (next: number) => + set({ + count: next + get().count, + }) + }) + ); + + const { + updateCount, + set, + subscribe + } = asyncStore((state) => state); + + // This will trigger on every update. + subscribe(({ count }) => { + console.log(count); + }); + + // This will only trigger if we update the count + // to 100 more than its previous value. + subscribe(({ count }) => { + console.log(count); + }, ({ next, prev }) => next > prev + 100); + + // Our count will now be 1, triggering the + // first subscription but not the second. + set({ + count: 1 + }) + + // This will trigger both our subscriptions. + updateCount(100) + +} + +asyncCounter() + +``` + +We can likewise instantiate async atoms via `atomAsync()`: + +```ts +// async.ts + +... + +const asyncCounter = async () => { + + ... + + const counterAtom = await atomAsync(async (set, get) => [ + 0, + (next) => set(next + get()), + ]); + + const [ + value, + add, + get, + set, + subscribe + ] = counterAtom((value) => value); + +} + +asyncAtomCounter() + +``` + +As with async stores, async atoms include `get()`, `set()`, and `subscribe()`, allowing you to manage the atom's state as needed. + +If synchronous usage independent of React is required, `createBase()` and `atomBase()` allow you you to instantiate stores and atoms: + +```ts +// sync.ts +import { createBase } from 'delta-state'; + +... + +const syncCounterStore = () => { + + const syncCounter = createBase((set, get) => ({ + count: 0, + updateCount: (next: number) => + set({ + count: next + get().count, + }) + }) + ); + + const { + count, + updateCount, + get, + set, + subscribe + } = syncCounter((state) => state); + +} + +const syncCounterAtom = () => { + const counterAtom = atomAsync((set, get) => [ + 0, + (next) => set(next + get()), + ]); + + const [ + value, + add, + get, + set, + subscribe + ] = counterAtom((value) => value); +} + +syncCounterStore() +syncCounterAtom() + +``` + +The APIs for sync base stores and atoms follow those of their asynchronous counterparts minus the need for async functions and awaiting the store or atom in order to use it. + ---- ### Credits and Thanks 🙏 diff --git a/examples/async.ts b/examples/async.ts index 703d200..4aa7633 100644 --- a/examples/async.ts +++ b/examples/async.ts @@ -38,11 +38,11 @@ const test = async () => { (next) => set(next + get()), ]); - const [, add, , subscribeAtom] = myAsyncAtom((value) => value); + const [, add, ,, subscribeAtom] = myAsyncAtom((value) => value); subscribeAtom((next) => { console.log(next); - }); + }, ({ next, prev }) => next > prev); add(1); }; diff --git a/package.json b/package.json index 074e64f..7384874 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "delta-state", - "version": "1.6.0", + "version": "1.8.0", "description": "A modern version of the Delta state manager - written for TS and with the use of React Hooks.", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/src/atom.ts b/src/atom.ts index 8769457..92103cd 100644 --- a/src/atom.ts +++ b/src/atom.ts @@ -112,7 +112,7 @@ const createStoreFromState = () => { return useCreatedStore; }; -const createInternalAsyncReference = (atomStore: Atom) => { +const createInternalBaseReference = (atomStore: Atom) => { const useCreatedStore = ( selector: (state: T) => U, comparator?: ({ next, prev }: { next: U; prev: U }) => boolean, @@ -142,6 +142,24 @@ const createInternalAsyncReference = (atomStore: Atom) => { shouldUpdate && atomStore.setState(next); }; + const callbackWithComparator = ( + callbackComparator: ({ + next, + prev + }:{ + next: U, + prev: U + }) => boolean, + subscriptionCallback: (next: T) => void + ) => { + const currentState = atomStore.getState() + + atomStore.subscribers.add((state) => callbackComparator({ + next: state as U, + prev: currentState as any + }) && subscriptionCallback(state as T)); + } + return [ selection, atomStore.update, @@ -149,15 +167,37 @@ const createInternalAsyncReference = (atomStore: Atom) => { atomStore.subscribe(callback as Listener>); return selector(atomStore.getState()); }, - (callback: (next: U) => void) => { - atomStore.subscribers.add(callback as (state: Partial) => void); + (next: T) => { + atomStore.subscribers.forEach((callback) => { + callback(next); + }); + }, + ( + callback: (next: T) => void, + callbackComparator?: ({ + next, + prev + }:{ + next: U, + prev: U + }) => boolean + ) => { + + callbackComparator ? callbackWithComparator( + callbackComparator, + callback + ) : atomStore.subscribers.add( + callback as (state: any) => void + ); + }, ] as [ U, (next: T) => void, () => U, + (next: T) => void, ( - callback: (next: U) => void, + callback: (next: T) => void, comparator?: ({ next, prev }: { next: U; prev: U }) => boolean, ) => void, ]; @@ -188,7 +228,36 @@ const createAsyncAtomFromState = () => { atomStore.setState(init[0]); atomStore.update = init[1]; - return createInternalAsyncReference(atomStore); + return createInternalBaseReference(atomStore); + }; + + return useCreatedStore; +}; + + +const createBaseAtomFromState = () => { + const useCreatedStore = ( + creator: ( + set: (next: U) => void, + get: () => U, + ) => [U, (next: U) => void], + ) => { + const createNextAtom = createAtomApi(); + const atomStore = createNextAtom({} as any); + + const setState = (next: Partial): void => { + atomStore.subscribers.forEach((callback) => { + callback(next); + }); + }; + + const getState = (): U => atomStore.getState(); + + const init = creator(setState, getState); + atomStore.setState(init[0]); + atomStore.update = init[1]; + + return createInternalBaseReference(atomStore); }; return useCreatedStore; @@ -196,3 +265,4 @@ const createAsyncAtomFromState = () => { export const atom = createStoreFromState(); export const atomAsync = createAsyncAtomFromState(); +export const atomBase = createBaseAtomFromState() diff --git a/src/index.ts b/src/index.ts index 433834a..9af6404 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,3 @@ -export { atom, useAtom, atomAsync } from "./atom.ts"; -export { create, createAsync } from "./store.ts"; +export { atom, useAtom, atomAsync, atomBase } from "./atom.ts"; +export { create, createAsync, createBase } from "./store.ts"; export { AtomHook, DerivedAtom } from "./types.ts"; diff --git a/src/store.ts b/src/store.ts index 1c21ace..c8c4265 100644 --- a/src/store.ts +++ b/src/store.ts @@ -95,9 +95,7 @@ const createStoreFromState = () => { return useCreatedStore; }; -export const create = createStoreFromState(); - -const createInternalAsyncReference = (store: Store) => { +const createInternalBaseReference = (store: Store) => { const useCreatedStore = ( selector: (state: T) => U, comparator?: ({ next, prev }: { next: U; prev: U }) => boolean, @@ -134,6 +132,24 @@ const createInternalAsyncReference = (store: Store) => { }); }; + const callbackWithComparator = ( + callbackComparator: ({ + next, + prev + }:{ + next: U, + prev: U + }) => boolean, + subscriptionCallback: (next: U) => void + ) => { + const currentState = store.getState() + + store.subscribers.add((state) => callbackComparator({ + next: state as U, + prev: currentState as any + }) && subscriptionCallback(state as U)); + } + return { ...selection, get() { @@ -148,8 +164,22 @@ const createInternalAsyncReference = (store: Store) => { }); }); }, - subscribe(callback: (next: U) => void) { - store.subscribers.add(callback as (state: Partial) => void); + subscribe( + callback: (next: U) => void, + callbackComparator?: ({ + next, + prev + }:{ + next: U, + prev: U + }) => boolean + ) { + callbackComparator ? callbackWithComparator( + callbackComparator, + callback + ) : store.subscribers.add( + callback as (state: any) => void + ); }, }; }; @@ -177,10 +207,41 @@ const createAsyncStoreFromState = () => { next: init, }); - return createInternalAsyncReference(store); + return createInternalBaseReference(store); + }; + + return useCreatedStore; +}; + + + +const createBaseStoreFromState = () => { + const useCreatedStore = ( + creator: (set: (next: Partial) => void, get: () => U) => U, + ) => { + const createNextStore = createStoreApi(); + const store = createNextStore({} as any); + + const setState = (next: Partial): void => { + store.subscribers.forEach((callback) => { + callback(next); + }); + }; + + const getState = (): U => store.getState(); + + const init = creator(setState, getState); + store.setState({ + next: init, + }); + + return createInternalBaseReference(store); }; return useCreatedStore; }; + +export const create = createStoreFromState(); export const createAsync = createAsyncStoreFromState(); +export const createBase = createBaseStoreFromState() \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 20e453c..a606669 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,4 +1,11 @@ { + "ts-node": { + // these options are overrides used only by ts-node + // same as the --compilerOptions flag and the TS_NODE_COMPILER_OPTIONS environment variable + "compilerOptions": { + "module": "commonjs" + } + }, "compilerOptions": { "target": "esnext", "strict": true,