From 65f5f8ff16dc126dc906e7e36aca53c8505c7021 Mon Sep 17 00:00:00 2001 From: Ada Lundhe Date: Tue, 27 Feb 2024 22:06:53 -0600 Subject: [PATCH 1/3] AL: refactor lib --- examples/async.ts | 46 ++++---- src/async/atom.ts | 71 ++++++++++++ src/async/index.ts | 2 + src/async/store.ts | 37 ++++++ src/atom.ts | 272 --------------------------------------------- src/base/atom.ts | 92 +++++++++++++++ src/base/index.ts | 2 + src/base/store.ts | 194 ++++++++++++++++++++++++++++++++ src/base/types.ts | 59 ++++++++++ src/index.ts | 5 +- src/react/atom.ts | 100 +++++++++++++++++ src/react/index.ts | 2 + src/react/store.ts | 102 +++++++++++++++++ src/store.ts | 251 ----------------------------------------- src/types.ts | 33 ------ src/vanilla.ts | 46 -------- 16 files changed, 682 insertions(+), 632 deletions(-) create mode 100644 src/async/atom.ts create mode 100644 src/async/index.ts create mode 100644 src/async/store.ts delete mode 100644 src/atom.ts create mode 100644 src/base/atom.ts create mode 100644 src/base/index.ts create mode 100644 src/base/store.ts create mode 100644 src/base/types.ts create mode 100644 src/react/atom.ts create mode 100644 src/react/index.ts create mode 100644 src/react/store.ts delete mode 100644 src/store.ts delete mode 100644 src/types.ts delete mode 100644 src/vanilla.ts diff --git a/examples/async.ts b/examples/async.ts index 6da80ac..821856a 100644 --- a/examples/async.ts +++ b/examples/async.ts @@ -1,32 +1,24 @@ -import { createAsync } from "../src"; - -interface CounterStore { - count: number; - updateCount: (next: number) => void; - getCount: () => number; -} +import { atom } from "../src/async"; const test = async () => { - const asyncStore = await createAsync(async (set, get) => ({ - count: 0, - updateCount: (next: number) => - set({ - count: next + get().count, - }), - getCount: () => get().count, - })); - - - const { get, set, subscribe } = asyncStore((state) => state); - - subscribe( - ({ count }) => { - console.log(count); - }, - ({ next, prev }) => next.count > prev.count + 10 - ); - - set({ count: get().count + 100 }); + + const atomTwo = await atom(0) + const atomThree = await atom(0) + + + atomThree.subscribe((count) => { + atomTwo.set(count + atomTwo.get()) + console.log(count) + }) + + atomThree.set(1) + atomThree.set(1) + atomThree.set(1) + + console.log(atomThree.get()) + + console.log(atomTwo.get()) + }; diff --git a/src/async/atom.ts b/src/async/atom.ts new file mode 100644 index 0000000..3daa466 --- /dev/null +++ b/src/async/atom.ts @@ -0,0 +1,71 @@ +import { createAtomApi } from "src/atom.ts"; +import { Atom, Read } from "src/base/types.ts"; + + +const createAsyncAtom = async ( + creator: T | Read>, + comparator?: ({ next, prev }: { next: T; prev: T }) => boolean, +) => { + +const createNextAtom = createAtomApi(); +const atomStore = createNextAtom({} as any); + +const getState = (atom: Atom): T => atom.get() + +const asyncCreator = creator as ( + get: (atom: Atom) => T +) => Promise + + const init = typeof creator === 'function' ? await asyncCreator(getState) : creator; + atomStore.setState(init) + + const atom = { + store: atomStore, + get: () => atomStore.getState(), + set: (next: T) => { + const shouldUpdate = !comparator || comparator({ + next, + prev: atomStore.getState() + }) + + if (shouldUpdate){ + atomStore.setState(next) + atomStore.subscribers.forEach((callback) => { + callback(next) + }) + } + + }, + subscribe: ( + callback: (next: T) => void, + callbackComparator?: ({ + next, + prev + }:{ + next: T, + prev: T + }) => boolean + ) => { + + if (callbackComparator){ + const currentState = atomStore.getState() + + atomStore.subscribers.add((state) => callbackComparator({ + next: state as T, + prev: currentState as any + }) && callback(state as T)); + + } else { + atomStore.subscribers.add( + callback as (state: any) => void + ); + } + } + } as Atom + + return atom + +}; + + +export const atom = createAsyncAtom; \ No newline at end of file diff --git a/src/async/index.ts b/src/async/index.ts new file mode 100644 index 0000000..d0cf3f3 --- /dev/null +++ b/src/async/index.ts @@ -0,0 +1,2 @@ +export { atom } from './atom' +export { create } from './store' \ No newline at end of file diff --git a/src/async/store.ts b/src/async/store.ts new file mode 100644 index 0000000..f809f4f --- /dev/null +++ b/src/async/store.ts @@ -0,0 +1,37 @@ +import { + createInternalBaseReference, + createStoreApi +} from 'src/base/store.ts' +import { Listener } from 'src/base/types.ts' + + + + +const createAsyncStoreFromState = () => { + const useCreatedStore = async ( + creator: (set: (next: Partial) => void, get: () => U) => Promise, + ) => { + const createNextStore = createStoreApi(); + const store = createNextStore({} as any); + + const setState = (next: Partial): void => { + store.subscribers.forEach((callback: Listener) => { + callback(next); + }); + }; + + const getState = (): U => store.getState(); + + const init = await creator(setState, getState); + store.setState({ + next: init, + }); + + return createInternalBaseReference(store); + }; + + return useCreatedStore; + }; + + + export const create = createAsyncStoreFromState() \ No newline at end of file diff --git a/src/atom.ts b/src/atom.ts deleted file mode 100644 index 48254ff..0000000 --- a/src/atom.ts +++ /dev/null @@ -1,272 +0,0 @@ -import { useMemo, useRef, useSyncExternalStore } from "react"; -import useSyncExports from "use-sync-external-store/shim/with-selector.js"; -import { Atom, Listener } from "./types.ts"; -import { useSyncExternalAtomWithSelectorAsync } from "./vanilla.ts"; - -const { useSyncExternalStoreWithSelector } = useSyncExports; - -export const createAtom = (atom: T): Atom => - ({ - value: atom, - subscribers: new Set>(), - subscribe(callback: Listener) { - this.subscribers.add(callback); - return () => this.subscribers.delete(callback); - }, - getState() { - return this.value; - }, - getInitState() { - return atom; - }, - setState(next: T) { - this.value = next; - }, - }) as Atom; - -const createAtomApi = () => createAtom; - -export const useAtom = ( - atom: T, - update: (set: (next: T) => T) => (next: T) => T | Promise, - link?: (source: T, local: T) => T, -) => { - const atomStore = useRef(createAtom(atom)).current; - const lastLinkedState = useRef(atom); - - const set = (next: T) => { - atomStore.value = next; - atomStore.subscribers.forEach((callback) => callback({})); - return next; - }; - - const setUpdate = update(set); - - useMemo(() => { - if (lastLinkedState.current !== atom && link) { - lastLinkedState.current = atom; - atomStore.value = link(lastLinkedState.current, atomStore.value); - } - }, [atom, atomStore, link]); - - return [ - useSyncExternalStore( - (callback) => atomStore.subscribe(callback), - () => atomStore.value, - () => atom, - ), - setUpdate, - ] as [T, typeof setUpdate]; -}; - -const createInternalReference = (atomStore: Atom) => { - const init = atomStore.value; - - const useCreatedStore = ( - selector: (value: T) => T, - comparator?: ({ next, prev }: { next: T; prev: T }) => boolean, - ) => { - return [ - useSyncExternalStoreWithSelector( - (callback: Listener) => atomStore.subscribe(callback), - () => atomStore.value, - () => init, - selector, - comparator - ? (a: T, b: T) => - comparator({ - next: a, - prev: b, - }) - : undefined, - ), - atomStore.update, - ] as [T, (next: T) => void]; - }; - - Object.assign(useCreatedStore, atomStore) - - return useCreatedStore; -}; - -const createStoreFromState = () => { - const useCreatedStore = ( - creator: (set: (next: U) => void, get: () => U) => [U, (next: U) => void], - ) => { - const createNextAtom = createAtomApi(); - const store = createNextAtom({} as U); - - const setState = (next: U) => { - store.value = next; - store.subscribers.forEach((callback: Listener) => callback({})); - return next; - }; - - const getState = () => store.value; - - const init = creator(setState, getState); - store.setState(init[0]); - store.update = init[1]; - - return createInternalReference(store); - }; - - return useCreatedStore; -}; - -const createInternalBaseReference = (atomStore: Atom) => { - const useCreatedStore = ( - selector: (state: T) => U, - comparator?: ({ next, prev }: { next: U; prev: U }) => boolean, - ) => { - const selection = useSyncExternalAtomWithSelectorAsync( - atomStore, - selector, - comparator - ? (a: U, b: U) => - comparator({ - next: a, - prev: b, - }) - : undefined, - ); - - const callback = (next: T) => { - const currentState = selector(atomStore.getState()); - const nextState = selector(next); - const shouldUpdate = comparator - ? comparator({ - next: currentState, - prev: nextState, - }) - : true; - - 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, - () => { - atomStore.subscribe(callback as Listener>); - return selector(atomStore.getState()); - }, - (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: T) => void, - comparator?: ({ next, prev }: { next: U; prev: U }) => boolean, - ) => void, - ]; - }; - - Object.assign(useCreatedStore, atomStore) - - return useCreatedStore; -}; - -const createAsyncAtomFromState = () => { - const useCreatedStore = async ( - creator: ( - set: (next: U) => void, - get: () => U, - ) => Promise<[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 = await creator(setState, getState); - atomStore.setState(init[0]); - atomStore.update = init[1]; - - 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; -}; - -export const atom = createStoreFromState(); -export const atomAsync = createAsyncAtomFromState(); -export const atomBase = createBaseAtomFromState() diff --git a/src/base/atom.ts b/src/base/atom.ts new file mode 100644 index 0000000..88a4654 --- /dev/null +++ b/src/base/atom.ts @@ -0,0 +1,92 @@ +import { Atom, Read, AtomStore, Listener } from "./types.ts"; + +export const createAtom = (atom: T): AtomStore => + ({ + value: atom, + subscribers: new Set>(), + subscribe(callback: Listener) { + this.subscribers.add(callback); + return () => this.subscribers.delete(callback); + }, + getState() { + return this.value; + }, + getInitState() { + return atom; + }, + setState(next: T) { + this.value = next; + }, + }) as AtomStore; + +export const createAtomApi = () => createAtom; + + + +const createBaseAtom = ( + creator: T | Read, + comparator?: ({ next, prev }: { next: T; prev: T }) => boolean, +) => { + + const createNextAtom = createAtomApi(); + const atomStore = createNextAtom({} as any); + + const getState = (atom: Atom): T => atom.get() + + const syncCreator = creator as ( + get: (atom: Atom) => T + ) => T + + const init = typeof creator === 'function' ? syncCreator(getState) : creator; + atomStore.setState(init) + + const atom = { + store: atomStore, + get: () => atomStore.getState(), + set: (next: T) => { + const shouldUpdate = !comparator || comparator({ + next, + prev: atomStore.getState() + }) + + if (shouldUpdate){ + atomStore.setState(next) + atomStore.subscribers.forEach((callback) => { + callback(next) + }) + } + + }, + subscribe: ( + callback: (next: T) => void, + callbackComparator?: ({ + next, + prev + }:{ + next: T, + prev: T + }) => boolean + ) => { + + if (callbackComparator){ + const currentState = atomStore.getState() + + atomStore.subscribers.add((state) => callbackComparator({ + next: state as T, + prev: currentState as any + }) && callback(state as T)); + + } else { + atomStore.subscribers.add( + callback as (state: any) => void + ); + } + } + } as Atom + +return atom + +}; + + +export const atom = createBaseAtom; \ No newline at end of file diff --git a/src/base/index.ts b/src/base/index.ts new file mode 100644 index 0000000..d0cf3f3 --- /dev/null +++ b/src/base/index.ts @@ -0,0 +1,2 @@ +export { atom } from './atom' +export { create } from './store' \ No newline at end of file diff --git a/src/base/store.ts b/src/base/store.ts new file mode 100644 index 0000000..fc97501 --- /dev/null +++ b/src/base/store.ts @@ -0,0 +1,194 @@ +import { Listener } from './types.ts' +import { Store } from './types.ts'; + + +export const createStoreApi = () => { + const implementStore = (state: T) => ({ + state, + subscribers: new Set>(), + subscribe(callback: Listener) { + this.subscribers.add(callback); + return () => this.subscribers.delete(callback); + }, + getState() { + return this.state; + }, + getInitState() { + return state; + }, + setState({ + next, + replace = false, + }: { + next: Partial; + replace?: boolean; + }) { + const nextState = + typeof next === "function" + ? (next as (next: T) => T)(next) + : (next as T); + + if (!Object.is(nextState, this.state)) { + this.state = + replace ?? (typeof nextState !== "object" || nextState === null) + ? nextState + : Object.assign({}, this.state, nextState); + } + }, + delete(callback: Listener) { + this.subscribers.delete(callback); + }, + }); + + return implementStore; +}; + +const useBaseExternalStoreWithSelector = ( + store: Store, + selector: (snapshot: Snapshot) => Selection, + comparator?: (a: Selection, b: Selection) => boolean, +) => { + const callback = (next: Snapshot) => { + + const requestedUpdate = { + ...store.getState(), + ...(next ? next : {}), + }; + const currentState = selector(store.getState()); + const nextState = selector(requestedUpdate); + const shouldUpdate = comparator + ? comparator(currentState, nextState) + : true; + + shouldUpdate && + store.setState({ + next: requestedUpdate, + }); + }; + + store.subscribe(callback as any); + return selector(store.getState()); +}; + +export const createInternalBaseReference = (store: Store) => { + const useCreatedStore = ( + selector: (state: T) => U, + comparator?: ({ next, prev }: { next: U; prev: U }) => boolean, + ) => { + const selection = useBaseExternalStoreWithSelector( + store, + selector, + comparator + ? (a: U, b: U) => + comparator({ + next: a, + prev: b, + }) + : undefined, + ); + + const callback = (next: Partial) => { + const requestedUpdate = { + ...store.getState(), + ...(next ? next : {}), + }; + const currentState = selector(store.getState()); + const nextState = selector(requestedUpdate); + const shouldUpdate = comparator + ? comparator({ + next: currentState, + prev: nextState, + }) + : true; + + shouldUpdate && + store.setState({ + next: requestedUpdate, + }); + }; + + 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() { + store.subscribe(callback); + return selector(store.getState()); + }, + set(state: Partial) { + store.subscribers.forEach((callback: Listener) => { + callback({ + ...store.getState(), + ...state, + }); + }); + }, + 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 + ); + }, + }; + }; + + Object.assign(useCreatedStore, 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 = createBaseStoreFromState() \ No newline at end of file diff --git a/src/base/types.ts b/src/base/types.ts new file mode 100644 index 0000000..baed399 --- /dev/null +++ b/src/base/types.ts @@ -0,0 +1,59 @@ + +export type Listener = (next: Partial) => void; + +export type Store = { + state: S; + subscribers: Set>; + subscribe: (callback: Listener) => () => boolean; + getState: () => S; + getInitState: () => S; + setState: ({ next, replace }: { next: S; replace?: boolean }) => void; + delete: (callback: Listener) => void; +}; + +export type AtomStore = { + value: T; + update: (next: T) => void; + subscribers: Set>; + subscribe: (callback: Listener) => () => boolean; + getState: () => T; + getInitState: () => T; + setState: (state: T) => void; + delete: (callback: Listener) => void; + }; + + + export type Atom = { + store: AtomStore; + get: () => T; + set: (next: T) => void; + subscribe: ( + callback: Listener, + comparator?: ({ + next, + prev + }: { + next: T, + prev: T + }) => boolean + ) => void; + } + + + +export type AtomHook = ( + selector: (value: T) => T, + comparator?: (({ next, prev }: { next: T; prev: T }) => boolean) | undefined, + ) => [T, (next: T) => void]; + +export type DerivedAtom = ( + atom: T, + update: (set: (next: T) => T) => (next: T) => T | Promise, + link?: ((source: T, local: T) => T) | undefined, +) => [T, (next: T) => T | Promise]; + + + export type Getter = (atom: Atom) => Value + export type Read = ( + get: Getter + ) => Value \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 9af6404..728800b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,2 @@ -export { atom, useAtom, atomAsync, atomBase } from "./atom.ts"; -export { create, createAsync, createBase } from "./store.ts"; -export { AtomHook, DerivedAtom } from "./types.ts"; +export { atom } from './base' +export { useAtom, create } from './react' diff --git a/src/react/atom.ts b/src/react/atom.ts new file mode 100644 index 0000000..74242f6 --- /dev/null +++ b/src/react/atom.ts @@ -0,0 +1,100 @@ +import { Atom, Read, Listener } from "src/base/types.ts"; +import { createAtomApi } from 'src/base/atom.ts' +import { useRef, useMemo, useSyncExternalStore } from "react"; + + +const getValueFromCreator = ( + creator: T | Read +) => { + + const getState = (atom: Atom): T => atom.get() + const syncCreator = creator as ( + get: (atom: Atom) => T + ) => T + + + return typeof creator === 'function' ? syncCreator(getState) : creator; +} + + +const createAtom = ( + creator: T | Read +) => { + const createNextAtom = createAtomApi(); + const atomStore = createNextAtom({} as any); + + + const init = getValueFromCreator( + creator + ) + atomStore.setState(init) + + return { + store: atomStore, + get: () => atomStore.getState(), + set: (next: T) => { + atomStore.setState(next) + atomStore.subscribers.forEach((callback) => { + callback(next) + }) + + }, + subscribe: ( + callback: (next: T) => void, + callbackComparator?: ({ + next, + prev + }:{ + next: T, + prev: T + }) => boolean + ) => { + + if (callbackComparator){ + const currentState = atomStore.getState() + + atomStore.subscribers.add((state) => callbackComparator({ + next: state as T, + prev: currentState as any + }) && callback(state as T)); + + } else { + atomStore.subscribers.add( + callback as (state: any) => void + ); + } + } + } as Atom + +} + + +export const useAtom = ( + creator: T | Read, + link?: (source: T, local: T) => T, +) => { + + const atomRef = useRef(createAtom(creator)).current; + const lastLinkedState = useRef( + getValueFromCreator(creator) + ); + + const creatorValue = getValueFromCreator(creator) + + useMemo(() => { + if (lastLinkedState.current !== atomRef && link) { + lastLinkedState.current = creatorValue; + atomRef.store.value = link(lastLinkedState.current, atomRef.store.value); + } + }, [creatorValue, atomRef, link]); + + const value = useSyncExternalStore( + (callback: Listener) => atomRef.store.subscribe(callback), + () => atomRef.get(), + () => atomRef.store.value, + ) + atomRef.store.value = value + + return atomRef + +}; diff --git a/src/react/index.ts b/src/react/index.ts new file mode 100644 index 0000000..6d5a05c --- /dev/null +++ b/src/react/index.ts @@ -0,0 +1,2 @@ +export { useAtom } from './atom' +export { create } from './store' \ No newline at end of file diff --git a/src/react/store.ts b/src/react/store.ts new file mode 100644 index 0000000..a3580b8 --- /dev/null +++ b/src/react/store.ts @@ -0,0 +1,102 @@ +import useSyncExports from "use-sync-external-store/shim/with-selector.js"; +import { Listener, Store } from "src/base/types.ts"; + +const { useSyncExternalStoreWithSelector } = useSyncExports; + +export const createStoreApi = () => { + const implementStore = (state: T) => ({ + state, + subscribers: new Set>(), + subscribe(callback: Listener) { + this.subscribers.add(callback); + return () => this.subscribers.delete(callback); + }, + getState() { + return this.state; + }, + getInitState() { + return state; + }, + setState({ + next, + replace = false, + }: { + next: Partial; + replace?: boolean; + }) { + const nextState = + typeof next === "function" + ? (next as (next: T) => T)(next) + : (next as T); + + if (!Object.is(nextState, this.state)) { + this.state = + replace ?? (typeof nextState !== "object" || nextState === null) + ? nextState + : Object.assign({}, this.state, nextState); + } + }, + delete(callback: Listener) { + this.subscribers.delete(callback); + }, + }); + + return implementStore; +}; + +const createInternalReference = (store: Store, init: T) => { + const useCreatedStore = ( + selector: (state: T) => U, + comparator?: ({ next, prev }: { next: U; prev: U }) => boolean, + ) => { + return useSyncExternalStoreWithSelector( + (callback: Listener) => store.subscribe(callback), + () => store.getState(), + () => init, + selector, + comparator + ? (a: U, b: U) => + comparator({ + next: a, + prev: b, + }) + : undefined, + ); + }; + + Object.assign(useCreatedStore, store) + + return useCreatedStore; +}; + +const createStoreFromState = () => { + const useCreatedStore = ( + creator: (set: (next: Partial) => void, get: () => U) => U, + ) => { + const createNextStore = createStoreApi(); + const store = createNextStore({} as any); + + const setState = (next: Partial): void => { + store.setState({ + next, + }); + store.subscribers.forEach((callback) => callback({})); + }; + + const getState = (): U => store.getState(); + + const init = creator(setState, getState); + store.setState({ + next: init, + }); + + return createInternalReference(store, init); + }; + + return useCreatedStore; +}; + + + + +export const create = createStoreFromState(); \ No newline at end of file diff --git a/src/store.ts b/src/store.ts deleted file mode 100644 index 8c496ca..0000000 --- a/src/store.ts +++ /dev/null @@ -1,251 +0,0 @@ -import useSyncExports from "use-sync-external-store/shim/with-selector.js"; -import { Listener, Store } from "./types.ts"; -import { useSyncExternalStoreWithSelectorAsync } from "./vanilla.ts"; - -const { useSyncExternalStoreWithSelector } = useSyncExports; - -export const createStoreApi = () => { - const implementStore = (state: T) => ({ - state, - subscribers: new Set>(), - subscribe(callback: Listener) { - this.subscribers.add(callback); - return () => this.subscribers.delete(callback); - }, - getState() { - return this.state; - }, - getInitState() { - return state; - }, - setState({ - next, - replace = false, - }: { - next: Partial; - replace?: boolean; - }) { - const nextState = - typeof next === "function" - ? (next as (next: T) => T)(next) - : (next as T); - - if (!Object.is(nextState, this.state)) { - this.state = - replace ?? (typeof nextState !== "object" || nextState === null) - ? nextState - : Object.assign({}, this.state, nextState); - } - }, - delete(callback: Listener) { - this.subscribers.delete(callback); - }, - }); - - return implementStore; -}; - -const createInternalReference = (store: Store, init: T) => { - const useCreatedStore = ( - selector: (state: T) => U, - comparator?: ({ next, prev }: { next: U; prev: U }) => boolean, - ) => { - return useSyncExternalStoreWithSelector( - (callback: Listener) => store.subscribe(callback), - () => store.getState(), - () => init, - selector, - comparator - ? (a: U, b: U) => - comparator({ - next: a, - prev: b, - }) - : undefined, - ); - }; - - Object.assign(useCreatedStore, store) - - return useCreatedStore; -}; - -const createStoreFromState = () => { - const useCreatedStore = ( - creator: (set: (next: Partial) => void, get: () => U) => U, - ) => { - const createNextStore = createStoreApi(); - const store = createNextStore({} as any); - - const setState = (next: Partial): void => { - store.setState({ - next, - }); - store.subscribers.forEach((callback) => callback({})); - }; - - const getState = (): U => store.getState(); - - const init = creator(setState, getState); - store.setState({ - next: init, - }); - - return createInternalReference(store, init); - }; - - return useCreatedStore; -}; - -const createInternalBaseReference = (store: Store) => { - const useCreatedStore = ( - selector: (state: T) => U, - comparator?: ({ next, prev }: { next: U; prev: U }) => boolean, - ) => { - const selection = useSyncExternalStoreWithSelectorAsync( - store, - selector, - comparator - ? (a: U, b: U) => - comparator({ - next: a, - prev: b, - }) - : undefined, - ); - - const callback = (next: Partial) => { - const requestedUpdate = { - ...store.getState(), - ...(next ? next : {}), - }; - const currentState = selector(store.getState()); - const nextState = selector(requestedUpdate); - const shouldUpdate = comparator - ? comparator({ - next: currentState, - prev: nextState, - }) - : true; - - shouldUpdate && - store.setState({ - next: requestedUpdate, - }); - }; - - 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() { - store.subscribe(callback); - return selector(store.getState()); - }, - set(state: Partial) { - store.subscribers.forEach((callback) => { - callback({ - ...store.getState(), - ...state, - }); - }); - }, - 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 - ); - }, - }; - }; - - Object.assign(useCreatedStore, store) - - return useCreatedStore; -}; - -const createAsyncStoreFromState = () => { - const useCreatedStore = async ( - creator: (set: (next: Partial) => void, get: () => U) => Promise, - ) => { - 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 = await creator(setState, getState); - store.setState({ - next: init, - }); - - 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/src/types.ts b/src/types.ts deleted file mode 100644 index 00ab441..0000000 --- a/src/types.ts +++ /dev/null @@ -1,33 +0,0 @@ -export type Listener = (next: Partial) => void; - -export type Atom = { - value: T; - update: (next: T) => void; - subscribers: Set>; - subscribe: (callback: Listener) => () => boolean; - getState: () => T; - getInitState: () => T; - setState: (state: T) => void; - delete: (callback: Listener) => void; -}; - -export type Store = { - state: S; - subscribers: Set>; - subscribe: (callback: Listener) => () => boolean; - getState: () => S; - getInitState: () => S; - setState: ({ next, replace }: { next: S; replace?: boolean }) => void; - delete: (callback: Listener) => void; -}; - -export type AtomHook = ( - selector: (value: T) => T, - comparator?: (({ next, prev }: { next: T; prev: T }) => boolean) | undefined, -) => [T, (next: T) => void]; - -export type DerivedAtom = ( - atom: T, - update: (set: (next: T) => T) => (next: T) => T | Promise, - link?: ((source: T, local: T) => T) | undefined, -) => [T, (next: T) => T | Promise]; diff --git a/src/vanilla.ts b/src/vanilla.ts deleted file mode 100644 index 42ba872..0000000 --- a/src/vanilla.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Atom, Store } from "./types.ts"; - -export const useSyncExternalStoreWithSelectorAsync = ( - store: Store, - selector: (snapshot: Snapshot) => Selection, - comparator?: (a: Selection, b: Selection) => boolean, -) => { - const callback = (next: Snapshot) => { - const requestedUpdate = { - ...store.getState(), - ...(next ? next : {}), - }; - const currentState = selector(store.getState()); - const nextState = selector(requestedUpdate); - const shouldUpdate = comparator - ? comparator(currentState, nextState) - : true; - - shouldUpdate && - store.setState({ - next: requestedUpdate, - }); - }; - - store.subscribe(callback as any); - return selector(store.getState()); -}; - -export const useSyncExternalAtomWithSelectorAsync = ( - store: Atom, - selector: (snapshot: Snapshot) => Selection, - comparator?: (a: Selection, b: Selection) => boolean, -) => { - const callback = (next: Snapshot) => { - const currentState = selector(store.getState()); - const nextState = selector(next); - const shouldUpdate = comparator - ? comparator(currentState, nextState) - : true; - - shouldUpdate && store.setState(next); - }; - - store.subscribe(callback as any); - return selector(store.getState()); -}; From ed6008d171e605123ad6bab0d7ab1aa2d4e4f843 Mon Sep 17 00:00:00 2001 From: Ada Lundhe Date: Wed, 28 Feb 2024 11:26:39 -0600 Subject: [PATCH 2/3] AL: fixes to linking and add transforms for all get set on atoms --- .eslintrc.json | 3 ++ README.md | 18 +++---- examples/async.ts | 24 ++++----- package.json | 5 +- src/async/atom.ts | 110 +++++++++++++++++++-------------------- src/async/index.ts | 4 +- src/async/store.ts | 57 +++++++++----------- src/base/atom.ts | 83 ++++++++++++++--------------- src/base/index.ts | 4 +- src/base/store.ts | 126 ++++++++++++++++++++------------------------- src/base/types.ts | 67 +++++++++--------------- src/index.ts | 5 +- src/react/atom.ts | 105 +++++++++++++------------------------ src/react/index.ts | 4 +- src/react/store.ts | 9 ++-- tsconfig.json | 5 +- 16 files changed, 272 insertions(+), 357 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index bee3c28..c40e130 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -96,6 +96,9 @@ ["^delta-state$", "./src/index.ts"], ["delta-state", "./src"] ] + }, + "typescript": { + "project": "./" } } }, diff --git a/README.md b/README.md index 09fe0ba..e776883 100644 --- a/README.md +++ b/README.md @@ -461,10 +461,10 @@ A `link()` is a function accepting two arguments - the first the source state an Stores aren't solely for holding application state - they can also be used to generate atoms on-the-fly! ```tsx -import { create, useAtom, DerivedAtom } from 'delta-state' +import { create, useAtom, Atomic } from 'delta-state' interface CounterStore { - counterAtom: DerivedAtom + counterAtom: Atomic }; const useCounterStore = create((set) => ({ @@ -479,23 +479,17 @@ export default function CounterApp() { useCounterAtom: state.counterAtom })); - const [ - counter, - setCounter - ] = useCounterAtom( - 0, - (set) => (next: number) => set(next + 1) - ); + const atom = useCounterAtom(0); return ( <>
-

