From 6878338f12691d62a7035096aa9ad4d20bd57b3e Mon Sep 17 00:00:00 2001 From: Ada Lundhe Date: Sun, 10 Mar 2024 14:31:01 -0500 Subject: [PATCH] AL: reduce API to just stores and add async/Suspense support --- README.md | 567 ++++--------------------------------------- package.json | 2 +- src/async/index.ts | 1 - src/async/store.ts | 30 --- src/base/atom.ts | 108 --------- src/base/index.ts | 3 +- src/base/store.ts | 264 ++++++++------------ src/base/types.ts | 41 ++-- src/index.ts | 5 +- src/react/atom.ts | 102 -------- src/react/context.ts | 8 +- src/react/derived.ts | 30 --- src/react/index.ts | 2 - src/react/store.ts | 126 ++++------ 14 files changed, 220 insertions(+), 1069 deletions(-) delete mode 100644 src/async/index.ts delete mode 100644 src/async/store.ts delete mode 100644 src/base/atom.ts delete mode 100644 src/react/atom.ts delete mode 100644 src/react/derived.ts diff --git a/README.md b/README.md index 3a1f3f8..84e4617 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ # delta ๐ŸŒŒ -Delta is a Typescript-first, minimal, and composable state manager for React that takes from the best features of ๐Ÿป [Zustand](https://github.com/pmndrs/zustand/tree/main) and ๐Ÿ‘ป [Jotai](https://github.com/pmndrs/jotai). Combining the concepts of state stores and atoms with a Zustand-like API, Delta lets your state flow the way you want. +Delta is a Typescript-first, minimal state manager for React that takes from the best features of ๐Ÿป [Zustand](https://github.com/pmndrs/zustand/tree/main) and ๐Ÿ‘ป [Jotai](https://github.com/pmndrs/jotai). Delta combines a Zustand-like API with support for React Suspense, letting you manage your state the way you want. ---- ### Wait another state manager? ๐Ÿ˜ตโ€๐Ÿ’ซ @@ -211,280 +211,15 @@ And like that we've create and consumed our first Delta store!

---- -### Going Atomic ๐Ÿ”ฌ - -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. Let's create an atom via the `useAtom()` hook: - -```tsx -// app.tsx -import { useAtom } from 'delta-state' - -const TrainerAboutPage = () => { - - const trainerName = useAtom('Ash'); - - return ( -
-
-

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