- {counter} + {atom.get()}

@@ -584,7 +578,7 @@ To access `get()`, just specify it as an argument in addition to `set()`!
---- -### Async Usage Without React 🤖 +### Async and 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()`. diff --git a/examples/async.ts b/examples/async.ts index 821856a..8b50b01 100644 --- a/examples/async.ts +++ b/examples/async.ts @@ -1,25 +1,21 @@ import { atom } from "../src/async"; const test = async () => { + const atomTwo = await atom(0); + const atomThree = await atom(0); - const atomTwo = await atom(0) - const atomThree = await atom(0) - - atomThree.subscribe((count) => { - atomTwo.set(count + atomTwo.get()) - console.log(count) - }) - - atomThree.set(1) - atomThree.set(1) - atomThree.set(1) - - console.log(atomThree.get()) + atomTwo.set(count + atomTwo.get()); + console.log(count); + }); - console.log(atomTwo.get()) + atomThree.set(1); + atomThree.set(1); + atomThree.set(1); + console.log(atomThree.get()); + console.log(atomTwo.get()); }; test(); diff --git a/package.json b/package.json index acc3754..120d8d1 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-import-resolver-alias": "^1.1.2", + "eslint-import-resolver-typescript": "^3.6.1", "eslint-plugin-import": "^2.29.1", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-react": "^7.33.2", @@ -62,9 +63,9 @@ "vite-plugin-dts": "^3.7.3" }, "dependencies": { - "use-sync-external-store": "^1.2.0", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "use-sync-external-store": "^1.2.0" }, "files": [ "dist" diff --git a/src/async/atom.ts b/src/async/atom.ts index 3daa466..73a17fd 100644 --- a/src/async/atom.ts +++ b/src/async/atom.ts @@ -1,71 +1,69 @@ -import { createAtomApi } from "src/atom.ts"; -import { Atom, Read } from "src/base/types.ts"; +import { createAtomApi, getValueFromCreator } from "~/base/atom.ts"; +import { Atom, Read } from "~/base/types.ts"; +const getValueFromAsyncCreator = async (creator: T | Read>) => { + const getState = (atom: Atom): T => atom.get(); + const syncCreator = creator as (get: (atom: Atom) => T) => T; + + return typeof creator === "function" ? await syncCreator(getState) : creator; +}; const createAsyncAtom = async ( creator: T | Read>, comparator?: ({ next, prev }: { next: T; prev: T }) => boolean, ) => { + const createNextAtom = createAtomApi(); + const atomStore = createNextAtom({} as any); -const createNextAtom = createAtomApi(); -const atomStore = createNextAtom({} as any); - -const getState = (atom: Atom): T => atom.get() - -const asyncCreator = creator as ( - get: (atom: Atom) => T -) => Promise + const getState = (atom: Atom): T => atom.get(); - const init = typeof creator === 'function' ? await asyncCreator(getState) : creator; - atomStore.setState(init) + const asyncCreator = creator as (get: (atom: Atom) => T) => Promise; - const atom = { - store: atomStore, - get: () => atomStore.getState(), - set: (next: T) => { - const shouldUpdate = !comparator || comparator({ - next, - prev: atomStore.getState() - }) + const init = + typeof creator === "function" ? await asyncCreator(getState) : creator; + atomStore.setState(init); - if (shouldUpdate){ - atomStore.setState(next) - atomStore.subscribers.forEach((callback) => { - callback(next) - }) - } - - }, - subscribe: ( - callback: (next: T) => void, - callbackComparator?: ({ - next, - prev - }:{ - next: T, - prev: T - }) => boolean - ) => { + const atom = { + store: atomStore, + get: (transform?: Read) => + transform ? getValueFromCreator(transform) : atomStore.getState(), + set: async (next: T | Read>) => { + const value = await getValueFromAsyncCreator(next); + const shouldUpdate = + !comparator || + comparator({ + next: value, + prev: atomStore.getState(), + }); - if (callbackComparator){ - const currentState = atomStore.getState() - - atomStore.subscribers.add((state) => callbackComparator({ - next: state as T, - prev: currentState as any - }) && callback(state as T)); - - } else { - atomStore.subscribers.add( - callback as (state: any) => void - ); - } + if (shouldUpdate) { + atomStore.setState(value); + atomStore.subscribers.forEach((callback) => { + callback(value); + }); } - } as Atom + }, + subscribe: ( + callback: (next: T) => void, + callbackComparator?: ({ next, prev }: { next: T; prev: T }) => boolean, + ) => { + if (callbackComparator) { + const currentState = atomStore.getState(); - return atom + atomStore.subscribers.add( + (state) => + callbackComparator({ + next: state as T, + prev: currentState as any, + }) && callback(state as T), + ); + } else { + atomStore.subscribers.add(callback as (state: any) => void); + } + }, + } as Atom; + return atom; }; - - -export const atom = createAsyncAtom; \ No newline at end of file + +export const atom = createAsyncAtom; diff --git a/src/async/index.ts b/src/async/index.ts index d0cf3f3..d88d07b 100644 --- a/src/async/index.ts +++ b/src/async/index.ts @@ -1,2 +1,2 @@ -export { atom } from './atom' -export { create } from './store' \ No newline at end of file +export { atom } from "./atom"; +export { create } from "./store"; diff --git a/src/async/store.ts b/src/async/store.ts index f809f4f..d7f6fec 100644 --- a/src/async/store.ts +++ b/src/async/store.ts @@ -1,37 +1,30 @@ -import { - createInternalBaseReference, - createStoreApi -} from 'src/base/store.ts' -import { Listener } from 'src/base/types.ts' - - - +import { createInternalBaseReference, createStoreApi } from "~/base/store.ts"; +import { Listener } from "~/base/types.ts"; const createAsyncStoreFromState = () => { - const useCreatedStore = async ( - creator: (set: (next: Partial) => void, get: () => U) => Promise, - ) => { - const createNextStore = createStoreApi(); - const store = createNextStore({} as any); - - const setState = (next: Partial): void => { - store.subscribers.forEach((callback: Listener) => { - callback(next); - }); - }; - - const getState = (): U => store.getState(); - - const init = await creator(setState, getState); - store.setState({ - next: init, + const useCreatedStore = async ( + creator: (set: (next: Partial) => void, get: () => U) => Promise, + ) => { + const createNextStore = createStoreApi(); + const store = createNextStore({} as any); + + const setState = (next: Partial): void => { + store.subscribers.forEach((callback: Listener) => { + callback(next); }); - - return createInternalBaseReference(store); }; - - return useCreatedStore; + + const getState = (): U => store.getState(); + + const init = await creator(setState, getState); + store.setState({ + next: init, + }); + + return createInternalBaseReference(store); }; - - - export const create = createAsyncStoreFromState() \ No newline at end of file + + return useCreatedStore; +}; + +export const create = createAsyncStoreFromState(); diff --git a/src/base/atom.ts b/src/base/atom.ts index 88a4654..878aac8 100644 --- a/src/base/atom.ts +++ b/src/base/atom.ts @@ -1,4 +1,11 @@ -import { Atom, Read, AtomStore, Listener } from "./types.ts"; +import { Atom, AtomStore, Listener, Read } from "./types.ts"; + +export const getValueFromCreator = (creator: T | Read) => { + const getState = (atom: Atom): T => atom.get(); + const syncCreator = creator as (get: (atom: Atom) => T) => T; + + return typeof creator === "function" ? syncCreator(getState) : creator; +}; export const createAtom = (atom: T): AtomStore => ({ @@ -21,72 +28,60 @@ export const createAtom = (atom: T): AtomStore => export const createAtomApi = () => createAtom; - - const createBaseAtom = ( creator: T | Read, comparator?: ({ next, prev }: { next: T; prev: T }) => boolean, ) => { - const createNextAtom = createAtomApi(); const atomStore = createNextAtom({} as any); - const getState = (atom: Atom): T => atom.get() + const getState = (atom: Atom): T => atom.get(); - const syncCreator = creator as ( - get: (atom: Atom) => T - ) => T + const syncCreator = creator as (get: (atom: Atom) => T) => T; - const init = typeof creator === 'function' ? syncCreator(getState) : creator; - atomStore.setState(init) + const init = typeof creator === "function" ? syncCreator(getState) : creator; + atomStore.setState(init); const atom = { store: atomStore, get: () => atomStore.getState(), - set: (next: T) => { - const shouldUpdate = !comparator || comparator({ - next, - prev: atomStore.getState() - }) + set: (next: T | Read) => { + const value = getValueFromCreator(next); + const shouldUpdate = + !comparator || + comparator({ + next: value, + prev: atomStore.getState(), + }); - if (shouldUpdate){ - atomStore.setState(next) + if (shouldUpdate) { + atomStore.setState(value); atomStore.subscribers.forEach((callback) => { - callback(next) - }) + callback(value); + }); } - }, subscribe: ( callback: (next: T) => void, - callbackComparator?: ({ - next, - prev - }:{ - next: T, - prev: T - }) => boolean + callbackComparator?: ({ next, prev }: { next: T; prev: T }) => boolean, ) => { + if (callbackComparator) { + const currentState = atomStore.getState(); - if (callbackComparator){ - const currentState = atomStore.getState() - - atomStore.subscribers.add((state) => callbackComparator({ - next: state as T, - prev: currentState as any - }) && callback(state as T)); - - } else { atomStore.subscribers.add( - callback as (state: any) => void + (state) => + callbackComparator({ + next: state as T, + prev: currentState as any, + }) && callback(state as T), ); + } else { + atomStore.subscribers.add(callback as (state: any) => void); } - } - } as Atom - -return atom + }, + } as Atom; + return atom; }; - - -export const atom = createBaseAtom; \ No newline at end of file + +export const atom = createBaseAtom; diff --git a/src/base/index.ts b/src/base/index.ts index d0cf3f3..d88d07b 100644 --- a/src/base/index.ts +++ b/src/base/index.ts @@ -1,2 +1,2 @@ -export { atom } from './atom' -export { create } from './store' \ No newline at end of file +export { atom } from "./atom"; +export { create } from "./store"; diff --git a/src/base/store.ts b/src/base/store.ts index fc97501..1c34485 100644 --- a/src/base/store.ts +++ b/src/base/store.ts @@ -1,44 +1,42 @@ -import { Listener } from './types.ts' -import { Store } from './types.ts'; - +import { Listener, Store } from "./types.ts"; export const createStoreApi = () => { - const implementStore = (state: T) => ({ - state, - subscribers: new Set>(), - subscribe(callback: Listener) { - this.subscribers.add(callback); - return () => this.subscribers.delete(callback); - }, - getState() { - return this.state; - }, - getInitState() { - return state; - }, - setState({ - next, - replace = false, - }: { - next: Partial; - replace?: boolean; - }) { - const nextState = - typeof next === "function" - ? (next as (next: T) => T)(next) - : (next as T); - - if (!Object.is(nextState, this.state)) { - this.state = - replace ?? (typeof nextState !== "object" || nextState === null) - ? nextState - : Object.assign({}, this.state, nextState); - } - }, - delete(callback: Listener) { - this.subscribers.delete(callback); - }, - }); + const implementStore = (state: T) => ({ + state, + subscribers: new Set>(), + subscribe(callback: Listener) { + this.subscribers.add(callback); + return () => this.subscribers.delete(callback); + }, + getState() { + return this.state; + }, + getInitState() { + return state; + }, + setState({ + next, + replace = false, + }: { + next: Partial; + replace?: boolean; + }) { + const nextState = + typeof next === "function" + ? (next as (next: T) => T)(next) + : (next as T); + + if (!Object.is(nextState, this.state)) { + this.state = + replace ?? (typeof nextState !== "object" || nextState === null) + ? nextState + : Object.assign({}, this.state, nextState); + } + }, + delete(callback: Listener) { + this.subscribers.delete(callback); + }, + }); return implementStore; }; @@ -49,7 +47,6 @@ const useBaseExternalStoreWithSelector = ( comparator?: (a: Selection, b: Selection) => boolean, ) => { const callback = (next: Snapshot) => { - const requestedUpdate = { ...store.getState(), ...(next ? next : {}), @@ -108,22 +105,19 @@ export const createInternalBaseReference = (store: Store) => { }; const callbackWithComparator = ( - callbackComparator: ({ - next, - prev - }:{ - next: U, - prev: U - }) => boolean, - subscriptionCallback: (next: U) => void + 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)); - } + const currentState = store.getState(); + + store.subscribers.add( + (state) => + callbackComparator({ + next: state as U, + prev: currentState as any, + }) && subscriptionCallback(state as U), + ); + }; return { ...selection, @@ -141,25 +135,16 @@ export const createInternalBaseReference = (store: Store) => { }, subscribe( callback: (next: U) => void, - callbackComparator?: ({ - next, - prev - }:{ - next: U, - prev: U - }) => boolean + callbackComparator?: ({ next, prev }: { next: U; prev: U }) => boolean, ) { - callbackComparator ? callbackWithComparator( - callbackComparator, - callback - ) : store.subscribers.add( - callback as (state: any) => void - ); + callbackComparator + ? callbackWithComparator(callbackComparator, callback) + : store.subscribers.add(callback as (state: any) => void); }, }; }; - Object.assign(useCreatedStore, store) + Object.assign(useCreatedStore, store); return useCreatedStore; }; @@ -190,5 +175,4 @@ const createBaseStoreFromState = () => { return useCreatedStore; }; - -export const create = createBaseStoreFromState() \ No newline at end of file +export const create = createBaseStoreFromState(); diff --git a/src/base/types.ts b/src/base/types.ts index baed399..7b304bd 100644 --- a/src/base/types.ts +++ b/src/base/types.ts @@ -1,4 +1,3 @@ - export type Listener = (next: Partial) => void; export type Store = { @@ -12,48 +11,30 @@ export type Store = { }; export type AtomStore = { - value: T; - update: (next: T) => void; - subscribers: Set>; - subscribe: (callback: Listener) => () => boolean; - getState: () => T; - getInitState: () => T; - setState: (state: T) => void; - delete: (callback: Listener) => void; - }; - - - export type Atom = { - store: AtomStore; - get: () => T; - set: (next: T) => void; - subscribe: ( - callback: Listener, - comparator?: ({ - next, - prev - }: { - next: T, - prev: T - }) => boolean - ) => void; - } - + value: T; + update: (next: T) => void; + subscribers: Set>; + subscribe: (callback: Listener) => () => boolean; + getState: () => T; + getInitState: () => T; + setState: (state: T) => void; + delete: (callback: Listener) => void; +}; +export type Atom = { + store: AtomStore; + get: (transform?: Read) => T; + set: (next: T | Read) => void; + subscribe: ( + callback: Listener, + comparator?: ({ next, prev }: { next: T; prev: T }) => boolean, + ) => void; +}; -export type AtomHook = ( - selector: (value: T) => T, - comparator?: (({ next, prev }: { next: T; prev: T }) => boolean) | undefined, - ) => [T, (next: T) => void]; - -export type DerivedAtom = ( - atom: T, - update: (set: (next: T) => T) => (next: T) => T | Promise, - link?: ((source: T, local: T) => T) | undefined, -) => [T, (next: T) => T | Promise]; +export type Atomic = ( + creator: T | Read, + link?: ((source: T, local: T) => T) | undefined, +) => Atom; - - export type Getter = (atom: Atom) => Value - export type Read = ( - get: Getter - ) => Value \ No newline at end of file +export type Getter = (atom: Atom) => Value; +export type Read = (get: Getter) => Value; diff --git a/src/index.ts b/src/index.ts index 728800b..b1b5a08 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,3 @@ -export { atom } from './base' -export { useAtom, create } from './react' +export { atom } from "./base"; +export { useAtom, create } from "./react"; +export { type Atomic } from "./base/types"; diff --git a/src/react/atom.ts b/src/react/atom.ts index 74242f6..d137362 100644 --- a/src/react/atom.ts +++ b/src/react/atom.ts @@ -1,100 +1,69 @@ -import { Atom, Read, Listener } from "src/base/types.ts"; -import { createAtomApi } from 'src/base/atom.ts' -import { useRef, useMemo, useSyncExternalStore } from "react"; +import { useMemo, useRef, useSyncExternalStore } from "react"; +import { createAtomApi, getValueFromCreator } from "~/base/atom.ts"; +import { Atom, Listener, Read } from "~/base/types.ts"; - -const getValueFromCreator = ( - creator: T | Read -) => { - - const getState = (atom: Atom): T => atom.get() - const syncCreator = creator as ( - get: (atom: Atom) => T - ) => T - - - return typeof creator === 'function' ? syncCreator(getState) : creator; -} - - -const createAtom = ( - creator: T | Read -) => { +const createAtom = (creator: T | Read) => { const createNextAtom = createAtomApi(); const atomStore = createNextAtom({} as any); + const init = getValueFromCreator(creator); + atomStore.setState(init); - const init = getValueFromCreator( - creator - ) - atomStore.setState(init) - return { store: atomStore, - get: () => atomStore.getState(), - set: (next: T) => { - atomStore.setState(next) - atomStore.subscribers.forEach((callback) => { - callback(next) - }) - + get: (transform?: Read) => + transform ? getValueFromCreator(transform) : atomStore.getState(), + set: (next: T | Read) => { + const value = getValueFromCreator(next); + atomStore.setState(value); + atomStore.subscribers.forEach((callback: Listener) => { + callback(value); + }); }, subscribe: ( callback: (next: T) => void, - callbackComparator?: ({ - next, - prev - }:{ - next: T, - prev: T - }) => boolean + callbackComparator?: ({ next, prev }: { next: T; prev: T }) => boolean, ) => { + if (callbackComparator) { + const currentState = atomStore.getState(); - if (callbackComparator){ - const currentState = atomStore.getState() - - atomStore.subscribers.add((state) => callbackComparator({ - next: state as T, - prev: currentState as any - }) && callback(state as T)); - - } else { atomStore.subscribers.add( - callback as (state: any) => void + (state) => + callbackComparator({ + next: state as T, + prev: currentState as any, + }) && callback(state as T), ); + } else { + atomStore.subscribers.add(callback as (state: any) => void); } - } - } as Atom - -} - + }, + } as Atom; +}; export const useAtom = ( creator: T | Read, link?: (source: T, local: T) => T, ) => { - const atomRef = useRef(createAtom(creator)).current; - const lastLinkedState = useRef( - getValueFromCreator(creator) - ); + const lastLinkedState = useRef(getValueFromCreator(creator)); - const creatorValue = getValueFromCreator(creator) + const creatorValue = getValueFromCreator(creator); useMemo(() => { - if (lastLinkedState.current !== atomRef && link) { + if (lastLinkedState.current !== creatorValue && link) { + atomRef.store.value = link(creatorValue, atomRef.store.value); + lastLinkedState.current = creatorValue; - atomRef.store.value = link(lastLinkedState.current, atomRef.store.value); } - }, [creatorValue, atomRef, link]); + }, [creatorValue, lastLinkedState, atomRef, link]); const value = useSyncExternalStore( (callback: Listener) => atomRef.store.subscribe(callback), - () => atomRef.get(), () => atomRef.store.value, - ) - atomRef.store.value = value - - return atomRef + () => atomRef.store.value, + ); + atomRef.store.value = value; + return atomRef; }; diff --git a/src/react/index.ts b/src/react/index.ts index 6d5a05c..b8d81bc 100644 --- a/src/react/index.ts +++ b/src/react/index.ts @@ -1,2 +1,2 @@ -export { useAtom } from './atom' -export { create } from './store' \ No newline at end of file +export { useAtom } from "./atom"; +export { create } from "./store"; diff --git a/src/react/store.ts b/src/react/store.ts index a3580b8..e61c031 100644 --- a/src/react/store.ts +++ b/src/react/store.ts @@ -1,5 +1,5 @@ import useSyncExports from "use-sync-external-store/shim/with-selector.js"; -import { Listener, Store } from "src/base/types.ts"; +import { Listener, Store } from "~/base/types.ts"; const { useSyncExternalStoreWithSelector } = useSyncExports; @@ -64,7 +64,7 @@ const createInternalReference = (store: Store, init: T) => { ); }; - Object.assign(useCreatedStore, store) + Object.assign(useCreatedStore, store); return useCreatedStore; }; @@ -96,7 +96,4 @@ const createStoreFromState = () => { return useCreatedStore; }; - - - -export const create = createStoreFromState(); \ No newline at end of file +export const create = createStoreFromState(); diff --git a/tsconfig.json b/tsconfig.json index a606669..923b41f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,7 +27,10 @@ "allowImportingTsExtensions": true, "moduleResolution": "bundler", "noUncheckedIndexedAccess": true, - "baseUrl": "." + "baseUrl": ".", + "paths": { + "~/*": ["./src/*"] + } }, "include": ["src"], "exclude": ["node_modules", "dist"] From 038798ba3a7c6a987b7ee727b529df035f5e27c5 Mon Sep 17 00:00:00 2001 From: Ada Lundhe Date: Wed, 28 Feb 2024 12:55:04 -0600 Subject: [PATCH 3/3] AL: update atom --- README.md | 386 +++++++++++++++++++++++---------------------------- package.json | 2 +- 2 files changed, 176 insertions(+), 212 deletions(-) diff --git a/README.md b/README.md index e776883..0911aed 100644 --- a/README.md +++ b/README.md @@ -216,43 +216,20 @@ And like that we've create and consumed our first Delta store! Using a store to manage a single bit of state naturally seems a bit clunky and cumbersome. This is where Delta's other state management mechanism - atoms - comes in handy. -Unlike full-blown stores, atoms are designed specifically to handle smaller, focused bits of state. Delta offers two ways to instantiate them - via `atom()` and via the `useAtom()` hook. Let's start with the former: +Unlike full-blown stores, atoms are designed specifically to handle smaller, focused bits of state. Delta offers two ways to instantiate them - via the `useAtom()` hook: ```tsx // app.tsx - -import { atom } from 'delta-state' - -const useTrainerAtom = atom((set) => [ - 'Ash', - (updatedName: string) => set(updatedName) -]); - -const TrainerAboutPage = () => { - ... -} -``` -
- -Like a store, we call the atom method, pass a type or interface specifying what the type of state we're providing is, pass a function specifying the initial state, and return a custom React hook to consume in a component. Unlike a store, we only need to pass the type of data specific to the atom then specify an initial value and an action to update the atom's state. - -This condensed API is what makes atoms unique - like React's `useState()` all information relevant to the given piece of state are effectively specified inline. Unlike `useState()`, we get to define how that state is updated right in the declaration. This means no matter where an atom is consumed, the mechanism for manipulating its state remains as consistent as possible. - -Continuing below, we call our custom `useTrainerAtom()` hook inside our component, passing a selector like we did when we called our custom store hook: - - -```tsx -// app.tsx -... +import { useAtom } from 'delta-state' const TrainerAboutPage = () => { - const [name, updatedName] = useTrainerAtom((state) => state); + const trainerName = useAtom('Ash'); return (
-