-
-
-
- - trainerName.set(e.target.value)} - /> -
-
-
- ); -} -``` - -We can then call `get()` and `set()` on our atom to access and update 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()`, the methods to manipulate and retrieve state are owned by the atom, so keeping track of where changes occur and state is being consumed 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 -import { useAtom } from 'delta-state' - -const TrainerAboutPage = () => { - - const lastName = useAtom('Pikachu'); - const trainerName = useAtom((get) => `Ash ${get(lastName)}`); - - return ( -
-
-

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

-
-
-
- - trainerName.set(() => e.target.value)} - /> -
-
-
- ); -} -``` - -This allows you to compose atoms while keeping individual atom state pure. - -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 ๐Ÿฒ Let's cover some tricks and techniques for Delta!
-#### Linking ๐Ÿ”— - -One of the primary advantages of Delta we first mentioned was composable state by combining stores and atoms. One of the important features of Delta's atoms is that, like Jotai, atoms can be derived from any piece of state - including other atoms. Let's look at the counter app below: - -```tsx -import { create, useAtom } from 'delta-state' - -interface CounterStore { - counter: number - add: (next: number) => void -}; - -const useCounterStore = create((set) => ({ - counter: 0, - add: (next: number) => set({ - counter: next + 1 - }) -})); - -const CounterApp = () => { - - const { - count, - add - } = useCounterStore((state) => ({ - count: state.counter, - add: state.add - })); - - const atom = useAtom(count); - - return ( - <> -
-
- - -

- {atom.get()} -

-
-
- - ); -} - -``` - -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?! - -By default, atom state is isolated. Only that atom's `set()` can update it's state. However, we can tell our atom we want it to listen for and update based on changes to source state by providing a `link()`: - -```tsx -import { create, useAtom } from 'delta-state' - -interface CounterStore { - counter: number - add: (next: number) => void -}; - -const useCounterStore = create((set) => ({ - counter: 0, - add: (next: number) => set({ - counter: next + 1 - }) -})); - -const CounterApp = () => { - - const { - count, - add - } = useCounterStore((state) => ({ - count: state.counter, - add: state.add - })); - - const atom = useAtom(count, (source, local) => local + 1); - - return ( - <> -
-
- - -

- {atom.get()} -

-
-
- - ); -} -``` - -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. -
- -#### Stores as Atom Generators ๐Ÿงช - -Stores aren't solely for holding application state - they can also be used to generate atoms on-the-fly! - -```tsx -import { create, useAtom, Atomic } from 'delta-state' - -interface CounterStore { - counterAtom: Atomic -}; - -const useCounterStore = create((set) => ({ - counterAtom: useAtom -})); - -export default function CounterApp() { - - const { - useCounterAtom - } = useCounterStore((state) => ({ - useCounterAtom: state.counterAtom - })); - - const atom = useCounterAtom(0); - - return ( - <> -
-
- -

- {atom.get()} -

-
-
- - ); -} -``` - -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 -Stores and the atom's `subscribe()` method take an optional `comparator()` function that allows you to filter state updates or subscription events: +A store's `subscribe()` method can take an optional `comparator()` function as an argument, which allows you to filter state updates or subscription events: ```tsx import { create } from 'delta-state' @@ -543,44 +278,7 @@ 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. +Comparator functions accept a single object argument containing `next` and `prev` - with `next` being the requested state update and `prev` being the current store 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.
#### Getting State in an Action @@ -601,244 +299,73 @@ To access `get()`, just specify it as an argument in addition to `set()`!
---- -### Async and Usage Without React ๐Ÿค– - -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. - -Rather than calling `useAtom()`, you'll instead call `atom()` to create an `async` or `base` atom: - +### Use with Suspense ๐Ÿค– -```ts -// async.ts +Borrowing from Jotai, Delta stores support Suspense and async usage. To enable async usage, you need to pass an async function to the call to `create()` and wrap the store type with a `Promise` generic: -import { atom } from "delta-state/base"; - -const runCounter = () => { - const counterAtom = atom(0) - counterAtom.set((get) => get(counterAtom) + 1) -} - - -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) -} - - -runCounter() -``` - -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 -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 - }) -})); +```tsx +// app.tsx +import { create } from 'delta-state' -... +type PokemonTrainer = { + trainerName: string; + updateTrainerName: (updatedName: string) => void; +}; -const runAsyncCounter = async () => { - - // 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 createCounterStore( - (set, get) => ({ - count: 0, - updateCount: (next: number) => - set({ - count: next + get().count, - }) +// Here we pass an async function and wrap our `PokemonTrainer` type +// with a Promise generic. +const useTrainerStore = create>(async (set) => ({ + trainerName: "Ash", + updateTrainerName: (updatedName: string) => set({ + trainerName: updatedName }) - ); -} - -runAsyncCounter() -``` - -For base stores: - -```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 runCounter = () => { - const customStore = createCounterStore( - (set, get) => ({ - count: 0, - updateCount: (next: number) => - set({ - count: next + get().count, - }) - }) - ); -} - -runCounter() -``` - -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 -// async.ts - -... - -const runAsyncCounter = async () => { - - const asyncStore = await createAsync( - (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() - +const TrainerAboutPage = () => { + ... } - -runAsyncCounter() ``` -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 +The call your hook and use your store as you normally would! +```tsx +// app.tsx ... -const runAsyncCounter = async () => { - - const asyncStore = await createAsync( - (set, get) => ({ - count: 0, - updateCount: (next: number) => - set({ - count: next + get().count, - }) - }) - ); +const TrainerAboutPage = () => { const { - set - } = asyncStore((state) => state); - - // Our count will now be 10. - set({ - count: 10 - }) - -} - -runAsyncCounter() -``` -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 runAsyncCounter = async () => { + name, + updateName + } = useTrainerStore((state) => ({ + name: state.trainerName, + updateName: state.updateTrainerName + })); - const asyncStore = await createAsync( - (set, get) => ({ - count: 0, - updateCount: (next: number) => - set({ - count: next + get().count, - }) - }) + return ( +
+
+

Hello, my name is {name}

+
+
+
+ + updateName(e.target.value)} + /> +
+
+
); - - 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) - } - -runAsyncCounter() - ``` ---- ### Credits and Thanks ๐Ÿ™ -A massive thank you to Pmndrs, the Zustand and Jotai maintainers, and Daishi Kato for creating two wonderful state management libraries that inspired this project. You make writing software fun and worthwhile. +A massive thank you to Poimandres, the Zustand and Jotai maintainers, and Daishi Kato for creating two wonderful state management libraries that inspired this project. You make writing software fun and worthwhile. -AL diff --git a/package.json b/package.json index 7813403..1557c3c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "delta-state", - "version": "1.10.0", + "version": "1.11.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/async/index.ts b/src/async/index.ts deleted file mode 100644 index dc877e1..0000000 --- a/src/async/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { create } from "./store"; diff --git a/src/async/store.ts b/src/async/store.ts deleted file mode 100644 index d7f6fec..0000000 --- a/src/async/store.ts +++ /dev/null @@ -1,30 +0,0 @@ -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, - }); - - return createInternalBaseReference(store); - }; - - return useCreatedStore; -}; - -export const create = createAsyncStoreFromState(); diff --git a/src/base/atom.ts b/src/base/atom.ts deleted file mode 100644 index 3208d1c..0000000 --- a/src/base/atom.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { Atom, Listener, Read } from "./types.ts"; - -export const isPromiseLike = (x: unknown): x is PromiseLike => - typeof (x as any)?.then === "function"; - -export const use = ( - promise: PromiseLike & { - status?: "pending" | "fulfilled" | "rejected"; - value?: T; - reason?: unknown; - }, -): T => { - if (promise.status === "pending") { - throw promise; - } else if (promise.status === "fulfilled") { - return promise.value as T; - } else if (promise.status === "rejected") { - throw promise.reason; - } else { - promise.status = "pending"; - promise.then( - (v) => { - promise.status = "fulfilled"; - promise.value = v; - }, - (e) => { - promise.status = "rejected"; - promise.reason = e; - }, - ); - throw promise; - } -}; - -const isGetter = (x: unknown): x is Read => typeof x === "function"; - -export const getValue = (getState: T | Read): T => { - if (isPromiseLike(getState)) { - return use(getState) as Awaited; - } else if (isGetter(getState)) { - return getState((atom: Atom): V => atom.get()); - } else { - return getState; - } -}; - -export const createBaseAtom = ( - get: T | Read, - comparator?: ({ next, prev }: { next: T; prev: T }) => boolean, - setter?: (value: T) => void, -) => { - const atom = { - value: get, - subscribers: new Set>(), - get(next?: T | Read) { - return next ? getValue(next) : getValue(this.value); - }, - set(next: T | Read) { - const value = getValue(next) as unknown as Awaited; - const shouldUpdate = - !comparator || - comparator({ - next: value, - prev: getValue(this.value), - }); - - if (shouldUpdate) { - this.value = value; - - setter && setter(this.value); - this.subscribers.forEach((callback) => { - callback(value); - }); - } - }, - subscribe( - callback: (next: T) => void, - callbackComparator?: ({ next, prev }: { next: T; prev: T }) => boolean, - ) { - if (callbackComparator) { - const subscribers = this.subscribers.add( - (state) => - callbackComparator({ - next: state as T, - prev: getValue(this.value), - }) && callback(state as T), - ); - - return () => - subscribers.forEach((callback) => { - callback({}); - }); - } else { - const subscribers = this.subscribers.add( - callback as (state: any) => void, - ); - return () => - subscribers.forEach((callback) => { - callback({}); - }); - } - }, - } as Atom; - - return atom; -}; - -export const atom = createBaseAtom; diff --git a/src/base/index.ts b/src/base/index.ts index d88d07b..42a785b 100644 --- a/src/base/index.ts +++ b/src/base/index.ts @@ -1,2 +1 @@ -export { atom } from "./atom"; -export { create } from "./store"; +export { create } from "./store.ts"; diff --git a/src/base/store.ts b/src/base/store.ts index 1c34485..7602963 100644 --- a/src/base/store.ts +++ b/src/base/store.ts @@ -1,178 +1,110 @@ -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); - }, - }); - - return implementStore; +import { Listener, Read, StateStore } from "./types.ts"; + +export const isPromiseLike = (x: unknown): x is PromiseLike => + typeof (x as any)?.then === "function"; + +export const use = ( + promise: PromiseLike & { + status?: "pending" | "fulfilled" | "rejected"; + value?: T; + reason?: unknown; + }, +): T => { + if (promise.status === "pending") { + throw promise; + } else if (promise.status === "fulfilled") { + return promise.value as T; + } else if (promise.status === "rejected") { + throw promise.reason; + } else { + promise.status = "pending"; + promise.then( + (v) => { + promise.status = "fulfilled"; + promise.value = v; + }, + (e) => { + promise.status = "rejected"; + promise.reason = e; + }, + ); + throw promise; + } }; -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, - }); - }; +const isGetter = (x: unknown): x is Read => typeof x === "function"; - store.subscribe(callback as any); - return selector(store.getState()); +export const getValue = (getState: T | Read): T => { + if (isPromiseLike(getState)) { + return use(getState) as Awaited; + } else if (isGetter(getState)) { + return getState((store: StateStore): V => store.get()); + } else { + return 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, +export const createBaseStore = ( + get: T | Read, + comparator?: ({ next, prev }: { next: T; prev: T }) => boolean, + setter?: (value: T) => void, +) => { + const store = { + value: get, + comparator: comparator, + setter: setter, + subscribers: new Set>(), + get(next?: T | Read) { + return next ? getValue(next) : getValue(this.value); + }, + set(next: T | Read) { + const value = getValue(next) as unknown as Awaited; + const shouldUpdate = + !this.comparator || + this.comparator({ + next: value, + prev: getValue(this.value), }); - }; - - 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), - ); - }; + if (shouldUpdate) { + this.value = value; - return { - ...selection, - get() { - store.subscribe(callback); - return selector(store.getState()); - }, - set(state: Partial) { - store.subscribers.forEach((callback: Listener) => { - callback({ - ...store.getState(), - ...state, - }); + setter && setter(this.value); + this.subscribers.forEach((callback) => { + callback(value); }); - }, - 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); - }; + } + }, + subscribe( + callback: (next: T) => void, + callbackComparator?: ({ next, prev }: { next: T; prev: T }) => boolean, + ) { + if (callbackComparator) { + const subscribers = this.subscribers.add( + (state) => + callbackComparator({ + next: state as T, + prev: getValue(this.value), + }) && callback(state as T), + ); + + return () => + subscribers.forEach((callback) => { + callback({}); + }); + } else { + const subscribers = this.subscribers.add( + callback as (state: any) => void, + ); + return () => + subscribers.forEach((callback) => { + callback({}); + }); + } + }, + } as StateStore; - return useCreatedStore; + return store; }; -export const create = createBaseStoreFromState(); +export const create = createBaseStore; diff --git a/src/base/types.ts b/src/base/types.ts index 8ece793..4c51b25 100644 --- a/src/base/types.ts +++ b/src/base/types.ts @@ -1,29 +1,10 @@ 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: (atom?: Atom) => T; - getInitState: () => T; - setState: (state: T) => void; - delete: (callback: Listener) => void; -}; - -export type Atom = { +export type StateStore = { value: T; subscribers: Set>; + comparator?: ({ next, prev }: { next: T; prev: T }) => boolean; + setter?: (value: T) => void; get: (next?: Read) => T; set: (next: T | Read) => void; subscribe: ( @@ -32,10 +13,18 @@ export type Atom = { ) => () => void; }; -export type Atomic = ( - creator: T | Read, +export type Store = ( + creator: T | Read | ReadWrite, link?: ((source: T, local: T) => T) | undefined, -) => Atom; +) => StateStore; -export type Getter = (atom: Atom) => Value; +export type Getter = (store: StateStore) => Value; export type Read = (get: Getter) => Value; + +export type StoreGetter = ( + store?: StateStore ? Awaited : Value>, +) => Value extends PromiseLike ? Awaited : Value; +export type StoreSetter = ( + next: Partial ? Awaited : Value>, +) => void; +export type ReadWrite = (set: StoreSetter, get: StoreGetter) => V; diff --git a/src/index.ts b/src/index.ts index cfcb969..93c9cbf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,2 @@ -export { atom } from "./base"; -export { useAtom, create, useDerived } from "./react"; -export { type Atomic } from "./base/types"; +export { create } from "./react"; +export { type Store } from "./base/types.ts"; diff --git a/src/react/atom.ts b/src/react/atom.ts deleted file mode 100644 index c59160f..0000000 --- a/src/react/atom.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { - ReducerWithoutAction, - useCallback, - useEffect, - useReducer, - useRef, -} from "react"; -import { isPromiseLike, use } from "~/base/atom.ts"; -import { Atom } from "~/base/types.ts"; -import { useStore } from "./context.ts"; - -const useGetAtomStore = (atom: Atom) => { - const store = useStore(); - - const [[value, _], rerender] = useReducer< - ReducerWithoutAction]>, - undefined - >( - (prev) => { - const nextValue = atom.get(); - if (Object.is(prev[0], nextValue) && prev[1] === atom) { - return prev; - } - return [nextValue, atom]; - }, - undefined, - () => [store.get(), atom], - ); - - useEffect(() => { - const unsub = atom.subscribe(() => { - rerender(); - }); - return unsub; - }, [atom]); - - return isPromiseLike(value) ? use(value) : (value as Awaited); -}; - -const useSetAtomStore = (atom: Atom) => { - const setAtom = useCallback( - (next: T) => { - next !== atom.get() && atom.set(next); - }, - [atom], - ); - return setAtom; -}; - -export const useAtomStore = (atom: Atom) => { - return [useGetAtomStore(atom), useSetAtomStore(atom)] as [ - T, - (value: T) => void, - ]; -}; - -export const useProxy = (atom: Atom) => { - type ValueType = V extends PromiseLike ? Awaited : V; - type ProxyType = { - value: ValueType; - }; - - useAtomStore(atom); - - return useRef( - new Proxy( - { - value: atom.get(), - } as ProxyType extends { - value: ValueType; - } - ? ProxyType - : any, - { - get() { - return atom.get(); - }, - set( - target: ProxyType, - props: string, - value: ProxyType[keyof ProxyType], - _: any, - ) { - if (atom.get() !== value) { - target[props as keyof ProxyType] = value; - atom.set(value); - } - return true; - }, - }, - ), - ).current as { - value: ValueType; - }; -}; - -export const useAtom = (atom: Atom) => { - useAtomStore(atom); - const atomRef = useRef(atom).current; - - return atomRef as T extends PromiseLike ? Atom> : Atom; -}; diff --git a/src/react/context.ts b/src/react/context.ts index 7d00a0e..623829d 100644 --- a/src/react/context.ts +++ b/src/react/context.ts @@ -1,16 +1,16 @@ import { createContext, useContext } from "react"; -import { createBaseAtom, getValue } from "~/base/atom.ts"; +import { createBaseStore, getValue } from "~/base/store.ts"; import { Read } from "~/base/types.ts"; export const atom = (creator: T | Read) => { - return createBaseAtom(getValue(creator)); + return createBaseStore(getValue(creator)); }; -type Store = ReturnType>; +type Store = ReturnType>; const AtomContext = createContext | undefined>(undefined); export const useStore = (options?: { store: Store }): Store => { const store = useContext(AtomContext); - return options?.store || store || createBaseAtom(undefined as T); + return options?.store || store || createBaseStore(undefined as T); }; diff --git a/src/react/derived.ts b/src/react/derived.ts deleted file mode 100644 index 564d61f..0000000 --- a/src/react/derived.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { useMemo, useRef, useState } from "react"; -import { createBaseAtom, getValue, isPromiseLike } from "~/base/atom.ts"; - -export const useDerived = ( - derived: T, - options?: { - comparator?: ({ next, prev }: { next: T; prev: T }) => boolean; - link?: (source: T, local: T) => T; - }, -) => { - const atomValue = ( - isPromiseLike(derived) ? getValue(derived) : derived - ) as Awaited; - - const comparator = useRef(options?.comparator).current; - const link = useRef(options?.link).current; - - const lastLinkedState = useRef(atomValue); - const [value, setAtom] = useState(atomValue); - const atomRef = useRef(createBaseAtom(value, comparator, setAtom)).current; - - useMemo(() => { - if (lastLinkedState.current !== atomValue && link) { - atomRef.value = link(atomValue, atomRef.value) as Awaited; - lastLinkedState.current = atomValue; - } - }, [atomValue, lastLinkedState, atomRef, link]); - - return atomRef; -}; diff --git a/src/react/index.ts b/src/react/index.ts index 40e4006..42a785b 100644 --- a/src/react/index.ts +++ b/src/react/index.ts @@ -1,3 +1 @@ -export { useAtom, useProxy } from "./atom.ts"; -export { useDerived } from "./derived.ts"; export { create } from "./store.ts"; diff --git a/src/react/store.ts b/src/react/store.ts index e61c031..906694c 100644 --- a/src/react/store.ts +++ b/src/react/store.ts @@ -1,96 +1,74 @@ -import useSyncExports from "use-sync-external-store/shim/with-selector.js"; -import { Listener, Store } from "~/base/types.ts"; +import { useMemo, useReducer, useRef } from "react"; +import { createBaseStore, isPromiseLike, use } from "~/base/store.ts"; +import { ReadWrite, StateStore } from "~/base/types.ts"; -const { useSyncExternalStoreWithSelector } = useSyncExports; +const createStore = ( + store: StateStore, + init: T | ReadWrite, + selector: (state: Awaited) => U, + comparator?: ({ next, prev }: { next: U; prev: U }) => boolean, +) => { + store.comparator = comparator as typeof store.comparator; + store.value = selector( + (isPromiseLike(init) ? use(init) : init) as Awaited, + ) as unknown as T; -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; + return store as unknown as StateStore; }; -const createInternalReference = (store: Store, init: T) => { +const createInternalAtomReference = ( + atom: StateStore, + init: T | ReadWrite, +) => { const useCreatedStore = ( - selector: (state: T) => U, + selector: (state: Awaited) => 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, - ); - }; + const atomRef = useRef( + createStore(atom, init, selector, comparator), + ).current; - Object.assign(useCreatedStore, store); + const prev = useRef(atomRef.value); + + const [value, rerender] = useReducer((_: U, next: U) => { + atomRef.value = next; + return next; + }, atomRef.value); + + useMemo(() => { + atomRef.subscribe((next) => { + if (!Object.is(prev.current, next)) { + prev.current = next as U; + rerender(next as U); + } + }); + }, [atomRef]); + + return value as unknown as U; + }; return useCreatedStore; }; const createStoreFromState = () => { - const useCreatedStore = ( - creator: (set: (next: Partial) => void, get: () => U) => U, - ) => { - const createNextStore = createStoreApi(); - const store = createNextStore({} as any); + const useCreatedStore = (creator: ReadWrite) => { + const store = createBaseStore({} as any); const setState = (next: Partial): void => { - store.setState({ - next, - }); - store.subscribers.forEach((callback) => callback({})); + store.set({ + ...store.get(), + ...next, + } as U); }; - const getState = (): U => store.getState(); + const getState = ( + selected?: StateStore, + ): U extends PromiseLike ? Awaited : U => + selected ? selected.get() : store.get(); - const init = creator(setState, getState); - store.setState({ - next: init, - }); + const init = creator(setState, getState as any); - return createInternalReference(store, init); + return createInternalAtomReference(store, init); }; return useCreatedStore;