Hello, my name is {name}

+

Hello, my name is {trainerName.get()}

@@ -260,7 +237,7 @@ const TrainerAboutPage = () => { updateName(e.target.value)} + onChange={(e) => trainerName.set(e.target.value)} />
@@ -268,18 +245,12 @@ const TrainerAboutPage = () => { ); } ``` -
- -> Why Selectors with Atoms? 💡 -> -> While atoms are designed to handle discrete pieces of state, sometimes these discrete pieces may take the form of arrays, objects, or other more complex data. By requiring use of selectors with atoms like we do with stores, we make dealing with complex data easier and achieve a more consistent API. - -
-Like `useState()`, our call to our atom hook returns a two-element array, with the first item being the state value and the second being the action to update that state. +The `useAtom()` hook returns an atom object, which we can then call `get()` and `set()` on to access and update state. -Finally let's examine the `useAtom()` hook: +This condensed API is what makes atoms unique - like React's `useState()` all information relevant to the given piece of state are effectively specified inline. Unlike `useState()`, the methods to manipulate and retrieve state are owned by the atom, so keeping track of where changes occur and state is being accessed is easier. +We can do a lot more than just passing values to atoms - `get()`, `set()` and calls to `useAtom()` also accept functions with a helpful (optional) `get()` arg to extract values from other atoms: ```tsx // app.tsx @@ -287,15 +258,13 @@ import { useAtom } from 'delta-state' const TrainerAboutPage = () => { - const [name, updatedName] = useAtom( - 'Ash', - (set) => (updatedName: string) => set(updatedName) - ); + const lastName = useAtom('Pikachu'); + const trainerName = useAtom((get) => `Ash ${get(lastName)}`); return (
-

Hello, my name is {name}

+

Hello, my name is {trainerName.get((get) => `${get(trainerName)}!`)}

@@ -303,7 +272,7 @@ const TrainerAboutPage = () => { updateName(e.target.value)} + onChange={(e) => trainerName.set(() => e.target.value)} />
@@ -311,15 +280,44 @@ const TrainerAboutPage = () => { ); } ``` -
-This achieves exactly what the above example does, but localizes the state to the given component. +This allows you to compose atoms while keeping individual atom state pure. -> Atom vs useAtom? 💡 -> -> In general, we recommend using `atom()` when you need to share a discrete piece of state between multiple components and `useAtom()` when that state needs to be local to the component. +Atoms also allow you to subscribe to state updates, via `subscribe()`: -
+```tsx +import { useAtom } from 'delta-state' + +export default function CounterApp() { + + const atom = useAtom(0); + const atomTwo = useAtom('even'); + + // Our subscription will trigger every time a state update + // for our first atom occurs, which we can then use to set + // the state of our second atom. + atom.subscribe((count) => { + atomTwo.set((get) => count%2 ? 'odd' : 'even') + }); + + return ( + <> +
+
+ +

+ {atom.get()} +

+
+
+ + ); +} +``` + +By default, subscriptions will trigger any time a given atom's state is updated. ---- ### Recipes 🍲 @@ -358,13 +356,7 @@ const CounterApp = () => { add: state.add })); - const [ - counter, - setCounter - ] = useAtom( - count, - (set) => (next: number) => set(next + 1) - ); + const atom = useAtom(count); return ( <> @@ -372,15 +364,15 @@ const CounterApp = () => {
-

- {counter} + {atom.get()}

@@ -389,7 +381,6 @@ const CounterApp = () => { } ``` -
Our app above uses a store and an atom created using the `useAtom()` hook to manage the same counter. Since our atom's state is created using the `useAtom()` hook from the state of the store's count value, we refer to the atom as a derived atom. We want both the "Increment Local" and "Increment Global" buttons to increase our counter. However when we press "Increase Global" nothing happens! What's the deal?! @@ -420,14 +411,7 @@ const CounterApp = () => { add: state.add })); - const [ - counter, - setCounter - ] = useAtom( - count, - (set) => (next: number) => set(next + 1), - (source, next) => next + 1 - ); + const atom = useAtom(count, (source, local) => local + 1); return ( <> @@ -435,15 +419,15 @@ const CounterApp = () => {
-

- {counter} + {atom.get()}

@@ -451,7 +435,6 @@ const CounterApp = () => { ); } ``` -
A `link()` is a function accepting two arguments - the first the source state and the second the atom's "local" state - and returns a value matching the type specified to the atom that the atom will use for its next state. Link functions allow you to reconcile the difference between the source and local state of a derived atom so that the behavior of your application remains consistent.
@@ -498,12 +481,12 @@ export default function CounterApp() { } ``` -Delta includes the `DerivedAtom` type, which allows you to pass the useAtom hook as a store item. You can then alias and instantiate an instance of that atom wherever needed! +Delta includes the `Atomic` type, which allows you to pass the useAtom hook as a store item. You can then create an instance of that atom wherever needed!
#### Comparators and Controlling State Updates -In addition to selectors store and atom hooks you create via `atom()` can take an optional comparator function that will only allow for store or atom state to be updated if the comparator function returns `true`: +Stores and the atom's `subscribe()` method take an optional `comparator()` function that allows you to filter state updates or subscription events: ```tsx import { create } from 'delta-state' @@ -542,8 +525,13 @@ const CounterApp = () => { return ( <>
-
-

@@ -557,6 +545,43 @@ const CounterApp = () => { ``` +For atoms, comparators can be used to filter subscription events: + +```tsx +import { useAtom } from 'delta-state' + +export default function CounterApp() { + + const atom = useAtom(0); + + atom.subscribe( + (count) => { + console.log('Next odd number is: ', count); + }, + + // Our subscription will only trigger + // on odd numbers. We can also omit `prev` + // since we aren't using it. + ({ next }) => next%2 === 1 + ); + + return ( + <> +
+
+ +

+ {atom.get()} +

+
+
+ + ); +} +``` + Comparator functions accept a single object argument containing `next` and `prev` - with `next` being the requested state update and `prev` being the currnet store or atom state - and return a `boolean` (the value of the comparison performed in the function). We recommend using comparators to optimize your application's performance by controlling when state updates and React re-renders occur.
@@ -580,46 +605,70 @@ To access `get()`, just specify it as an argument in addition to `set()`! ---- ### Async and 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()`. +Delta supports both sync and async use without react. Import from either `delta-state/async` or `delta-state/base` to access the async and base versions of atoms. -Let's start with `createAsync()` - as before we'll create an interface to represent our store's state and actions: +Rather than calling `useAtom()`, you'll instead call `atom()` to create an `async` or `base` atom: ```ts // async.ts -import { createAsync } from "delta-state"; +import { atom } from "delta-state/base"; -interface CounterStore { - count: number; - updateCount: (next: number) => void; +const runCounter = () => { + const counterAtom = atom(0) + counterAtom.set((get) => get(counterAtom) + 1) } -const asyncCounter = async () => { - + +runCounter() +``` + +Async atoms require async functions for `atom()` and `set()`, which must be awaited: + +```ts +// async.ts + +import { atom } from "delta-state/async"; + +const runCounter = async () => { + const counterAtom = await atom(async (get) => 0) + await counterAtom.set(async (get) => get(counterAtom) + 1) } -asyncCounter() +runCounter() ``` -Next we'll instantiate an async store instace via `createAsync()`: - +Creating async and base stores follows much the same pattern as their react counterpart, with the exception that you must pass an async function and await the Promise returned by `create()` for async stores: ```ts -// async.ts +import { create } from "delta-state/async"; + +interface Store { + counter: number; + add: (amount: number) => void +} + +// This isn't just a function but a Promise! We'll need to +// await it. +const createCounterStore = create(async (set) => ({ + counter: 0, + add: (amount) => set({ + counter: amount + 1 + }) +})); + ... -const asyncCounter = async () => { +const runAsyncCounter = async () => { - // The call to createAsync() returns a Promise so + // The call to create() 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) => ({ + const asyncStore = await createCounterStore( + (set, get) => ({ count: 0, updateCount: (next: number) => set({ @@ -629,21 +678,31 @@ const asyncCounter = async () => { ); } -asyncCounter() +runAsyncCounter() ``` -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: +For base stores: ```ts -// async.ts +import { create } from "delta-state/base"; -... +interface Store { + counter: number; + add: (amount: number) => void +} +const createCounterStore = create((set) => ({ + counter: 0, + add: (amount) => set({ + counter: amount + 1 + }) +})); -const asyncCounter = async () => { - const asyncStore = await createAsync( - async (set, get) => ({ +... + +const runCounter = () => { + const customStore = createCounterStore( + (set, get) => ({ count: 0, updateCount: (next: number) => set({ @@ -651,25 +710,13 @@ const asyncCounter = async () => { }) }) ); - - 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() +runCounter() ``` -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 + +As with React stores, we return both our value and action. However, +unlike React stores, state for base and async store 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 @@ -677,10 +724,10 @@ to access the updated store value after calling an action you must call the `get ... -const asyncCounter = async () => { +const runAsyncCounter = async () => { const asyncStore = await createAsync( - async (set, get) => ({ + (set, get) => ({ count: 0, updateCount: (next: number) => set({ @@ -705,7 +752,7 @@ const asyncCounter = async () => { } -asyncCounter() +runAsyncCounter() ``` We can also update the store by calling `set()`, which works exactly as it does when you call it inside an action: @@ -715,10 +762,10 @@ We can also update the store by calling `set()`, which works exactly as it does ... -const asyncCounter = async () => { +const runAsyncCounter = async () => { const asyncStore = await createAsync( - async (set, get) => ({ + (set, get) => ({ count: 0, updateCount: (next: number) => set({ @@ -738,19 +785,19 @@ const asyncCounter = async () => { } -asyncCounter() +runAsyncCounter() ``` -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: +If you need to access store state changes reactively (i.e. whenever an action or `set()` mutates store state), you can call `subscribe()` on both async and base stores. Like `subscribe()` for atoms, the function takes a callback and optional comparator function: ```ts // async.ts ... -const asyncCounter = async () => { +const runAsyncCounter = async () => { const asyncStore = await createAsync( - async (set, get) => ({ + (set, get) => ({ count: 0, updateCount: (next: number) => set({ @@ -787,93 +834,10 @@ const asyncCounter = async () => { } -asyncCounter() +runAsyncCounter() ``` -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/package.json b/package.json index 120d8d1..48be585 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "delta-state", - "version": "1.8.1", + "version": "1.9.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",