diff --git a/.changeset/petite-actors-lie2.md b/.changeset/petite-actors-lie2.md new file mode 100644 index 00000000..df0b90d7 --- /dev/null +++ b/.changeset/petite-actors-lie2.md @@ -0,0 +1,10 @@ +--- +'@tanstack/angular-store': minor +'@tanstack/preact-store': minor +'@tanstack/svelte-store': minor +'@tanstack/solid-store': minor +'@tanstack/vue-store': minor +'@tanstack/store': minor +--- + +feat: introduce more frameworks hooks for other non-react adapters diff --git a/.gitignore b/.gitignore index cc376bd6..182bead0 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,8 @@ dist .env.test.local .env.production.local .next +.angular/* +.cache/* npm-debug.log* yarn-debug.log* @@ -41,3 +43,16 @@ stats.html vite.config.js.timestamp-* vite.config.ts.timestamp-* + +examples/angular/atoms/.angular/cache/* +examples/angular/atoms/.angular/cache/21.2.7/atoms/angular-compiler.db +examples/angular/atoms/.angular/cache/21.2.7/atoms/angular-compiler.db-lock +examples/angular/store-actions/.angular/cache/21.2.7/store-actions/.tsbuildinfo +examples/angular/store-actions/.angular/cache/21.2.7/store-actions/angular-compiler.db +examples/angular/store-actions/.angular/cache/21.2.7/store-actions/angular-compiler.db-lock +examples/angular/store-context/.angular/cache/21.2.7/store-context/.tsbuildinfo +examples/angular/store-context/.angular/cache/21.2.7/store-context/angular-compiler.db +examples/angular/store-context/.angular/cache/21.2.7/store-context/angular-compiler.db-lock +examples/angular/stores/.angular/cache/21.2.7/stores/.tsbuildinfo +examples/angular/stores/.angular/cache/21.2.7/stores/angular-compiler.db +examples/angular/stores/.angular/cache/21.2.7/stores/angular-compiler.db-lock diff --git a/docs/config.json b/docs/config.json index 23c3c93a..e84654ae 100644 --- a/docs/config.json +++ b/docs/config.json @@ -83,88 +83,171 @@ "label": "API Reference", "children": [ { - "label": "JavaScript Reference", + "label": "Core API Reference", "to": "reference/index" + } + ], + "frameworks": [ + { + "label": "react", + "children": [ + { + "label": "React Hooks", + "to": "framework/react/reference/index" + } + ] + }, + { + "label": "vue", + "children": [ + { + "label": "Vue Hooks", + "to": "framework/vue/reference/index" + } + ] + }, + { + "label": "solid", + "children": [ + { + "label": "Solid Hooks", + "to": "framework/solid/reference/index" + } + ] }, { - "label": "Classes / Store", + "label": "angular", + "children": [ + { + "label": "Angular Inject API", + "to": "framework/angular/reference/index" + } + ] + }, + { + "label": "svelte", + "children": [ + { + "label": "Svelte Hooks", + "to": "framework/svelte/reference/index" + } + ] + }, + { + "label": "preact", + "children": [ + { + "label": "Preact Hooks", + "to": "framework/preact/reference/index" + } + ] + } + ] + }, + { + "collapsible": true, + "defaultCollapsed": true, + "label": "Store API Reference", + "children": [ + { + "label": "Store", "to": "reference/classes/Store" }, { - "label": "Interfaces / Atom", - "to": "reference/interfaces/atom" + "label": "ReadonlyStore", + "to": "reference/classes/ReadonlyStore" + }, + { + "label": "Atom", + "to": "reference/interfaces/Atom" + }, + { + "label": "AtomOptions", + "to": "reference/interfaces/AtomOptions" + }, + { + "label": "BaseAtom", + "to": "reference/interfaces/BaseAtom" + }, + { + "label": "InternalBaseAtom", + "to": "reference/interfaces/InternalBaseAtom" + }, + { + "label": "InternalReadonlyAtom", + "to": "reference/interfaces/InternalReadonlyAtom" }, { - "label": "Interfaces / AtomOptions", - "to": "reference/interfaces/atomoptions" + "label": "InteropSubscribable", + "to": "reference/interfaces/InteropSubscribable" }, { - "label": "Interfaces / BaseAtom", - "to": "reference/interfaces/baseatom" + "label": "Readable", + "to": "reference/interfaces/Readable" }, { - "label": "Interfaces / InternalBaseAtom", - "to": "reference/interfaces/internalbaseatom" + "label": "ReadonlyAtom", + "to": "reference/interfaces/ReadonlyAtom" }, { - "label": "Interfaces / InternalReadonlyAtom", - "to": "reference/interfaces/internalreadonlyatom" + "label": "StoreActionsApi", + "to": "reference/interfaces/StoreActionsApi" }, { - "label": "Interfaces / InteropSubscribable", - "to": "reference/interfaces/interopsubscribable" + "label": "Subscribable", + "to": "reference/interfaces/Subscribable" }, { - "label": "Interfaces / Readable", - "to": "reference/interfaces/readable" + "label": "Subscription", + "to": "reference/interfaces/Subscription" }, { - "label": "Interfaces / ReadonlyAtom", - "to": "reference/interfaces/readonlyatom" + "label": "AnyAtom", + "to": "reference/type-aliases/AnyAtom" }, { - "label": "Interfaces / Subscribable", - "to": "reference/interfaces/subscribable" + "label": "Observer", + "to": "reference/type-aliases/Observer" }, { - "label": "Interfaces / Subscription", - "to": "reference/interfaces/subscription" + "label": "Selection", + "to": "reference/type-aliases/Selection" }, { - "label": "Type Aliases / AnyAtom", - "to": "reference/type-aliases/anyatom" + "label": "StoreAction", + "to": "reference/type-aliases/StoreAction" }, { - "label": "Type Aliases / Observer", - "to": "reference/type-aliases/observer" + "label": "StoreActionMap", + "to": "reference/type-aliases/StoreActionMap" }, { - "label": "Type Aliases / Selection", - "to": "reference/type-aliases/selection" + "label": "StoreActionsFactory", + "to": "reference/type-aliases/StoreActionsFactory" }, { - "label": "Functions / batch", + "label": "batch", "to": "reference/functions/batch" }, { - "label": "Functions / createAsyncAtom", - "to": "reference/functions/createasyncatom" + "label": "createAsyncAtom", + "to": "reference/functions/createAsyncAtom" }, { - "label": "Functions / createAtom", - "to": "reference/functions/createatom" + "label": "createAtom", + "to": "reference/functions/createAtom" }, { - "label": "Functions / createStore", - "to": "reference/functions/createstore" + "label": "createStore", + "to": "reference/functions/createStore" }, { - "label": "Functions / flush", + "label": "flush", "to": "reference/functions/flush" }, { - "label": "Functions / toObserver", - "to": "reference/functions/toobserver" + "label": "toObserver", + "to": "reference/functions/toObserver" } ], "frameworks": [ @@ -172,44 +255,48 @@ "label": "react", "children": [ { - "label": "React Reference", - "to": "framework/react/reference/index" - }, - { - "label": "Functions / createStoreContext", + "label": "createStoreContext", "to": "framework/react/reference/functions/createStoreContext" }, { - "label": "Functions / useCreateAtom", + "label": "useCreateAtom", "to": "framework/react/reference/functions/useCreateAtom" }, { - "label": "Functions / useValue", + "label": "useCreateStore", + "to": "framework/react/reference/functions/useCreateStore" + }, + { + "label": "useSelector", + "to": "framework/react/reference/functions/useSelector" + }, + { + "label": "useValue", "to": "framework/react/reference/functions/useValue" }, { - "label": "Functions / useSetValue", + "label": "useSetValue", "to": "framework/react/reference/functions/useSetValue" }, { - "label": "Functions / useAtom", + "label": "useAtom", "to": "framework/react/reference/functions/useAtom" }, { - "label": "Functions / useCreateStore", - "to": "framework/react/reference/functions/useCreateStore" + "label": "useStoreActions", + "to": "framework/react/reference/functions/useStoreActions" }, { - "label": "Functions / useSelector", - "to": "framework/react/reference/functions/useSelector" + "label": "useStore (deprecated)", + "to": "framework/react/reference/functions/useStore" }, { - "label": "Variables / useStore (deprecated)", - "to": "framework/react/reference/variables/useStore" + "label": "shallow", + "to": "framework/react/reference/functions/shallow" }, { - "label": "Functions / shallow", - "to": "framework/react/reference/functions/shallow" + "label": "UseSelectorOptions", + "to": "framework/react/reference/interfaces/UseSelectorOptions" } ] }, @@ -217,16 +304,36 @@ "label": "vue", "children": [ { - "label": "Vue Reference", - "to": "framework/vue/reference/index" + "label": "useSelector", + "to": "framework/vue/reference/functions/useSelector" + }, + { + "label": "useValue", + "to": "framework/vue/reference/functions/useValue" }, { - "label": "Functions / useStore", + "label": "useSetValue", + "to": "framework/vue/reference/functions/useSetValue" + }, + { + "label": "useAtom", + "to": "framework/vue/reference/functions/useAtom" + }, + { + "label": "useStoreActions", + "to": "framework/vue/reference/functions/useStoreActions" + }, + { + "label": "useStore (deprecated)", "to": "framework/vue/reference/functions/useStore" }, { - "label": "Functions / shallow", + "label": "shallow", "to": "framework/vue/reference/functions/shallow" + }, + { + "label": "UseSelectorOptions", + "to": "framework/vue/reference/interfaces/UseSelectorOptions" } ] }, @@ -234,12 +341,36 @@ "label": "solid", "children": [ { - "label": "Solid Reference", - "to": "framework/solid/reference/index" + "label": "useSelector", + "to": "framework/solid/reference/functions/useSelector" }, { - "label": "Functions / useStore", + "label": "useValue", + "to": "framework/solid/reference/functions/useValue" + }, + { + "label": "useSetValue", + "to": "framework/solid/reference/functions/useSetValue" + }, + { + "label": "useAtom", + "to": "framework/solid/reference/functions/useAtom" + }, + { + "label": "useStoreActions", + "to": "framework/solid/reference/functions/useStoreActions" + }, + { + "label": "useStore (deprecated)", "to": "framework/solid/reference/functions/useStore" + }, + { + "label": "shallow", + "to": "framework/solid/reference/functions/shallow" + }, + { + "label": "UseSelectorOptions", + "to": "framework/solid/reference/interfaces/UseSelectorOptions" } ] }, @@ -247,12 +378,36 @@ "label": "angular", "children": [ { - "label": "Angular Reference", - "to": "framework/angular/reference/index" + "label": "injectSelector", + "to": "framework/angular/reference/functions/injectSelector" + }, + { + "label": "injectValue", + "to": "framework/angular/reference/functions/injectValue" + }, + { + "label": "injectSetValue", + "to": "framework/angular/reference/functions/injectSetValue" + }, + { + "label": "injectAtom", + "to": "framework/angular/reference/functions/injectAtom" }, { - "label": "Functions / injectStore", + "label": "injectStoreActions", + "to": "framework/angular/reference/functions/injectStoreActions" + }, + { + "label": "injectStore (deprecated)", "to": "framework/angular/reference/functions/injectStore" + }, + { + "label": "shallow", + "to": "framework/angular/reference/functions/shallow" + }, + { + "label": "InjectSelectorOptions", + "to": "framework/angular/reference/interfaces/InjectSelectorOptions" } ] }, @@ -260,16 +415,36 @@ "label": "svelte", "children": [ { - "label": "Svelte Reference", - "to": "framework/svelte/reference/index" + "label": "useSelector", + "to": "framework/svelte/reference/functions/useSelector" + }, + { + "label": "useValue", + "to": "framework/svelte/reference/functions/useValue" + }, + { + "label": "useSetValue", + "to": "framework/svelte/reference/functions/useSetValue" + }, + { + "label": "useAtom", + "to": "framework/svelte/reference/functions/useAtom" }, { - "label": "Functions / useStore", + "label": "useStoreActions", + "to": "framework/svelte/reference/functions/useStoreActions" + }, + { + "label": "useStore (deprecated)", "to": "framework/svelte/reference/functions/useStore" }, { - "label": "Functions / shallow", + "label": "shallow", "to": "framework/svelte/reference/functions/shallow" + }, + { + "label": "UseSelectorOptions", + "to": "framework/svelte/reference/interfaces/UseSelectorOptions" } ] }, @@ -277,16 +452,48 @@ "label": "preact", "children": [ { - "label": "Preact Reference", - "to": "framework/preact/reference/index" + "label": "createStoreContext", + "to": "framework/preact/reference/functions/createStoreContext" }, { - "label": "Functions / useStore", + "label": "useCreateAtom", + "to": "framework/preact/reference/functions/useCreateAtom" + }, + { + "label": "useCreateStore", + "to": "framework/preact/reference/functions/useCreateStore" + }, + { + "label": "useSelector", + "to": "framework/preact/reference/functions/useSelector" + }, + { + "label": "useValue", + "to": "framework/preact/reference/functions/useValue" + }, + { + "label": "useSetValue", + "to": "framework/preact/reference/functions/useSetValue" + }, + { + "label": "useAtom", + "to": "framework/preact/reference/functions/useAtom" + }, + { + "label": "useStoreActions", + "to": "framework/preact/reference/functions/useStoreActions" + }, + { + "label": "useStore (deprecated)", "to": "framework/preact/reference/functions/useStore" }, { - "label": "Functions / shallow", + "label": "shallow", "to": "framework/preact/reference/functions/shallow" + }, + { + "label": "UseSelectorOptions", + "to": "framework/preact/reference/interfaces/UseSelectorOptions" } ] } @@ -327,6 +534,22 @@ { "label": "Simple", "to": "framework/vue/examples/simple" + }, + { + "label": "Atoms", + "to": "framework/vue/examples/atoms" + }, + { + "label": "Stores", + "to": "framework/vue/examples/stores" + }, + { + "label": "Store Actions", + "to": "framework/vue/examples/store-actions" + }, + { + "label": "Store Context", + "to": "framework/vue/examples/store-context" } ] }, @@ -336,6 +559,22 @@ { "label": "Simple", "to": "framework/angular/examples/simple" + }, + { + "label": "Atoms", + "to": "framework/angular/examples/atoms" + }, + { + "label": "Stores", + "to": "framework/angular/examples/stores" + }, + { + "label": "Store Actions", + "to": "framework/angular/examples/store-actions" + }, + { + "label": "Store Context", + "to": "framework/angular/examples/store-context" } ] }, @@ -345,6 +584,22 @@ { "label": "Simple", "to": "framework/svelte/examples/simple" + }, + { + "label": "Atoms", + "to": "framework/svelte/examples/atoms" + }, + { + "label": "Stores", + "to": "framework/svelte/examples/stores" + }, + { + "label": "Store Actions", + "to": "framework/svelte/examples/store-actions" + }, + { + "label": "Store Context", + "to": "framework/svelte/examples/store-context" } ] }, @@ -354,6 +609,22 @@ { "label": "Simple", "to": "framework/solid/examples/simple" + }, + { + "label": "Atoms", + "to": "framework/solid/examples/atoms" + }, + { + "label": "Stores", + "to": "framework/solid/examples/stores" + }, + { + "label": "Store Actions", + "to": "framework/solid/examples/store-actions" + }, + { + "label": "Store Context", + "to": "framework/solid/examples/store-context" } ] }, @@ -363,6 +634,22 @@ { "label": "Simple", "to": "framework/preact/examples/simple" + }, + { + "label": "Atoms", + "to": "framework/preact/examples/atoms" + }, + { + "label": "Stores", + "to": "framework/preact/examples/stores" + }, + { + "label": "Store Actions", + "to": "framework/preact/examples/store-actions" + }, + { + "label": "Store Context", + "to": "framework/preact/examples/store-context" } ] } diff --git a/docs/framework/angular/quick-start.md b/docs/framework/angular/quick-start.md index b4b48695..89e4847c 100644 --- a/docs/framework/angular/quick-start.md +++ b/docs/framework/angular/quick-start.md @@ -52,7 +52,7 @@ export function updateState(animal: 'dogs' | 'cats') { **display.component.ts** ```angular-ts -import { injectStore } from '@tanstack/angular-store'; +import { injectSelector } from '@tanstack/angular-store'; import { store } from './store'; @Component({ @@ -65,7 +65,7 @@ import { store } from './store'; }) export class Display { animal = input.required(); - count = injectStore(store, (state) => state[this.animal()]); + count = injectSelector(store, (state) => state[this.animal()]); } ``` @@ -86,3 +86,5 @@ export class Increment { updateState = updateState; } ``` + +`injectStore` remains available as a deprecated alias to `injectSelector`. diff --git a/docs/framework/angular/reference/functions/createStoreContext.md b/docs/framework/angular/reference/functions/createStoreContext.md new file mode 100644 index 00000000..05ac169b --- /dev/null +++ b/docs/framework/angular/reference/functions/createStoreContext.md @@ -0,0 +1,94 @@ +--- +id: createStoreContext +title: createStoreContext +--- + +# Function: createStoreContext() + +```ts +function createStoreContext(): object; +``` + +Defined in: [packages/angular-store/src/createStoreContext.ts:48](https://github.com/TanStack/store/blob/main/packages/angular-store/src/createStoreContext.ts#L48) + +Creates a typed Angular dependency-injection context for sharing a bundle of +atoms and stores with a component subtree. + +The returned `provideStoreContext` function accepts a factory that creates the +context value. Using a factory (rather than a static value) ensures each +component instance — and each SSR request — receives its own state, avoiding +cross-request pollution. + +Consumers call `injectStoreContext()` inside an injection context (typically a +constructor or field initializer) to retrieve the contextual atoms and stores, +then compose them with existing hooks like [injectSelector](injectSelector.md), +[injectValue](injectValue.md), and [injectAtom](injectAtom.md). + +## Type Parameters + +### TValue + +`TValue` *extends* `object` + +## Returns + +`object` + +### injectStoreContext() + +```ts +injectStoreContext: () => TValue; +``` + +#### Returns + +`TValue` + +### provideStoreContext() + +```ts +provideStoreContext: (factory) => Provider; +``` + +#### Parameters + +##### factory + +() => `TValue` + +#### Returns + +`Provider` + +## Example + +```ts +const { provideStoreContext, injectStoreContext } = createStoreContext<{ + countAtom: Atom + totalsStore: Store<{ count: number }> +}>() + +// Parent component provides the context +@Component({ + providers: [ + provideStoreContext(() => ({ + countAtom: createAtom(0), + totalsStore: new Store({ count: 0 }), + })), + ], + template: ``, +}) +class ParentComponent {} + +// Child component consumes the context +@Component({ template: `{{ count() }}` }) +class ChildComponent { + private ctx = injectStoreContext() + count = injectValue(this.ctx.countAtom) +} +``` + +## Throws + +When `injectStoreContext()` is called without a matching + `provideStoreContext()` in a parent component's providers. diff --git a/docs/framework/angular/reference/functions/injectAtom.md b/docs/framework/angular/reference/functions/injectAtom.md new file mode 100644 index 00000000..8d9bd6ef --- /dev/null +++ b/docs/framework/angular/reference/functions/injectAtom.md @@ -0,0 +1,48 @@ +--- +id: injectAtom +title: injectAtom +--- + +# Function: injectAtom() + +```ts +function injectAtom(atom, options?): WritableAtomSignal; +``` + +Defined in: [packages/angular-store/src/injectAtom.ts:44](https://github.com/TanStack/store/blob/main/packages/angular-store/src/injectAtom.ts#L44) + +Returns a [WritableAtomSignal](../interfaces/WritableAtomSignal.md) that reads the current atom value when +called and exposes a `.set` method for updates. + +Use this when a component needs to both read and update the same writable +atom. + +## Type Parameters + +### TValue + +`TValue` + +## Parameters + +### atom + +`Atom`\<`TValue`\> + +### options? + +[`InjectSelectorOptions`](../interfaces/InjectSelectorOptions.md)\<`TValue`\> + +## Returns + +[`WritableAtomSignal`](../interfaces/WritableAtomSignal.md)\<`TValue`\> + +## Example + +```ts +readonly count = injectAtom(countAtom) + +increment() { + this.count.set((prev) => prev + 1) +} +``` diff --git a/docs/framework/angular/reference/functions/injectSelector.md b/docs/framework/angular/reference/functions/injectSelector.md new file mode 100644 index 00000000..a92e9bc2 --- /dev/null +++ b/docs/framework/angular/reference/functions/injectSelector.md @@ -0,0 +1,58 @@ +--- +id: injectSelector +title: injectSelector +--- + +# Function: injectSelector() + +```ts +function injectSelector( + source, + selector, +options?): Signal; +``` + +Defined in: [packages/angular-store/src/injectSelector.ts:93](https://github.com/TanStack/store/blob/main/packages/angular-store/src/injectSelector.ts#L93) + +Selects a slice of state from an atom or store and returns it as an Angular +signal. + +This is the primary Angular read hook for TanStack Store. + +## Type Parameters + +### TState + +`TState` + +### TSelected + +`TSelected` = `NoInfer`\<`TState`\> + +## Parameters + +### source + +[`SelectionSource`](../type-aliases/SelectionSource.md)\<`TState`\> + +### selector + +(`state`) => `TSelected` + +### options? + +[`InjectSelectorOptions`](../interfaces/InjectSelectorOptions.md)\<`TSelected`\> + +## Returns + +`Signal`\<`TSelected`\> + +## Examples + +```ts +readonly count = injectSelector(counterStore, (state) => state.count) +``` + +```ts +readonly doubled = injectSelector(countAtom, (value) => value * 2) +``` diff --git a/docs/framework/angular/reference/functions/injectStore-1.md b/docs/framework/angular/reference/functions/injectStore-1.md new file mode 100644 index 00000000..6e89cddc --- /dev/null +++ b/docs/framework/angular/reference/functions/injectStore-1.md @@ -0,0 +1,55 @@ +--- +id: injectStore +title: injectStore +--- + +# ~~Function: injectStore()~~ + +```ts +function injectStore( + store, + selector?, +options?): Signal; +``` + +Defined in: [packages/angular-store/src/injectStore.ts:20](https://github.com/TanStack/store/blob/main/packages/angular-store/src/injectStore.ts#L20) + +Deprecated alias for [injectSelector](injectSelector.md). + +## Type Parameters + +### TState + +`TState` + +### TSelected + +`TSelected` = `NoInfer`\<`TState`\> + +## Parameters + +### store + +[`SelectionSource`](../type-aliases/SelectionSource.md)\<`TState`\> + +### selector? + +(`state`) => `TSelected` + +### options? + +`CompatibilityInjectStoreOptions`\<`TSelected`\> + +## Returns + +`Signal`\<`TSelected`\> + +## Example + +```ts +readonly count = injectStore(counterStore, (state) => state.count) +``` + +## Deprecated + +Use `injectSelector` instead. diff --git a/docs/framework/angular/reference/functions/injectStore.md b/docs/framework/angular/reference/functions/injectStore.md index 0219a908..b88a398b 100644 --- a/docs/framework/angular/reference/functions/injectStore.md +++ b/docs/framework/angular/reference/functions/injectStore.md @@ -1,84 +1,65 @@ --- -id: injectStore -title: injectStore +id: _injectStore +title: _injectStore --- -# Function: injectStore() - -## Call Signature +# Function: \_injectStore() ```ts -function injectStore( +function _injectStore( store, - selector?, -options?): Signal; + selector, + options?): [Signal, [TActions] extends [never] ? (updater) => void : TActions]; ``` -Defined in: [index.ts:16](https://github.com/TanStack/store/blob/main/packages/angular-store/src/index.ts#L16) - -### Type Parameters +Defined in: [packages/angular-store/src/\_injectStore.ts:24](https://github.com/TanStack/store/blob/main/packages/angular-store/src/_injectStore.ts#L24) -#### TState - -`TState` - -#### TSelected - -`TSelected` = `NoInfer`\<`TState`\> - -### Parameters - -#### store - -`Atom`\<`TState`\> - -#### selector? - -(`state`) => `TSelected` +Experimental combined read+write injection function for stores, mirroring +injectAtom's pattern. -#### options? +Returns `[signal, actions]` when the store has an actions factory, or +`[signal, setState]` for plain stores. -`CreateSignalOptions`\<`TSelected`\> & `object` +## Type Parameters -### Returns +### TState -`Signal`\<`TSelected`\> - -## Call Signature +`TState` -```ts -function injectStore( - store, - selector?, -options?): Signal; -``` +### TActions -Defined in: [index.ts:21](https://github.com/TanStack/store/blob/main/packages/angular-store/src/index.ts#L21) +`TActions` *extends* `StoreActionMap` -### Type Parameters +### TSelected -#### TState +`TSelected` = `NoInfer`\<`TState`\> -`TState` +## Parameters -#### TSelected +### store -`TSelected` = `NoInfer`\<`TState`\> +`Store`\<`TState`, `TActions`\> -### Parameters +### selector -#### store +(`state`) => `TSelected` -`Atom`\<`TState`\> | `ReadonlyAtom`\<`TState`\> +### options? -#### selector? +[`InjectSelectorOptions`](../interfaces/InjectSelectorOptions.md)\<`TSelected`\> -(`state`) => `TSelected` +## Returns -#### options? +\[`Signal`\<`TSelected`\>, \[`TActions`\] *extends* \[`never`\] ? (`updater`) => `void` : `TActions`\] -`CreateSignalOptions`\<`TSelected`\> & `object` +## Example -### Returns +```ts +// Store with actions +readonly result = _injectStore(petStore, (s) => s.cats) +// result[0] is Signal, result[1] is actions -`Signal`\<`TSelected`\> +// Store without actions +readonly result = _injectStore(plainStore, (s) => s) +// result[0] is Signal, result[1] is setState +``` diff --git a/docs/framework/angular/reference/functions/injectValue.md b/docs/framework/angular/reference/functions/injectValue.md new file mode 100644 index 00000000..834dc44e --- /dev/null +++ b/docs/framework/angular/reference/functions/injectValue.md @@ -0,0 +1,46 @@ +--- +id: injectValue +title: injectValue +--- + +# Function: injectValue() + +```ts +function injectValue(source, options?): Signal; +``` + +Defined in: [packages/angular-store/src/injectValue.ts:21](https://github.com/TanStack/store/blob/main/packages/angular-store/src/injectValue.ts#L21) + +Returns the current value signal for an atom or store. + +This is the whole-value counterpart to [injectSelector](injectSelector.md). + +## Type Parameters + +### TValue + +`TValue` + +## Parameters + +### source + +`Atom`\<`TValue`\> | `ReadonlyAtom`\<`TValue`\> | `Store`\<`TValue`, `any`\> | `ReadonlyStore`\<`TValue`\> + +### options? + +[`InjectSelectorOptions`](../interfaces/InjectSelectorOptions.md)\<`TValue`\> + +## Returns + +`Signal`\<`TValue`\> + +## Examples + +```ts +readonly count = injectValue(countAtom) +``` + +```ts +readonly state = injectValue(counterStore) +``` diff --git a/docs/framework/angular/reference/index.md b/docs/framework/angular/reference/index.md index b77cf408..a836ecd2 100644 --- a/docs/framework/angular/reference/index.md +++ b/docs/framework/angular/reference/index.md @@ -5,6 +5,20 @@ title: "@tanstack/angular-store" # @tanstack/angular-store +## Interfaces + +- [InjectSelectorOptions](interfaces/InjectSelectorOptions.md) +- [WritableAtomSignal](interfaces/WritableAtomSignal.md) + +## Type Aliases + +- [SelectionSource](type-aliases/SelectionSource.md) + ## Functions -- [injectStore](functions/injectStore.md) +- [\_injectStore](functions/injectStore.md) +- [createStoreContext](functions/createStoreContext.md) +- [injectAtom](functions/injectAtom.md) +- [injectSelector](functions/injectSelector.md) +- [~~injectStore~~](functions/injectStore-1.md) +- [injectValue](functions/injectValue.md) diff --git a/docs/framework/angular/reference/interfaces/InjectSelectorOptions.md b/docs/framework/angular/reference/interfaces/InjectSelectorOptions.md new file mode 100644 index 00000000..993b6db4 --- /dev/null +++ b/docs/framework/angular/reference/interfaces/InjectSelectorOptions.md @@ -0,0 +1,70 @@ +--- +id: InjectSelectorOptions +title: InjectSelectorOptions +--- + +# Interface: InjectSelectorOptions\ + +Defined in: [packages/angular-store/src/injectSelector.ts:11](https://github.com/TanStack/store/blob/main/packages/angular-store/src/injectSelector.ts#L11) + +## Extends + +- `Omit`\<`CreateSignalOptions`\<`TSelected`\>, `"equal"`\> + +## Type Parameters + +### TSelected + +`TSelected` + +## Properties + +### compare()? + +```ts +optional compare: (a, b) => boolean; +``` + +Defined in: [packages/angular-store/src/injectSelector.ts:15](https://github.com/TanStack/store/blob/main/packages/angular-store/src/injectSelector.ts#L15) + +#### Parameters + +##### a + +`TSelected` + +##### b + +`TSelected` + +#### Returns + +`boolean` + +*** + +### debugName? + +```ts +optional debugName: string; +``` + +Defined in: node\_modules/.pnpm/@angular+core@21.2.8\_@angular+compiler@21.2.8\_rxjs@7.8.2\_zone.js@0.16.1/node\_modules/@angular/core/types/\_chrome\_dev\_tools\_performance-chunk.d.ts:54 + +A debug name for the signal. Used in Angular DevTools to identify the signal. + +#### Inherited from + +```ts +Omit.debugName +``` + +*** + +### injector? + +```ts +optional injector: Injector; +``` + +Defined in: [packages/angular-store/src/injectSelector.ts:16](https://github.com/TanStack/store/blob/main/packages/angular-store/src/injectSelector.ts#L16) diff --git a/docs/framework/angular/reference/interfaces/WritableAtomSignal.md b/docs/framework/angular/reference/interfaces/WritableAtomSignal.md new file mode 100644 index 00000000..ce3ea69a --- /dev/null +++ b/docs/framework/angular/reference/interfaces/WritableAtomSignal.md @@ -0,0 +1,54 @@ +--- +id: WritableAtomSignal +title: WritableAtomSignal +--- + +# Interface: WritableAtomSignal()\ + +Defined in: [packages/angular-store/src/injectAtom.ts:21](https://github.com/TanStack/store/blob/main/packages/angular-store/src/injectAtom.ts#L21) + +A callable signal that reads the current atom value when invoked and +exposes a `.set` method matching the atom's native setter contract. + +This is the Angular-idiomatic return type for [injectAtom](../functions/injectAtom.md). It can +be used as a class property and called directly in templates. + +## Example + +```ts +readonly count = injectAtom(countAtom) + +// read in template: {{ count() }} +// write in class: this.count.set(5) +// this.count.set(prev => prev + 1) +``` + +## Type Parameters + +### T + +`T` + +```ts +WritableAtomSignal(): T; +``` + +Defined in: [packages/angular-store/src/injectAtom.ts:23](https://github.com/TanStack/store/blob/main/packages/angular-store/src/injectAtom.ts#L23) + +Read the current value. + +## Returns + +`T` + +## Properties + +### set + +```ts +set: (fn) => void & (value) => void; +``` + +Defined in: [packages/angular-store/src/injectAtom.ts:25](https://github.com/TanStack/store/blob/main/packages/angular-store/src/injectAtom.ts#L25) + +Set the atom value (accepts a direct value or an updater function). diff --git a/docs/framework/angular/reference/type-aliases/SelectionSource.md b/docs/framework/angular/reference/type-aliases/SelectionSource.md new file mode 100644 index 00000000..03444a60 --- /dev/null +++ b/docs/framework/angular/reference/type-aliases/SelectionSource.md @@ -0,0 +1,62 @@ +--- +id: SelectionSource +title: SelectionSource +--- + +# Type Alias: SelectionSource\ + +```ts +type SelectionSource = object; +``` + +Defined in: [packages/angular-store/src/injectSelector.ts:19](https://github.com/TanStack/store/blob/main/packages/angular-store/src/injectSelector.ts#L19) + +## Type Parameters + +### T + +`T` + +## Properties + +### get() + +```ts +get: () => T; +``` + +Defined in: [packages/angular-store/src/injectSelector.ts:20](https://github.com/TanStack/store/blob/main/packages/angular-store/src/injectSelector.ts#L20) + +#### Returns + +`T` + +*** + +### subscribe() + +```ts +subscribe: (listener) => object; +``` + +Defined in: [packages/angular-store/src/injectSelector.ts:21](https://github.com/TanStack/store/blob/main/packages/angular-store/src/injectSelector.ts#L21) + +#### Parameters + +##### listener + +(`value`) => `void` + +#### Returns + +`object` + +##### unsubscribe() + +```ts +unsubscribe: () => void; +``` + +###### Returns + +`void` diff --git a/docs/framework/preact/quick-start.md b/docs/framework/preact/quick-start.md index 841fcab7..a09c6ea0 100644 --- a/docs/framework/preact/quick-start.md +++ b/docs/framework/preact/quick-start.md @@ -7,7 +7,7 @@ The basic preact app example to get started with the TanStack preact-store. ```tsx import { render } from "preact"; -import { createStore, useStore } from "@tanstack/preact-store"; +import { createStore, useSelector } from "@tanstack/preact-store"; // You can instantiate the store outside of Preact components too! export const store = createStore({ @@ -18,7 +18,7 @@ export const store = createStore({ // This will only re-render when `state[animal]` changes. If an unrelated store property changes, it won't re-render const Display = ({ animal }) => { - const count = useStore(store, (state) => state[animal]); + const count = useSelector(store, (state) => state[animal]); return
{`${animal}: ${count}`}
; }; @@ -53,3 +53,4 @@ function App() { render(, document.getElementById("root")); ``` +`useStore` remains available as a deprecated alias to `useSelector`. diff --git a/docs/framework/preact/reference/functions/createStoreContext.md b/docs/framework/preact/reference/functions/createStoreContext.md new file mode 100644 index 00000000..bef5f673 --- /dev/null +++ b/docs/framework/preact/reference/functions/createStoreContext.md @@ -0,0 +1,95 @@ +--- +id: createStoreContext +title: createStoreContext +--- + +# Function: createStoreContext() + +```ts +function createStoreContext(): object; +``` + +Defined in: [preact-store/src/createStoreContext.tsx:44](https://github.com/TanStack/store/blob/main/packages/preact-store/src/createStoreContext.tsx#L44) + +Creates a typed Preact context for sharing a bundle of atoms and stores with +a subtree. + +The returned `StoreProvider` only transports the provided object through +Preact context. Consumers destructure the contextual atoms and stores, then +compose them with the existing hooks like [useSelector](useSelector.md), +[useValue](useValue.md), useSetValue, and [useAtom](useAtom.md). + +The object shape is preserved exactly, so keyed atoms and stores remain fully +typed when read back with `useStoreContext()`. + +## Type Parameters + +### TValue + +`TValue` *extends* `object` + +## Returns + +`object` + +### StoreProvider() + +```ts +StoreProvider: (__namedParameters) => Element; +``` + +#### Parameters + +##### \_\_namedParameters + +###### children? + +`ComponentChildren` + +###### value + +`TValue` + +#### Returns + +`Element` + +### useStoreContext() + +```ts +useStoreContext: () => TValue; +``` + +#### Returns + +`TValue` + +## Example + +```tsx +const { StoreProvider, useStoreContext } = createStoreContext<{ + countAtom: Atom + totalsStore: Store<{ count: number }> +}>() + +function CountButton() { + const { countAtom, totalsStore } = useStoreContext() + const count = useValue(countAtom) + const total = useSelector(totalsStore, (state) => state.count) + + return ( + + ) +} +``` + +## Throws + +When `useStoreContext()` is called outside the matching `StoreProvider`. diff --git a/docs/framework/preact/reference/functions/shallow.md b/docs/framework/preact/reference/functions/shallow.md deleted file mode 100644 index eb6e75c9..00000000 --- a/docs/framework/preact/reference/functions/shallow.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -id: shallow -title: shallow ---- - -# Function: shallow() - -```ts -function shallow(objA, objB): boolean; -``` - -Defined in: [index.ts:116](https://github.com/TanStack/store/blob/main/packages/preact-store/src/index.ts#L116) - -## Type Parameters - -### T - -`T` - -## Parameters - -### objA - -`T` - -### objB - -`T` - -## Returns - -`boolean` diff --git a/docs/framework/preact/reference/functions/useAtom.md b/docs/framework/preact/reference/functions/useAtom.md new file mode 100644 index 00000000..9a0daab4 --- /dev/null +++ b/docs/framework/preact/reference/functions/useAtom.md @@ -0,0 +1,49 @@ +--- +id: useAtom +title: useAtom +--- + +# Function: useAtom() + +```ts +function useAtom(atom, options?): [TValue, (fn) => void & (value) => void]; +``` + +Defined in: [preact-store/src/useAtom.ts:22](https://github.com/TanStack/store/blob/main/packages/preact-store/src/useAtom.ts#L22) + +Returns the current atom value together with a stable setter. + +Use this when a component needs to both read and update the same writable +atom. + +## Type Parameters + +### TValue + +`TValue` + +## Parameters + +### atom + +`Atom`\<`TValue`\> + +### options? + +[`UseSelectorOptions`](../interfaces/UseSelectorOptions.md)\<`TValue`\> + +## Returns + +\[`TValue`, (`fn`) => `void` & (`value`) => `void`\] + +## Example + +```tsx +const [count, setCount] = useAtom(countAtom) + +return ( + +) +``` diff --git a/docs/framework/preact/reference/functions/useCreateAtom.md b/docs/framework/preact/reference/functions/useCreateAtom.md new file mode 100644 index 00000000..a92253d0 --- /dev/null +++ b/docs/framework/preact/reference/functions/useCreateAtom.md @@ -0,0 +1,104 @@ +--- +id: useCreateAtom +title: useCreateAtom +--- + +# Function: useCreateAtom() + +## Call Signature + +```ts +function useCreateAtom(getValue, options?): ReadonlyAtom; +``` + +Defined in: [preact-store/src/useCreateAtom.ts:27](https://github.com/TanStack/store/blob/main/packages/preact-store/src/useCreateAtom.ts#L27) + +Creates a stable atom instance for the lifetime of the component. + +Pass an initial value to create a writable atom, or a getter function to +create a readonly derived atom. This mirrors createAtom, but only +creates the atom once per component mount. + +### Type Parameters + +#### T + +`T` + +### Parameters + +#### getValue + +(`prev?`) => `T` + +#### options? + +`AtomOptions`\<`T`\> + +### Returns + +`ReadonlyAtom`\<`T`\> + +### Example + +```tsx +function Counter() { + const countAtom = useCreateAtom(0) + const [count, setCount] = useAtom(countAtom) + + return ( + + ) +} +``` + +## Call Signature + +```ts +function useCreateAtom(initialValue, options?): Atom; +``` + +Defined in: [preact-store/src/useCreateAtom.ts:31](https://github.com/TanStack/store/blob/main/packages/preact-store/src/useCreateAtom.ts#L31) + +Creates a stable atom instance for the lifetime of the component. + +Pass an initial value to create a writable atom, or a getter function to +create a readonly derived atom. This mirrors createAtom, but only +creates the atom once per component mount. + +### Type Parameters + +#### T + +`T` + +### Parameters + +#### initialValue + +`T` + +#### options? + +`AtomOptions`\<`T`\> + +### Returns + +`Atom`\<`T`\> + +### Example + +```tsx +function Counter() { + const countAtom = useCreateAtom(0) + const [count, setCount] = useAtom(countAtom) + + return ( + + ) +} +``` diff --git a/docs/framework/preact/reference/functions/useCreateStore.md b/docs/framework/preact/reference/functions/useCreateStore.md new file mode 100644 index 00000000..56b8f91c --- /dev/null +++ b/docs/framework/preact/reference/functions/useCreateStore.md @@ -0,0 +1,161 @@ +--- +id: useCreateStore +title: useCreateStore +--- + +# Function: useCreateStore() + +## Call Signature + +```ts +function useCreateStore(getValue): ReadonlyStore; +``` + +Defined in: [preact-store/src/useCreateStore.ts:38](https://github.com/TanStack/store/blob/main/packages/preact-store/src/useCreateStore.ts#L38) + +Creates a stable store instance for the lifetime of the component. + +Pass an initial value to create a writable store, or a getter function to +create a readonly derived store. This mirrors createStore, but only +creates the store once per component mount. + +### Type Parameters + +#### T + +`T` + +### Parameters + +#### getValue + +(`prev?`) => `T` + +### Returns + +`ReadonlyStore`\<`T`\> + +### Example + +```tsx +function Counter() { + const counterStore = useCreateStore({ count: 0 }) + const count = useSelector(counterStore, (state) => state.count) + const setState = useSetValue(counterStore) + + return ( + + ) +} +``` + +## Call Signature + +```ts +function useCreateStore(initialValue): Store; +``` + +Defined in: [preact-store/src/useCreateStore.ts:41](https://github.com/TanStack/store/blob/main/packages/preact-store/src/useCreateStore.ts#L41) + +Creates a stable store instance for the lifetime of the component. + +Pass an initial value to create a writable store, or a getter function to +create a readonly derived store. This mirrors createStore, but only +creates the store once per component mount. + +### Type Parameters + +#### T + +`T` + +### Parameters + +#### initialValue + +`T` + +### Returns + +`Store`\<`T`\> + +### Example + +```tsx +function Counter() { + const counterStore = useCreateStore({ count: 0 }) + const count = useSelector(counterStore, (state) => state.count) + const setState = useSetValue(counterStore) + + return ( + + ) +} +``` + +## Call Signature + +```ts +function useCreateStore(initialValue, actions): Store; +``` + +Defined in: [preact-store/src/useCreateStore.ts:42](https://github.com/TanStack/store/blob/main/packages/preact-store/src/useCreateStore.ts#L42) + +Creates a stable store instance for the lifetime of the component. + +Pass an initial value to create a writable store, or a getter function to +create a readonly derived store. This mirrors createStore, but only +creates the store once per component mount. + +### Type Parameters + +#### T + +`T` + +#### TActions + +`TActions` *extends* `StoreActionMap` + +### Parameters + +#### initialValue + +`NonFunction`\<`T`\> + +#### actions + +`StoreActionsFactory`\<`T`, `TActions`\> + +### Returns + +`Store`\<`T`, `TActions`\> + +### Example + +```tsx +function Counter() { + const counterStore = useCreateStore({ count: 0 }) + const count = useSelector(counterStore, (state) => state.count) + const setState = useSetValue(counterStore) + + return ( + + ) +} +``` diff --git a/docs/framework/preact/reference/functions/useSelector.md b/docs/framework/preact/reference/functions/useSelector.md new file mode 100644 index 00000000..7a406e6b --- /dev/null +++ b/docs/framework/preact/reference/functions/useSelector.md @@ -0,0 +1,59 @@ +--- +id: useSelector +title: useSelector +--- + +# Function: useSelector() + +```ts +function useSelector( + source, + selector, + options?): TSelected; +``` + +Defined in: [preact-store/src/useSelector.ts:127](https://github.com/TanStack/store/blob/main/packages/preact-store/src/useSelector.ts#L127) + +Selects a slice of state from an atom or store and subscribes the component +to that selection. + +This is the primary Preact read hook for TanStack Store. Use it when a +component only needs part of a source value. + +## Type Parameters + +### TSource + +`TSource` + +### TSelected + +`TSelected` + +## Parameters + +### source + +`SelectionSource`\<`TSource`\> + +### selector + +(`snapshot`) => `TSelected` + +### options? + +[`UseSelectorOptions`](../interfaces/UseSelectorOptions.md)\<`TSelected`\> + +## Returns + +`TSelected` + +## Examples + +```tsx +const count = useSelector(counterStore, (state) => state.count) +``` + +```tsx +const doubled = useSelector(countAtom, (value) => value * 2) +``` diff --git a/docs/framework/preact/reference/functions/useStore-1.md b/docs/framework/preact/reference/functions/useStore-1.md new file mode 100644 index 00000000..24850d6d --- /dev/null +++ b/docs/framework/preact/reference/functions/useStore-1.md @@ -0,0 +1,61 @@ +--- +id: useStore +title: useStore +--- + +# ~~Function: useStore()~~ + +```ts +function useStore( + source, + selector, + compare?): TSelected; +``` + +Defined in: [preact-store/src/useStore.ts:13](https://github.com/TanStack/store/blob/main/packages/preact-store/src/useStore.ts#L13) + +Deprecated alias for [useSelector](useSelector.md). + +## Type Parameters + +### TSource + +`TSource` + +### TSelected + +`TSelected` + +## Parameters + +### source + +#### get + +() => `TSource` + +#### subscribe + +(`listener`) => `object` + +### selector + +(`snapshot`) => `TSelected` + +### compare? + +(`a`, `b`) => `boolean` + +## Returns + +`TSelected` + +## Example + +```tsx +const count = useStore(counterStore, (state) => state.count) +``` + +## Deprecated + +Use `useSelector` instead. diff --git a/docs/framework/preact/reference/functions/useStore.md b/docs/framework/preact/reference/functions/useStore.md index b9a67a5d..ecfcbe95 100644 --- a/docs/framework/preact/reference/functions/useStore.md +++ b/docs/framework/preact/reference/functions/useStore.md @@ -1,18 +1,24 @@ --- -id: useStore -title: useStore +id: _useStore +title: _useStore --- -# Function: useStore() +# Function: \_useStore() ```ts -function useStore( +function _useStore( store, selector, - options): TSelected; + options?): [TSelected, [TActions] extends [never] ? (updater) => void : TActions]; ``` -Defined in: [index.ts:100](https://github.com/TanStack/store/blob/main/packages/preact-store/src/index.ts#L100) +Defined in: [preact-store/src/\_useStore.ts:24](https://github.com/TanStack/store/blob/main/packages/preact-store/src/_useStore.ts#L24) + +Experimental combined read+write hook for stores, mirroring useAtom's tuple +pattern. + +Returns `[selected, actions]` when the store has an actions factory, or +`[selected, setState]` for plain stores. ## Type Parameters @@ -20,6 +26,10 @@ Defined in: [index.ts:100](https://github.com/TanStack/store/blob/main/packages/ `TState` +### TActions + +`TActions` *extends* `StoreActionMap` + ### TSelected `TSelected` = `NoInfer`\<`TState`\> @@ -28,16 +38,27 @@ Defined in: [index.ts:100](https://github.com/TanStack/store/blob/main/packages/ ### store -`Atom`\<`TState`\> | `ReadonlyAtom`\<`TState`\> +`Store`\<`TState`, `TActions`\> ### selector (`state`) => `TSelected` -### options +### options? -`UseStoreOptions`\<`TSelected`\> = `{}` +[`UseSelectorOptions`](../interfaces/UseSelectorOptions.md)\<`TSelected`\> ## Returns -`TSelected` +\[`TSelected`, \[`TActions`\] *extends* \[`never`\] ? (`updater`) => `void` : `TActions`\] + +## Example + +```tsx +// Store with actions +const [cats, { addCat }] = _useStore(petStore, (s) => s.cats) + +// Store without actions +const [count, setState] = _useStore(plainStore, (s) => s) +setState((prev) => prev + 1) +``` diff --git a/docs/framework/preact/reference/functions/useValue.md b/docs/framework/preact/reference/functions/useValue.md new file mode 100644 index 00000000..78d43934 --- /dev/null +++ b/docs/framework/preact/reference/functions/useValue.md @@ -0,0 +1,47 @@ +--- +id: useValue +title: useValue +--- + +# Function: useValue() + +```ts +function useValue(source, options?): TValue; +``` + +Defined in: [preact-store/src/useValue.ts:22](https://github.com/TanStack/store/blob/main/packages/preact-store/src/useValue.ts#L22) + +Subscribes to an atom or store and returns its current value. + +This is the whole-value counterpart to [useSelector](useSelector.md). Use it when the +component needs the entire current value from a source. + +## Type Parameters + +### TValue + +`TValue` + +## Parameters + +### source + +`Atom`\<`TValue`\> | `ReadonlyAtom`\<`TValue`\> | `Store`\<`TValue`, `any`\> | `ReadonlyStore`\<`TValue`\> + +### options? + +[`UseSelectorOptions`](../interfaces/UseSelectorOptions.md)\<`TValue`\> + +## Returns + +`TValue` + +## Examples + +```tsx +const count = useValue(countAtom) +``` + +```tsx +const state = useValue(counterStore) +``` diff --git a/docs/framework/preact/reference/index.md b/docs/framework/preact/reference/index.md index 1ef55ff2..7534c172 100644 --- a/docs/framework/preact/reference/index.md +++ b/docs/framework/preact/reference/index.md @@ -5,7 +5,17 @@ title: "@tanstack/preact-store" # @tanstack/preact-store +## Interfaces + +- [UseSelectorOptions](interfaces/UseSelectorOptions.md) + ## Functions -- [shallow](functions/shallow.md) -- [useStore](functions/useStore.md) +- [\_useStore](functions/useStore.md) +- [createStoreContext](functions/createStoreContext.md) +- [useAtom](functions/useAtom.md) +- [useCreateAtom](functions/useCreateAtom.md) +- [useCreateStore](functions/useCreateStore.md) +- [useSelector](functions/useSelector.md) +- [~~useStore~~](functions/useStore-1.md) +- [useValue](functions/useValue.md) diff --git a/docs/framework/preact/reference/interfaces/UseSelectorOptions.md b/docs/framework/preact/reference/interfaces/UseSelectorOptions.md new file mode 100644 index 00000000..b3a6fa5e --- /dev/null +++ b/docs/framework/preact/reference/interfaces/UseSelectorOptions.md @@ -0,0 +1,38 @@ +--- +id: UseSelectorOptions +title: UseSelectorOptions +--- + +# Interface: UseSelectorOptions\ + +Defined in: [preact-store/src/useSelector.ts:9](https://github.com/TanStack/store/blob/main/packages/preact-store/src/useSelector.ts#L9) + +## Type Parameters + +### TSelected + +`TSelected` + +## Properties + +### compare()? + +```ts +optional compare: (a, b) => boolean; +``` + +Defined in: [preact-store/src/useSelector.ts:10](https://github.com/TanStack/store/blob/main/packages/preact-store/src/useSelector.ts#L10) + +#### Parameters + +##### a + +`TSelected` + +##### b + +`TSelected` + +#### Returns + +`boolean` diff --git a/docs/framework/react/reference/functions/createStoreContext.md b/docs/framework/react/reference/functions/createStoreContext.md index 80dea06b..1515155b 100644 --- a/docs/framework/react/reference/functions/createStoreContext.md +++ b/docs/framework/react/reference/functions/createStoreContext.md @@ -16,7 +16,7 @@ Creates a typed React context for sharing a bundle of atoms and stores with a su The returned `StoreProvider` only transports the provided object through React context. Consumers destructure the contextual atoms and stores, then compose them with the existing hooks like [useSelector](useSelector.md), -[useValue](useValue.md), [useSetValue](useSetValue.md), and [useAtom](useAtom.md). +[useValue](useValue.md), useSetValue, and [useAtom](useAtom.md). The object shape is preserved exactly, so keyed atoms and stores remain fully typed when read back with `useStoreContext()`. diff --git a/docs/framework/react/reference/functions/shallow.md b/docs/framework/react/reference/functions/shallow.md deleted file mode 100644 index cef99f58..00000000 --- a/docs/framework/react/reference/functions/shallow.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -id: shallow -title: shallow ---- - -# Function: shallow() - -```ts -function shallow(objA, objB): boolean; -``` - -Defined in: [packages/react-store/src/shallow.ts:1](https://github.com/TanStack/store/blob/main/packages/react-store/src/shallow.ts#L1) - -## Type Parameters - -### T - -`T` - -## Parameters - -### objA - -`T` - -### objB - -`T` - -## Returns - -`boolean` diff --git a/docs/framework/react/reference/functions/useAtom.md b/docs/framework/react/reference/functions/useAtom.md index 67ab3f40..5cd2eb68 100644 --- a/docs/framework/react/reference/functions/useAtom.md +++ b/docs/framework/react/reference/functions/useAtom.md @@ -9,7 +9,7 @@ title: useAtom function useAtom(atom, options?): [TValue, (fn) => void & (value) => void]; ``` -Defined in: [packages/react-store/src/useAtom.ts:17](https://github.com/TanStack/store/blob/main/packages/react-store/src/useAtom.ts#L17) +Defined in: [packages/react-store/src/useAtom.ts:16](https://github.com/TanStack/store/blob/main/packages/react-store/src/useAtom.ts#L16) Returns the current atom value together with a stable setter. diff --git a/docs/framework/react/reference/functions/useSetValue.md b/docs/framework/react/reference/functions/useSetValue.md deleted file mode 100644 index 680f1745..00000000 --- a/docs/framework/react/reference/functions/useSetValue.md +++ /dev/null @@ -1,106 +0,0 @@ ---- -id: useSetValue -title: useSetValue ---- - -# Function: useSetValue() - -## Call Signature - -```ts -function useSetValue(source): (fn) => void & (value) => void; -``` - -Defined in: [packages/react-store/src/useSetValue.ts:23](https://github.com/TanStack/store/blob/main/packages/react-store/src/useSetValue.ts#L23) - -Returns a stable setter for a writable atom or store. - -Writable atoms preserve their native `set` contract, supporting both direct -values and updater functions. Writable stores preserve their native -`setState` contract, supporting updater functions. - -### Type Parameters - -#### TValue - -`TValue` - -### Parameters - -#### source - -`Atom`\<`TValue`\> - -### Returns - -(`fn`) => `void` & (`value`) => `void` - -### Examples - -```tsx -const setCount = useSetValue(countAtom) -setCount((prev) => prev + 1) -``` - -```tsx -const setState = useSetValue(appStore) -setState((prev) => ({ ...prev, count: prev.count + 1 })) -``` - -## Call Signature - -```ts -function useSetValue(source): (updater) => void; -``` - -Defined in: [packages/react-store/src/useSetValue.ts:24](https://github.com/TanStack/store/blob/main/packages/react-store/src/useSetValue.ts#L24) - -Returns a stable setter for a writable atom or store. - -Writable atoms preserve their native `set` contract, supporting both direct -values and updater functions. Writable stores preserve their native -`setState` contract, supporting updater functions. - -### Type Parameters - -#### TValue - -`TValue` - -#### TActions - -`TActions` *extends* `StoreActionMap` - -### Parameters - -#### source - -`Store`\<`TValue`, `TActions`\> - -### Returns - -```ts -(updater): void; -``` - -#### Parameters - -##### updater - -(`prev`) => `TValue` - -#### Returns - -`void` - -### Examples - -```tsx -const setCount = useSetValue(countAtom) -setCount((prev) => prev + 1) -``` - -```tsx -const setState = useSetValue(appStore) -setState((prev) => ({ ...prev, count: prev.count + 1 })) -``` diff --git a/docs/framework/react/reference/functions/useStore-1.md b/docs/framework/react/reference/functions/useStore-1.md new file mode 100644 index 00000000..7fab183d --- /dev/null +++ b/docs/framework/react/reference/functions/useStore-1.md @@ -0,0 +1,61 @@ +--- +id: useStore +title: useStore +--- + +# ~~Function: useStore()~~ + +```ts +function useStore( + source, + selector, + compare?): TSelected; +``` + +Defined in: [packages/react-store/src/useStore.ts:13](https://github.com/TanStack/store/blob/main/packages/react-store/src/useStore.ts#L13) + +Deprecated alias for [useSelector](useSelector.md). + +## Type Parameters + +### TSource + +`TSource` + +### TSelected + +`TSelected` + +## Parameters + +### source + +#### get + +() => `TSource` + +#### subscribe + +(`listener`) => `object` + +### selector + +(`snapshot`) => `TSelected` + +### compare? + +(`a`, `b`) => `boolean` + +## Returns + +`TSelected` + +## Example + +```tsx +const count = useStore(counterStore, (state) => state.count) +``` + +## Deprecated + +Use `useSelector` instead. diff --git a/docs/framework/react/reference/functions/useStore.md b/docs/framework/react/reference/functions/useStore.md index 6eaf58bb..bbc43c6b 100644 --- a/docs/framework/react/reference/functions/useStore.md +++ b/docs/framework/react/reference/functions/useStore.md @@ -1,55 +1,64 @@ --- -id: useStore -title: useStore +id: _useStore +title: _useStore --- -# ~~Function: useStore()~~ +# Function: \_useStore() ```ts -function useStore( - source, +function _useStore( + store, selector, - compare?): TSelected; + options?): [TSelected, [TActions] extends [never] ? (updater) => void : TActions]; ``` -Defined in: [packages/react-store/src/useStore.ts:8](https://github.com/TanStack/store/blob/main/packages/react-store/src/useStore.ts#L8) +Defined in: [packages/react-store/src/\_useStore.ts:24](https://github.com/TanStack/store/blob/main/packages/react-store/src/_useStore.ts#L24) -Deprecated alias for [useSelector](useSelector.md). +Experimental combined read+write hook for stores, mirroring useAtom's tuple +pattern. -## Type Parameters +Returns `[selected, actions]` when the store has an actions factory, or +`[selected, setState]` for plain stores. -### TSource +## Type Parameters -`TSource` +### TState -### TSelected +`TState` -`TSelected` +### TActions -## Parameters +`TActions` *extends* `StoreActionMap` -### source +### TSelected -#### get +`TSelected` = `NoInfer`\<`TState`\> -() => `TSource` +## Parameters -#### subscribe +### store -(`listener`) => `object` +`Store`\<`TState`, `TActions`\> ### selector -(`snapshot`) => `TSelected` +(`state`) => `TSelected` -### compare? +### options? -(`a`, `b`) => `boolean` +[`UseSelectorOptions`](../interfaces/UseSelectorOptions.md)\<`TSelected`\> ## Returns -`TSelected` +\[`TSelected`, \[`TActions`\] *extends* \[`never`\] ? (`updater`) => `void` : `TActions`\] -## Deprecated +## Example -Use `useSelector` instead. +```tsx +// Store with actions +const [cats, { addCat }] = _useStore(petStore, (s) => s.cats) + +// Store without actions +const [count, setState] = _useStore(plainStore, (s) => s) +setState((prev) => prev + 1) +``` diff --git a/docs/framework/react/reference/functions/useStoreActions.md b/docs/framework/react/reference/functions/useStoreActions.md deleted file mode 100644 index 1af312ee..00000000 --- a/docs/framework/react/reference/functions/useStoreActions.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -id: useStoreActions -title: useStoreActions ---- - -# Function: useStoreActions() - -```ts -function useStoreActions(store): TActions; -``` - -Defined in: [packages/react-store/src/useStoreActions.ts:16](https://github.com/TanStack/store/blob/main/packages/react-store/src/useStoreActions.ts#L16) - -Returns the stable actions bag from a writable store created with actions. - -Use this when a component only needs to call store actions and should not -subscribe to state changes. - -## Type Parameters - -### TValue - -`TValue` - -### TActions - -`TActions` *extends* `StoreActionMap` - -## Parameters - -### store - -`Store`\<`TValue`, `TActions`\> - -## Returns - -`TActions` - -## Example - -```tsx -const actions = useStoreActions(counterStore) -actions.inc() -``` diff --git a/docs/framework/react/reference/index.md b/docs/framework/react/reference/index.md index c02db638..0505d839 100644 --- a/docs/framework/react/reference/index.md +++ b/docs/framework/react/reference/index.md @@ -11,13 +11,11 @@ title: "@tanstack/react-store" ## Functions +- [\_useStore](functions/useStore.md) - [createStoreContext](functions/createStoreContext.md) -- [shallow](functions/shallow.md) - [useAtom](functions/useAtom.md) - [useCreateAtom](functions/useCreateAtom.md) - [useCreateStore](functions/useCreateStore.md) - [useSelector](functions/useSelector.md) -- [useSetValue](functions/useSetValue.md) -- [~~useStore~~](functions/useStore.md) -- [useStoreActions](functions/useStoreActions.md) +- [~~useStore~~](functions/useStore-1.md) - [useValue](functions/useValue.md) diff --git a/docs/framework/solid/quick-start.md b/docs/framework/solid/quick-start.md index 67819d76..19f63861 100644 --- a/docs/framework/solid/quick-start.md +++ b/docs/framework/solid/quick-start.md @@ -6,7 +6,7 @@ id: quick-start The basic Solid app example to get started with the TanStack Solid-store. ```jsx -import { createStore, useStore } from '@tanstack/solid-store'; +import { createStore, useSelector } from '@tanstack/solid-store'; // You can instantiate the store outside of Solid components too! export const store = createStore({ @@ -15,7 +15,7 @@ export const store = createStore({ }) export const Display = (props) => { -  const count = useStore(store, (state) => state[props.animals]); +  const count = useSelector(store, (state) => state[props.animals]);   return (           {props.animals}: {count()} @@ -58,3 +58,5 @@ const App = () => { export default App; ``` + +`useStore` remains available as a deprecated alias to `useSelector`. diff --git a/docs/framework/solid/reference/functions/shallow.md b/docs/framework/solid/reference/functions/shallow.md deleted file mode 100644 index d8809f33..00000000 --- a/docs/framework/solid/reference/functions/shallow.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -id: shallow -title: shallow ---- - -# Function: shallow() - -```ts -function shallow(objA, objB): boolean; -``` - -Defined in: [index.tsx:35](https://github.com/TanStack/store/blob/main/packages/solid-store/src/index.tsx#L35) - -## Type Parameters - -### T - -`T` - -## Parameters - -### objA - -`T` - -### objB - -`T` - -## Returns - -`boolean` diff --git a/docs/framework/solid/reference/functions/useAtom.md b/docs/framework/solid/reference/functions/useAtom.md new file mode 100644 index 00000000..a05c4337 --- /dev/null +++ b/docs/framework/solid/reference/functions/useAtom.md @@ -0,0 +1,49 @@ +--- +id: useAtom +title: useAtom +--- + +# Function: useAtom() + +```ts +function useAtom(atom, options?): [Accessor, (fn) => void & (value) => void]; +``` + +Defined in: [solid-store/src/useAtom.ts:23](https://github.com/TanStack/store/blob/main/packages/solid-store/src/useAtom.ts#L23) + +Returns the current atom accessor together with a setter. + +Use this when a component needs to both read and update the same writable +atom. + +## Type Parameters + +### TValue + +`TValue` + +## Parameters + +### atom + +`Atom`\<`TValue`\> + +### options? + +[`UseSelectorOptions`](../interfaces/UseSelectorOptions.md)\<`TValue`\> + +## Returns + +\[`Accessor`\<`TValue`\>, (`fn`) => `void` & (`value`) => `void`\] + +## Example + +```tsx +const [count, setCount] = useAtom(countAtom) + +return ( + +) +``` diff --git a/docs/framework/solid/reference/functions/useSelector.md b/docs/framework/solid/reference/functions/useSelector.md new file mode 100644 index 00000000..f2a109fd --- /dev/null +++ b/docs/framework/solid/reference/functions/useSelector.md @@ -0,0 +1,61 @@ +--- +id: useSelector +title: useSelector +--- + +# Function: useSelector() + +```ts +function useSelector( + source, + selector, +options?): Accessor; +``` + +Defined in: [solid-store/src/useSelector.ts:38](https://github.com/TanStack/store/blob/main/packages/solid-store/src/useSelector.ts#L38) + +Selects a slice of state from an atom or store and subscribes the component +to that selection. + +This is the primary Solid read hook for TanStack Store. It returns a Solid +accessor so consumers can read the selected value reactively. + +## Type Parameters + +### TSource + +`TSource` + +### TSelected + +`TSelected` + +## Parameters + +### source + +`SelectionSource`\<`TSource`\> + +### selector + +(`snapshot`) => `TSelected` + +### options? + +[`UseSelectorOptions`](../interfaces/UseSelectorOptions.md)\<`TSelected`\> + +## Returns + +`Accessor`\<`TSelected`\> + +## Examples + +```tsx +const count = useSelector(counterStore, (state) => state.count) + +return

{count()}

+``` + +```tsx +const doubled = useSelector(countAtom, (value) => value * 2) +``` diff --git a/docs/framework/solid/reference/functions/useStore-1.md b/docs/framework/solid/reference/functions/useStore-1.md new file mode 100644 index 00000000..6e73b2fc --- /dev/null +++ b/docs/framework/solid/reference/functions/useStore-1.md @@ -0,0 +1,61 @@ +--- +id: useStore +title: useStore +--- + +# ~~Function: useStore()~~ + +```ts +function useStore( + source, + selector, +compare?): Accessor; +``` + +Defined in: [solid-store/src/useStore.ts:14](https://github.com/TanStack/store/blob/main/packages/solid-store/src/useStore.ts#L14) + +Deprecated alias for [useSelector](useSelector.md). + +## Type Parameters + +### TSource + +`TSource` + +### TSelected + +`TSelected` + +## Parameters + +### source + +#### get + +() => `TSource` + +#### subscribe + +(`listener`) => `object` + +### selector + +(`snapshot`) => `TSelected` + +### compare? + +(`a`, `b`) => `boolean` + +## Returns + +`Accessor`\<`TSelected`\> + +## Example + +```tsx +const count = useStore(counterStore, (state) => state.count) +``` + +## Deprecated + +Use `useSelector` instead. diff --git a/docs/framework/solid/reference/functions/useStore.md b/docs/framework/solid/reference/functions/useStore.md index 6952ba19..046e7a04 100644 --- a/docs/framework/solid/reference/functions/useStore.md +++ b/docs/framework/solid/reference/functions/useStore.md @@ -1,18 +1,24 @@ --- -id: useStore -title: useStore +id: _useStore +title: _useStore --- -# Function: useStore() +# Function: \_useStore() ```ts -function useStore( +function _useStore( store, selector, -options): Accessor; + options?): [Accessor, [TActions] extends [never] ? (updater) => void : TActions]; ``` -Defined in: [index.tsx:12](https://github.com/TanStack/store/blob/main/packages/solid-store/src/index.tsx#L12) +Defined in: [solid-store/src/\_useStore.ts:23](https://github.com/TanStack/store/blob/main/packages/solid-store/src/_useStore.ts#L23) + +Experimental combined read+write hook for stores, mirroring useAtom's tuple +pattern. + +Returns `[selected, actions]` when the store has an actions factory, or +`[selected, setState]` for plain stores. ## Type Parameters @@ -20,6 +26,10 @@ Defined in: [index.tsx:12](https://github.com/TanStack/store/blob/main/packages/ `TState` +### TActions + +`TActions` *extends* `StoreActionMap` + ### TSelected `TSelected` = `NoInfer`\<`TState`\> @@ -28,16 +38,27 @@ Defined in: [index.tsx:12](https://github.com/TanStack/store/blob/main/packages/ ### store -`Atom`\<`TState`\> | `ReadonlyAtom`\<`TState`\> +`Store`\<`TState`, `TActions`\> ### selector (`state`) => `TSelected` -### options +### options? -`UseStoreOptions`\<`TSelected`\> = `{}` +[`UseSelectorOptions`](../interfaces/UseSelectorOptions.md)\<`TSelected`\> ## Returns -`Accessor`\<`TSelected`\> +\[`Accessor`\<`TSelected`\>, \[`TActions`\] *extends* \[`never`\] ? (`updater`) => `void` : `TActions`\] + +## Example + +```tsx +// Store with actions +const [cats, { addCat }] = _useStore(petStore, (s) => s.cats) + +// Store without actions +const [count, setState] = _useStore(plainStore, (s) => s) +setState((prev) => prev + 1) +``` diff --git a/docs/framework/solid/reference/functions/useValue.md b/docs/framework/solid/reference/functions/useValue.md new file mode 100644 index 00000000..770bda4d --- /dev/null +++ b/docs/framework/solid/reference/functions/useValue.md @@ -0,0 +1,49 @@ +--- +id: useValue +title: useValue +--- + +# Function: useValue() + +```ts +function useValue(source, options?): Accessor; +``` + +Defined in: [solid-store/src/useValue.ts:24](https://github.com/TanStack/store/blob/main/packages/solid-store/src/useValue.ts#L24) + +Subscribes to an atom or store and returns its current value accessor. + +This is the whole-value counterpart to [useSelector](useSelector.md). Use it when the +component needs the entire current value from a source. + +## Type Parameters + +### TValue + +`TValue` + +## Parameters + +### source + +`Atom`\<`TValue`\> | `ReadonlyAtom`\<`TValue`\> | `Store`\<`TValue`, `any`\> | `ReadonlyStore`\<`TValue`\> + +### options? + +[`UseSelectorOptions`](../interfaces/UseSelectorOptions.md)\<`TValue`\> + +## Returns + +`Accessor`\<`TValue`\> + +## Examples + +```tsx +const count = useValue(countAtom) + +return

{count()}

+``` + +```tsx +const state = useValue(counterStore) +``` diff --git a/docs/framework/solid/reference/index.md b/docs/framework/solid/reference/index.md index 71951743..5e9608c9 100644 --- a/docs/framework/solid/reference/index.md +++ b/docs/framework/solid/reference/index.md @@ -5,7 +5,14 @@ title: "@tanstack/solid-store" # @tanstack/solid-store +## Interfaces + +- [UseSelectorOptions](interfaces/UseSelectorOptions.md) + ## Functions -- [shallow](functions/shallow.md) -- [useStore](functions/useStore.md) +- [\_useStore](functions/useStore.md) +- [useAtom](functions/useAtom.md) +- [useSelector](functions/useSelector.md) +- [~~useStore~~](functions/useStore-1.md) +- [useValue](functions/useValue.md) diff --git a/docs/framework/solid/reference/interfaces/UseSelectorOptions.md b/docs/framework/solid/reference/interfaces/UseSelectorOptions.md new file mode 100644 index 00000000..2d3845b8 --- /dev/null +++ b/docs/framework/solid/reference/interfaces/UseSelectorOptions.md @@ -0,0 +1,38 @@ +--- +id: UseSelectorOptions +title: UseSelectorOptions +--- + +# Interface: UseSelectorOptions\ + +Defined in: [solid-store/src/useSelector.ts:4](https://github.com/TanStack/store/blob/main/packages/solid-store/src/useSelector.ts#L4) + +## Type Parameters + +### TSelected + +`TSelected` + +## Properties + +### compare()? + +```ts +optional compare: (a, b) => boolean; +``` + +Defined in: [solid-store/src/useSelector.ts:5](https://github.com/TanStack/store/blob/main/packages/solid-store/src/useSelector.ts#L5) + +#### Parameters + +##### a + +`TSelected` + +##### b + +`TSelected` + +#### Returns + +`boolean` diff --git a/docs/framework/svelte/quick-start.md b/docs/framework/svelte/quick-start.md index 3c126fb2..5fe7d502 100644 --- a/docs/framework/svelte/quick-start.md +++ b/docs/framework/svelte/quick-start.md @@ -45,17 +45,19 @@ export function updateState(animal: 'cats' | 'dogs') { **Display.svelte** ```html
{ animal }: { count.current }
``` +`useStore` remains available as a deprecated alias to `useSelector`. + **Increment.svelte** ```html @@ -58,6 +58,8 @@ const count = useStore(store, (state) => state[props.animal]); ``` +`useStore` remains available as a deprecated alias to `useSelector`. + **Increment.vue** ```vue + + diff --git a/examples/preact/atoms/package.json b/examples/preact/atoms/package.json new file mode 100644 index 00000000..16e05d54 --- /dev/null +++ b/examples/preact/atoms/package.json @@ -0,0 +1,26 @@ +{ + "name": "@tanstack/store-example-preact-atoms", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port=3050", + "build": "vite build", + "preview": "vite preview", + "test:types": "tsc" + }, + "dependencies": { + "@tanstack/preact-store": "^0.12.0", + "preact": "^10.29.1" + }, + "devDependencies": { + "@preact/preset-vite": "^2.10.5", + "@types/node": "^25.6.0", + "eslint": "^10.2.0", + "eslint-config-preact": "^2.0.0", + "typescript": "6.0.2", + "vite": "^8.0.8" + }, + "eslintConfig": { + "extends": "preact" + } +} diff --git a/examples/preact/atoms/src/index.tsx b/examples/preact/atoms/src/index.tsx new file mode 100644 index 00000000..bd29194d --- /dev/null +++ b/examples/preact/atoms/src/index.tsx @@ -0,0 +1,62 @@ +import { render } from 'preact' +import { + createAtom, + useAtom, + // useCreateAtom, + useValue, +} from '@tanstack/preact-store' + +// Optionally, you can create atoms outside of Preact components at module scope +const countAtom = createAtom(0) + +function App() { + // or define atoms inside of components with hook variant. You would have to pass atom as props or use store context though. + // const countAtom = useCreateAtom(0) + + return ( +
+

Preact Atom Hooks

+

+ This example creates a module-level atom and reads and updates it with + the Preact hooks. +

+ + + +
+ ) +} + +function AtomValuePanel() { + const count = useValue(countAtom) // useValue re-renders when the value changes. Useful for read-only access to an atom. + + return

Total: {count}

+} + +function AtomButtons() { + return ( +
+ + +
+ ) +} + +function AtomStepper() { + const [count, setCount] = useAtom(countAtom) // read and write access to the atom. Re-renders when the value changes. + + return ( +
+

Editable count: {count}

+ +
+ ) +} + +render(, document.getElementById('app')!) diff --git a/examples/preact/atoms/tsconfig.json b/examples/preact/atoms/tsconfig.json new file mode 100644 index 00000000..e71f296c --- /dev/null +++ b/examples/preact/atoms/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "noEmit": true, + "allowJs": true, + "checkJs": true, + "strict": true, + "jsx": "react-jsx", + "jsxImportSource": "preact", + "skipLibCheck": true + }, + "include": ["node_modules/vite/client.d.ts", "src", "vite.config.ts"], + "exclude": ["dist"] +} diff --git a/examples/preact/atoms/vite.config.ts b/examples/preact/atoms/vite.config.ts new file mode 100644 index 00000000..b9d4ccf9 --- /dev/null +++ b/examples/preact/atoms/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import preact from '@preact/preset-vite' + +export default defineConfig({ + plugins: [preact()], + optimizeDeps: { + exclude: ['@tanstack/preact-store'], + }, +}) diff --git a/examples/preact/simple/README.md b/examples/preact/simple/README.md index a9d90bf0..a29e7193 100644 --- a/examples/preact/simple/README.md +++ b/examples/preact/simple/README.md @@ -1,15 +1,6 @@ -# `create-preact` +# Preact Simple Example -

- -

+To run this example: -

Get started using Preact and Vite!

- -## Getting Started - -- `pnpm dev` - Starts a dev server at http://localhost:5173/ - -- `pnpm build` - Builds for production, emitting to `dist/` - -- `pnpm preview` - Starts a server at http://localhost:4173/ to test production build locally +- `npm install` +- `npm run dev` diff --git a/examples/preact/simple/src/index.tsx b/examples/preact/simple/src/index.tsx index ffc07839..09609b6d 100644 --- a/examples/preact/simple/src/index.tsx +++ b/examples/preact/simple/src/index.tsx @@ -1,21 +1,53 @@ import { render } from 'preact' -import { Store, useStore } from '@tanstack/preact-store' +import { Store, useSelector } from '@tanstack/preact-store' +// You can instantiate a Store outside of Preact components too! export const store = new Store({ - count: 0, + dogs: 0, + cats: 0, }) -function Counter() { - const count = useStore(store, (state) => state.count) +interface DisplayProps { + animal: 'dogs' | 'cats' +} + +// This will only re-render when `state[animal]` changes. If an unrelated store property changes, it won't re-render +function Display({ animal }: DisplayProps) { + const count = useSelector(store, (state) => state[animal]) // formerly, useStore. Now renamed to useSelector. + return
{`${animal}: ${count}`}
+} + +const updateState = (animal: 'dogs' | 'cats') => { + store.setState((state) => { + return { + ...state, + [animal]: state[animal] + 1, + } + }) +} + +interface IncrementProps { + animal: 'dogs' | 'cats' +} + +const Increment = ({ animal }: IncrementProps) => ( + +) + +function App() { return (
-
Count: {count}
- +

How many of your friends like cats or dogs?

+

+ Press one of the buttons to add a counter of how many of your friends + like cats or dogs +

+ + + +
) } -const root = document.body -render(, root) +render(, document.getElementById('app')!) diff --git a/examples/preact/simple/tsconfig.json b/examples/preact/simple/tsconfig.json index b8f664db..bd8198e7 100644 --- a/examples/preact/simple/tsconfig.json +++ b/examples/preact/simple/tsconfig.json @@ -13,5 +13,6 @@ "jsxImportSource": "preact", "skipLibCheck": true }, - "include": ["node_modules/vite/client.d.ts", "**/*"] + "include": ["node_modules/vite/client.d.ts", "src", "vite.config.ts"], + "exclude": ["dist"] } diff --git a/examples/preact/store-actions/README.md b/examples/preact/store-actions/README.md new file mode 100644 index 00000000..d019ddb4 --- /dev/null +++ b/examples/preact/store-actions/README.md @@ -0,0 +1,12 @@ +# Preact Store Actions Example + +This example demonstrates: + +- `useSelector` +- `_useStore` +- module-level `Store` actions + +To run this example: + +- `npm install` +- `npm run dev` diff --git a/examples/preact/store-actions/index.html b/examples/preact/store-actions/index.html new file mode 100644 index 00000000..b9bd38f8 --- /dev/null +++ b/examples/preact/store-actions/index.html @@ -0,0 +1,12 @@ + + + + + + TanStack Store Preact Store Actions Example App + + +
+ + + diff --git a/examples/preact/store-actions/package.json b/examples/preact/store-actions/package.json new file mode 100644 index 00000000..9c6b3bd1 --- /dev/null +++ b/examples/preact/store-actions/package.json @@ -0,0 +1,26 @@ +{ + "name": "@tanstack/store-example-preact-store-actions", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port=3050", + "build": "vite build", + "preview": "vite preview", + "test:types": "tsc" + }, + "dependencies": { + "@tanstack/preact-store": "^0.12.0", + "preact": "^10.29.1" + }, + "devDependencies": { + "@preact/preset-vite": "^2.10.5", + "@types/node": "^25.6.0", + "eslint": "^10.2.0", + "eslint-config-preact": "^2.0.0", + "typescript": "6.0.2", + "vite": "^8.0.8" + }, + "eslintConfig": { + "extends": "preact" + } +} diff --git a/examples/preact/store-actions/src/index.tsx b/examples/preact/store-actions/src/index.tsx new file mode 100644 index 00000000..96924921 --- /dev/null +++ b/examples/preact/store-actions/src/index.tsx @@ -0,0 +1,84 @@ +import { render } from 'preact' +import { Store, _useStore, useSelector } from '@tanstack/preact-store' + +// Optionally, you can create stores outside of Preact components at module scope +const petStore = new Store( + { + cats: 0, + dogs: 0, + }, + ({ setState, get }) => + // optionally, define actions for updating your store in specific ways right on the store. + ({ + addCat: () => + setState((prev) => ({ + ...prev, + cats: prev.cats + 1, + })), + addDog: () => + setState((prev) => ({ + ...prev, + dogs: prev.dogs + 1, + })), + log: () => console.log(get()), + }), +) + +function App() { + // or define stores inside of components with hook variant. You would have to pass store as props or use store context though. + // const petStore = useCreateStore(...) + + return ( +
+ +

Preact Store Actions

+

+ This example creates a module-level store with actions. Components read + state with useSelector and call mutations through{' '} + store.actions or the experimental _useStore{' '} + hook. +

+ + + +
+ ) +} + +function CatVoter() { + // _useStore gives both the selected state and actions in a single tuple + const [cats, { addCat }] = _useStore(petStore, (state) => state.cats) + + return ( +
+

Cats: {cats}

+ +
+ ) +} + +function DogVoter() { + // _useStore gives both the selected state and actions in a single tuple + const [dogs, { addDog }] = _useStore(petStore, (state) => state.dogs) + + return ( +
+

Dogs: {dogs}

+ +
+ ) +} + +function TotalCard() { + const total = useSelector(petStore, (state) => state.cats + state.dogs) + + return

Total votes: {total}

+} + +render(, document.getElementById('app')!) diff --git a/examples/preact/store-actions/tsconfig.json b/examples/preact/store-actions/tsconfig.json new file mode 100644 index 00000000..e71f296c --- /dev/null +++ b/examples/preact/store-actions/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "noEmit": true, + "allowJs": true, + "checkJs": true, + "strict": true, + "jsx": "react-jsx", + "jsxImportSource": "preact", + "skipLibCheck": true + }, + "include": ["node_modules/vite/client.d.ts", "src", "vite.config.ts"], + "exclude": ["dist"] +} diff --git a/examples/preact/store-actions/vite.config.ts b/examples/preact/store-actions/vite.config.ts new file mode 100644 index 00000000..b9d4ccf9 --- /dev/null +++ b/examples/preact/store-actions/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import preact from '@preact/preset-vite' + +export default defineConfig({ + plugins: [preact()], + optimizeDeps: { + exclude: ['@tanstack/preact-store'], + }, +}) diff --git a/examples/preact/store-context/README.md b/examples/preact/store-context/README.md new file mode 100644 index 00000000..c90e4891 --- /dev/null +++ b/examples/preact/store-context/README.md @@ -0,0 +1,17 @@ +# Preact Store Context Example + +This example demonstrates: + +- `createStoreContext` +- `useCreateStore` +- `useCreateAtom` +- `useStoreContext` +- `useSelector` +- `useValue` +- `useSetValue` +- `useAtom` + +To run this example: + +- `npm install` +- `npm run dev` diff --git a/examples/preact/store-context/index.html b/examples/preact/store-context/index.html new file mode 100644 index 00000000..099280fe --- /dev/null +++ b/examples/preact/store-context/index.html @@ -0,0 +1,12 @@ + + + + + + TanStack Store Preact Store Context Example App + + +
+ + + diff --git a/examples/preact/store-context/package.json b/examples/preact/store-context/package.json new file mode 100644 index 00000000..aefc8aa3 --- /dev/null +++ b/examples/preact/store-context/package.json @@ -0,0 +1,26 @@ +{ + "name": "@tanstack/store-example-preact-store-context", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port=3050", + "build": "vite build", + "preview": "vite preview", + "test:types": "tsc" + }, + "dependencies": { + "@tanstack/preact-store": "^0.12.0", + "preact": "^10.29.1" + }, + "devDependencies": { + "@preact/preset-vite": "^2.10.5", + "@types/node": "^25.6.0", + "eslint": "^10.2.0", + "eslint-config-preact": "^2.0.0", + "typescript": "6.0.2", + "vite": "^8.0.8" + }, + "eslintConfig": { + "extends": "preact" + } +} diff --git a/examples/preact/store-context/src/index.tsx b/examples/preact/store-context/src/index.tsx new file mode 100644 index 00000000..44f994c5 --- /dev/null +++ b/examples/preact/store-context/src/index.tsx @@ -0,0 +1,157 @@ +import { render } from 'preact' +import { + useAtom, + useCreateAtom, + createStoreContext, + useCreateStore, + useSelector, + useValue, +} from '@tanstack/preact-store' +import type { Atom, Store } from '@tanstack/preact-store' + +// one drawback of storing stores and atoms in context is you have to define types for the context manually, instead of everything being inferred. + +type CounterStore = { + cats: number + dogs: number +} + +type StoreContextValue = { + votesStore: Store + countAtom: Atom +} + +// create context provider and hook +const { StoreProvider, useStoreContext } = + createStoreContext() + +// top-level app component with provider +function App() { + // create the store + const votesStore = useCreateStore({ + cats: 0, + dogs: 0, + }) + // create the atom + const countAtom = useCreateAtom(0) + + return ( + // provide both the store and atom in a single context object + +
+

Preact Store Context

+

+ This example provides both atoms and stores through a single typed + context object, then consumes them from nested components. +

+ + + + +
+

Nested Atom Components

+ + + +
+
+
+ ) +} + +function CatCard() { + // pull a store from context + const { votesStore } = useStoreContext() + // select a value from the store with useSelector + const value = useSelector(votesStore, (state) => state.cats) + + return

Cats: {value}

+} + +function DogCard() { + // pull a store from context + const { votesStore } = useStoreContext() + // select a value from the store with useSelector + const value = useSelector(votesStore, (state) => state.dogs) + + return

Dogs: {value}

+} + +function TotalCard() { + // pull a store from context + const { votesStore } = useStoreContext() + // custom selector to calculate total votes from the store state + const total = useSelector(votesStore, (state) => state.cats + state.dogs) + + return

Total votes: {total}

+} + +function AtomSummary() { + // pull an atom from context + const { countAtom } = useStoreContext() + const count = useValue(countAtom) + + return

Atom count: {count}

+} + +function NestedAtomControls() { + const { countAtom } = useStoreContext() + + return ( +
+ + +
+ ) +} + +function DeepAtomEditor() { + const { countAtom } = useStoreContext() + const [count, setCount] = useAtom(countAtom) + + return ( +
+

Editable atom count: {count}

+ +
+ ) +} + +function StoreButtons() { + const { votesStore } = useStoreContext() + + return ( +
+ + +
+ ) +} + +render(, document.getElementById('app')!) diff --git a/examples/preact/store-context/tsconfig.json b/examples/preact/store-context/tsconfig.json new file mode 100644 index 00000000..e71f296c --- /dev/null +++ b/examples/preact/store-context/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "noEmit": true, + "allowJs": true, + "checkJs": true, + "strict": true, + "jsx": "react-jsx", + "jsxImportSource": "preact", + "skipLibCheck": true + }, + "include": ["node_modules/vite/client.d.ts", "src", "vite.config.ts"], + "exclude": ["dist"] +} diff --git a/examples/preact/store-context/vite.config.ts b/examples/preact/store-context/vite.config.ts new file mode 100644 index 00000000..b9d4ccf9 --- /dev/null +++ b/examples/preact/store-context/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import preact from '@preact/preset-vite' + +export default defineConfig({ + plugins: [preact()], + optimizeDeps: { + exclude: ['@tanstack/preact-store'], + }, +}) diff --git a/examples/preact/stores/README.md b/examples/preact/stores/README.md new file mode 100644 index 00000000..c4e36d67 --- /dev/null +++ b/examples/preact/stores/README.md @@ -0,0 +1,12 @@ +# Preact Store Hooks Example + +This example demonstrates: + +- `useSelector` +- `store.setState` +- module-level `Store` + +To run this example: + +- `npm install` +- `npm run dev` diff --git a/examples/preact/stores/index.html b/examples/preact/stores/index.html new file mode 100644 index 00000000..0a729bff --- /dev/null +++ b/examples/preact/stores/index.html @@ -0,0 +1,12 @@ + + + + + + TanStack Store Preact Stores Example App + + +
+ + + diff --git a/examples/preact/stores/package.json b/examples/preact/stores/package.json new file mode 100644 index 00000000..efedc217 --- /dev/null +++ b/examples/preact/stores/package.json @@ -0,0 +1,26 @@ +{ + "name": "@tanstack/store-example-preact-stores", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port=3050", + "build": "vite build", + "preview": "vite preview", + "test:types": "tsc" + }, + "dependencies": { + "@tanstack/preact-store": "^0.12.0", + "preact": "^10.29.1" + }, + "devDependencies": { + "@preact/preset-vite": "^2.10.5", + "@types/node": "^25.6.0", + "eslint": "^10.2.0", + "eslint-config-preact": "^2.0.0", + "typescript": "6.0.2", + "vite": "^8.0.8" + }, + "eslintConfig": { + "extends": "preact" + } +} diff --git a/examples/preact/stores/src/index.tsx b/examples/preact/stores/src/index.tsx new file mode 100644 index 00000000..f39190ca --- /dev/null +++ b/examples/preact/stores/src/index.tsx @@ -0,0 +1,79 @@ +import { render } from 'preact' +import { Store, useSelector } from '@tanstack/preact-store' + +// Optionally, you can create stores outside of Preact components at module scope +const petStore = new Store({ + cats: 0, + dogs: 0, +}) + +function App() { + // or define stores inside of components with hook variant. You would have to pass store as props or use store context though. + // const petStore = useCreateStore(...) + + return ( +
+

Preact Store Hooks

+

+ This example creates a module-level store. Components read state with + `useSelector` and update it directly with `store.setState`. +

+ + + + +
+ ) +} + +function CatsCard() { + // read state slice (only re-renders when the selected value changes) + const value = useSelector(petStore, (state) => state.cats) + + return

Cats: {value}

+} + +function DogsCard() { + // read state slice (only re-renders when the selected value changes) + const value = useSelector(petStore, (state) => state.dogs) + + return

Dogs: {value}

+} + +function StoreButtons() { + return ( +
+ + +
+ ) +} + +function TotalCard() { + const total = useSelector(petStore, (state) => state.cats + state.dogs) + + return

Total votes: {total}

+} + +render(, document.getElementById('app')!) diff --git a/examples/preact/stores/tsconfig.json b/examples/preact/stores/tsconfig.json new file mode 100644 index 00000000..e71f296c --- /dev/null +++ b/examples/preact/stores/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "noEmit": true, + "allowJs": true, + "checkJs": true, + "strict": true, + "jsx": "react-jsx", + "jsxImportSource": "preact", + "skipLibCheck": true + }, + "include": ["node_modules/vite/client.d.ts", "src", "vite.config.ts"], + "exclude": ["dist"] +} diff --git a/examples/preact/stores/vite.config.ts b/examples/preact/stores/vite.config.ts new file mode 100644 index 00000000..b9d4ccf9 --- /dev/null +++ b/examples/preact/stores/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import preact from '@preact/preset-vite' + +export default defineConfig({ + plugins: [preact()], + optimizeDeps: { + exclude: ['@tanstack/preact-store'], + }, +}) diff --git a/examples/react/atoms/src/index.tsx b/examples/react/atoms/src/index.tsx index 88da358b..e1c83deb 100644 --- a/examples/react/atoms/src/index.tsx +++ b/examples/react/atoms/src/index.tsx @@ -3,7 +3,6 @@ import { createAtom, useAtom, // useCreateAtom, - useSetValue, useValue, } from '@tanstack/react-store' @@ -35,15 +34,13 @@ function AtomValuePanel() { } function AtomButtons() { - const setCount = useSetValue(countAtom) // useSetValue never causes a re-render (useAtom does) if you need write-only in a component - return (
- -
) diff --git a/examples/react/store-actions/README.md b/examples/react/store-actions/README.md index 957f75ee..b36726b9 100644 --- a/examples/react/store-actions/README.md +++ b/examples/react/store-actions/README.md @@ -3,7 +3,7 @@ This example demonstrates: - `useSelector` -- `useStoreActions` +- `_useStore` - module-level `Store` actions To run this example: diff --git a/examples/react/store-actions/src/index.tsx b/examples/react/store-actions/src/index.tsx index 6d7d5161..10717d1f 100644 --- a/examples/react/store-actions/src/index.tsx +++ b/examples/react/store-actions/src/index.tsx @@ -1,5 +1,5 @@ import ReactDOM from 'react-dom/client' -import { Store, useSelector, useStoreActions } from '@tanstack/react-store' +import { Store, _useStore, useSelector } from '@tanstack/react-store' // Optionally, you can create stores outside of React components at module scope const petStore = new Store( @@ -7,19 +7,20 @@ const petStore = new Store( cats: 0, dogs: 0, }, - ({ set }) => + ({ setState, get }) => // optionally, define actions for updating your store in specific ways right on the store. ({ addCat: () => - set((prev) => ({ + setState((prev) => ({ ...prev, cats: prev.cats + 1, })), addDog: () => - set((prev) => ({ + setState((prev) => ({ ...prev, dogs: prev.dogs + 1, })), + log: () => console.log(get()), }), ) @@ -29,44 +30,44 @@ function App() { return (
+

React Store Actions

This example creates a module-level store with actions. Components read - state with `useSelector` and call mutations through `useStoreActions`. + state with useSelector and call mutations through{' '} + store.actions or the experimental _useStore{' '} + hook.

- - + + -
) } -function CatsCard() { - // read state slice (only re-renders when the selected value changes) - const value = useSelector(petStore, (state) => state.cats) +function CatVoter() { + // _useStore gives both the selected state and actions in a single tuple + const [cats, { addCat }] = _useStore(petStore, (state) => state.cats) - return

Cats: {value}

-} - -function DogsCard() { - // read state slice (only re-renders when the selected value changes) - const value = useSelector(petStore, (state) => state.dogs) - - return

Dogs: {value}

+ return ( +
+

Cats: {cats}

+ +
+ ) } -function StoreButtons() { - // pull stable action functions from the - const { addCat, addDog } = useStoreActions(petStore) +function DogVoter() { + // _useStore gives both the selected state and actions in a single tuple + const [dogs, { addDog }] = _useStore(petStore, (state) => state.dogs) return (
- +

Dogs: {dogs}

) diff --git a/examples/react/store-context/src/index.tsx b/examples/react/store-context/src/index.tsx index 63e8f67c..2c2add5c 100644 --- a/examples/react/store-context/src/index.tsx +++ b/examples/react/store-context/src/index.tsx @@ -5,7 +5,6 @@ import { createStoreContext, useCreateStore, useSelector, - useSetValue, useValue, } from '@tanstack/react-store' import type { Atom, Store } from '@tanstack/react-store' @@ -97,14 +96,13 @@ function AtomSummary() { function NestedAtomControls() { const { countAtom } = useStoreContext() - const setCount = useSetValue(countAtom) return (
- -
@@ -127,14 +125,13 @@ function DeepAtomEditor() { function StoreButtons() { const { votesStore } = useStoreContext() - const setVotes = useSetValue(votesStore) return (
+ +
+ ) +} + +function AtomStepper() { + const [count, setCount] = useAtom(countAtom) // read and write access to the atom. + + return ( +
+

Editable count: {count()}

+ +
+ ) +} + +render(() => , document.getElementById('root')!) diff --git a/examples/solid/atoms/tsconfig.json b/examples/solid/atoms/tsconfig.json new file mode 100644 index 00000000..5470f3ca --- /dev/null +++ b/examples/solid/atoms/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/solid/atoms/vite.config.ts b/examples/solid/atoms/vite.config.ts new file mode 100644 index 00000000..4095d9be --- /dev/null +++ b/examples/solid/atoms/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' +import solid from 'vite-plugin-solid' + +export default defineConfig({ + plugins: [solid()], +}) diff --git a/examples/solid/simple/README.md b/examples/solid/simple/README.md index 99613fc0..f34d525e 100644 --- a/examples/solid/simple/README.md +++ b/examples/solid/simple/README.md @@ -1,28 +1,6 @@ -## Usage +# Solid Simple Example -```bash -$ npm install # or pnpm install or yarn install -``` +To run this example: -### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs) - -## Available Scripts - -In the project directory, you can run: - -### `npm run dev` - -Runs the app in the development mode.
-Open [http://localhost:5173](http://localhost:5173) to view it in the browser. - -### `npm run build` - -Builds the app for production to the `dist` folder.
-It correctly bundles Solid in production mode and optimizes the build for the best performance. - -The build is minified and the filenames include the hashes.
-Your app is ready to be deployed! - -## Deployment - -Learn more about deploying your application with the [documentations](https://vitejs.dev/guide/static-deploy.html) +- `npm install` +- `npm run dev` diff --git a/examples/solid/simple/src/index.tsx b/examples/solid/simple/src/index.tsx index c99f311b..97a4355c 100644 --- a/examples/solid/simple/src/index.tsx +++ b/examples/solid/simple/src/index.tsx @@ -1,4 +1,4 @@ -import { Store, useStore } from '@tanstack/solid-store' +import { Store, useSelector } from '@tanstack/solid-store' import { render } from 'solid-js/web' // You can instantiate a Store outside of Solid components too! @@ -12,7 +12,7 @@ interface DisplayProps { } export const Display = (props: DisplayProps) => { - const count = useStore(store, (state) => state[props.animals]) + const count = useSelector(store, (state) => state[props.animals]) // formerly, useStore. Now renamed to useSelector. return (
{props.animals}: {count()} diff --git a/examples/solid/store-actions/README.md b/examples/solid/store-actions/README.md new file mode 100644 index 00000000..2b3d74e4 --- /dev/null +++ b/examples/solid/store-actions/README.md @@ -0,0 +1,12 @@ +# Solid Store Actions Example + +This example demonstrates: + +- `useSelector` +- `_useStore` +- module-level `Store` actions + +To run this example: + +- `npm install` +- `npm run dev` diff --git a/examples/solid/store-actions/index.html b/examples/solid/store-actions/index.html new file mode 100644 index 00000000..6d560e19 --- /dev/null +++ b/examples/solid/store-actions/index.html @@ -0,0 +1,12 @@ + + + + + + TanStack Store Solid Store Actions Example App + + +
+ + + diff --git a/examples/solid/store-actions/package.json b/examples/solid/store-actions/package.json new file mode 100644 index 00000000..454334b3 --- /dev/null +++ b/examples/solid/store-actions/package.json @@ -0,0 +1,20 @@ +{ + "name": "@tanstack/store-example-solid-store-actions", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port=3050", + "build": "tsc && vite build", + "test:types": "tsc", + "preview": "vite preview" + }, + "dependencies": { + "@tanstack/solid-store": "^0.10.0", + "solid-js": "^1.9.12" + }, + "devDependencies": { + "typescript": "6.0.2", + "vite": "^8.0.8", + "vite-plugin-solid": "^2.11.12" + } +} diff --git a/examples/solid/store-actions/src/index.tsx b/examples/solid/store-actions/src/index.tsx new file mode 100644 index 00000000..5f78c86c --- /dev/null +++ b/examples/solid/store-actions/src/index.tsx @@ -0,0 +1,81 @@ +import { render } from 'solid-js/web' +import { Store, _useStore, useSelector } from '@tanstack/solid-store' + +// Optionally, you can create stores outside of Solid components at module scope +const petStore = new Store( + { + cats: 0, + dogs: 0, + }, + ({ setState, get }) => + // optionally, define actions for updating your store in specific ways right on the store. + ({ + addCat: () => + setState((prev) => ({ + ...prev, + cats: prev.cats + 1, + })), + addDog: () => + setState((prev) => ({ + ...prev, + dogs: prev.dogs + 1, + })), + log: () => console.log(get()), + }), +) + +function App() { + return ( +
+ +

Solid Store Actions

+

+ This example creates a module-level store with actions. Components read + state with useSelector and call mutations through{' '} + store.actions or the experimental _useStore{' '} + hook. +

+ + + +
+ ) +} + +function CatVoter() { + // _useStore gives both the selected state and actions in a single tuple + const [cats, { addCat }] = _useStore(petStore, (state) => state.cats) + + return ( +
+

Cats: {cats()}

+ +
+ ) +} + +function DogVoter() { + // _useStore gives both the selected state and actions in a single tuple + const [dogs, { addDog }] = _useStore(petStore, (state) => state.dogs) + + return ( +
+

Dogs: {dogs()}

+ +
+ ) +} + +function TotalCard() { + const total = useSelector(petStore, (state) => state.cats + state.dogs) + + return

Total votes: {total()}

+} + +render(() => , document.getElementById('root')!) diff --git a/examples/solid/store-actions/tsconfig.json b/examples/solid/store-actions/tsconfig.json new file mode 100644 index 00000000..5470f3ca --- /dev/null +++ b/examples/solid/store-actions/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/solid/store-actions/vite.config.ts b/examples/solid/store-actions/vite.config.ts new file mode 100644 index 00000000..4095d9be --- /dev/null +++ b/examples/solid/store-actions/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' +import solid from 'vite-plugin-solid' + +export default defineConfig({ + plugins: [solid()], +}) diff --git a/examples/solid/store-context/README.md b/examples/solid/store-context/README.md new file mode 100644 index 00000000..8ea54a87 --- /dev/null +++ b/examples/solid/store-context/README.md @@ -0,0 +1,14 @@ +# Solid Store Context Example + +This example demonstrates: + +- Solid `createContext` +- `useSelector` +- `useValue` +- `useSetValue` +- `useAtom` + +To run this example: + +- `npm install` +- `npm run dev` diff --git a/examples/solid/store-context/index.html b/examples/solid/store-context/index.html new file mode 100644 index 00000000..8d3656ea --- /dev/null +++ b/examples/solid/store-context/index.html @@ -0,0 +1,12 @@ + + + + + + TanStack Store Solid Store Context Example App + + +
+ + + diff --git a/examples/solid/store-context/package.json b/examples/solid/store-context/package.json new file mode 100644 index 00000000..6475f866 --- /dev/null +++ b/examples/solid/store-context/package.json @@ -0,0 +1,20 @@ +{ + "name": "@tanstack/store-example-solid-store-context", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port=3050", + "build": "tsc && vite build", + "test:types": "tsc", + "preview": "vite preview" + }, + "dependencies": { + "@tanstack/solid-store": "^0.10.0", + "solid-js": "^1.9.12" + }, + "devDependencies": { + "typescript": "6.0.2", + "vite": "^8.0.8", + "vite-plugin-solid": "^2.11.12" + } +} diff --git a/examples/solid/store-context/src/index.tsx b/examples/solid/store-context/src/index.tsx new file mode 100644 index 00000000..0f3d3c27 --- /dev/null +++ b/examples/solid/store-context/src/index.tsx @@ -0,0 +1,162 @@ +import { createContext, useContext } from 'solid-js' +import { render } from 'solid-js/web' +import { + createAtom, + Store, + useAtom, + useSelector, + useValue, +} from '@tanstack/solid-store' +import type { Atom } from '@tanstack/solid-store' + +// one drawback of storing stores and atoms in context is you have to define types for the context manually, instead of everything being inferred. + +type CounterStore = { + cats: number + dogs: number +} + +type StoreContextValue = { + votesStore: Store + countAtom: Atom +} + +const StoreContext = createContext() + +function useStoreContext() { + const value = useContext(StoreContext) + + if (!value) { + throw new Error('Missing StoreProvider for StoreContext') + } + + return value +} + +function App() { + // Solid components only run once per mount, so stores and atoms created here stay stable for this provider instance. + const votesStore = new Store({ + cats: 0, + dogs: 0, + }) + const countAtom = createAtom(0) + + return ( + +
+

Solid Store Context

+

+ This example provides both atoms and stores through a single typed + context object, then consumes them from nested components. +

+ + + + +
+

Nested Atom Components

+ + + +
+
+
+ ) +} + +function CatCard() { + // pull a store from context + const { votesStore } = useStoreContext() + // select a value from the store with useSelector + const value = useSelector(votesStore, (state) => state.cats) + + return

Cats: {value()}

+} + +function DogCard() { + // pull a store from context + const { votesStore } = useStoreContext() + // select a value from the store with useSelector + const value = useSelector(votesStore, (state) => state.dogs) + + return

Dogs: {value()}

+} + +function TotalCard() { + // pull a store from context + const { votesStore } = useStoreContext() + // custom selector to calculate total votes from the store state + const total = useSelector(votesStore, (state) => state.cats + state.dogs) + + return

Total votes: {total()}

+} + +function AtomSummary() { + // pull an atom from context + const { countAtom } = useStoreContext() + const count = useValue(countAtom) + + return

Atom count: {count()}

+} + +function NestedAtomControls() { + const { countAtom } = useStoreContext() + + return ( +
+ + +
+ ) +} + +function DeepAtomEditor() { + const { countAtom } = useStoreContext() + const [count, setCount] = useAtom(countAtom) + + return ( +
+

Editable atom count: {count()}

+ +
+ ) +} + +function StoreButtons() { + const { votesStore } = useStoreContext() + + return ( +
+ + +
+ ) +} + +render(() => , document.getElementById('root')!) diff --git a/examples/solid/store-context/tsconfig.json b/examples/solid/store-context/tsconfig.json new file mode 100644 index 00000000..5470f3ca --- /dev/null +++ b/examples/solid/store-context/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/solid/store-context/vite.config.ts b/examples/solid/store-context/vite.config.ts new file mode 100644 index 00000000..4095d9be --- /dev/null +++ b/examples/solid/store-context/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' +import solid from 'vite-plugin-solid' + +export default defineConfig({ + plugins: [solid()], +}) diff --git a/examples/solid/stores/README.md b/examples/solid/stores/README.md new file mode 100644 index 00000000..623b48f3 --- /dev/null +++ b/examples/solid/stores/README.md @@ -0,0 +1,12 @@ +# Solid Store Hooks Example + +This example demonstrates: + +- `useSelector` +- `store.setState` +- module-level `Store` + +To run this example: + +- `npm install` +- `npm run dev` diff --git a/examples/solid/stores/index.html b/examples/solid/stores/index.html new file mode 100644 index 00000000..5eff5b4f --- /dev/null +++ b/examples/solid/stores/index.html @@ -0,0 +1,12 @@ + + + + + + TanStack Store Solid Stores Example App + + +
+ + + diff --git a/examples/solid/stores/package.json b/examples/solid/stores/package.json new file mode 100644 index 00000000..cc97b8d8 --- /dev/null +++ b/examples/solid/stores/package.json @@ -0,0 +1,20 @@ +{ + "name": "@tanstack/store-example-solid-stores", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port=3050", + "build": "tsc && vite build", + "test:types": "tsc", + "preview": "vite preview" + }, + "dependencies": { + "@tanstack/solid-store": "^0.10.0", + "solid-js": "^1.9.12" + }, + "devDependencies": { + "typescript": "6.0.2", + "vite": "^8.0.8", + "vite-plugin-solid": "^2.11.12" + } +} diff --git a/examples/solid/stores/src/index.tsx b/examples/solid/stores/src/index.tsx new file mode 100644 index 00000000..8f204e65 --- /dev/null +++ b/examples/solid/stores/src/index.tsx @@ -0,0 +1,76 @@ +import { render } from 'solid-js/web' +import { Store, useSelector } from '@tanstack/solid-store' + +// Optionally, you can create stores outside of Solid components at module scope +const petStore = new Store({ + cats: 0, + dogs: 0, +}) + +function App() { + return ( +
+

Solid Store Hooks

+

+ This example creates a module-level store. Components read state with + `useSelector` and update it directly with `store.setState`. +

+ + + + +
+ ) +} + +function CatsCard() { + // read state slice (only re-renders when the selected value changes) + const value = useSelector(petStore, (state) => state.cats) + + return

Cats: {value()}

+} + +function DogsCard() { + // read state slice (only re-renders when the selected value changes) + const value = useSelector(petStore, (state) => state.dogs) + + return

Dogs: {value()}

+} + +function StoreButtons() { + return ( +
+ + +
+ ) +} + +function TotalCard() { + const total = useSelector(petStore, (state) => state.cats + state.dogs) + + return

Total votes: {total()}

+} + +render(() => , document.getElementById('root')!) diff --git a/examples/solid/stores/tsconfig.json b/examples/solid/stores/tsconfig.json new file mode 100644 index 00000000..5470f3ca --- /dev/null +++ b/examples/solid/stores/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/solid/stores/vite.config.ts b/examples/solid/stores/vite.config.ts new file mode 100644 index 00000000..4095d9be --- /dev/null +++ b/examples/solid/stores/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' +import solid from 'vite-plugin-solid' + +export default defineConfig({ + plugins: [solid()], +}) diff --git a/examples/svelte/atoms/README.md b/examples/svelte/atoms/README.md new file mode 100644 index 00000000..8c30f684 --- /dev/null +++ b/examples/svelte/atoms/README.md @@ -0,0 +1,12 @@ +# Svelte Atom Hooks Example + +This example demonstrates: + +- `useValue` +- `useSetValue` +- `useAtom` + +To run this example: + +- `npm install` +- `npm run dev` diff --git a/examples/svelte/atoms/index.html b/examples/svelte/atoms/index.html new file mode 100644 index 00000000..7e5b4461 --- /dev/null +++ b/examples/svelte/atoms/index.html @@ -0,0 +1,12 @@ + + + + + + TanStack Store Svelte Atoms Example App + + +
+ + + diff --git a/examples/svelte/atoms/package.json b/examples/svelte/atoms/package.json new file mode 100644 index 00000000..1000e895 --- /dev/null +++ b/examples/svelte/atoms/package.json @@ -0,0 +1,24 @@ +{ + "name": "@tanstack/store-example-svelte-atoms", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --port=3050", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-check --tsconfig ./tsconfig.json && tsc -p tsconfig.node.json" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^7.0.0", + "@tsconfig/svelte": "^5.0.8", + "svelte": "^5.55.3", + "svelte-check": "^4.4.6", + "tslib": "^2.8.1", + "typescript": "6.0.2", + "vite": "^8.0.8" + }, + "dependencies": { + "@tanstack/svelte-store": "^0.11.0" + } +} diff --git a/examples/svelte/atoms/src/App.svelte b/examples/svelte/atoms/src/App.svelte new file mode 100644 index 00000000..bcc00d96 --- /dev/null +++ b/examples/svelte/atoms/src/App.svelte @@ -0,0 +1,30 @@ + + +
+

Svelte Atom Hooks

+

+ This example creates a module-level atom and reads and updates it with the + Svelte hooks. +

+

Total: {count.current}

+
+ + +
+
+

Editable count: {editableCount.current}

+ +
+
diff --git a/examples/svelte/atoms/src/main.ts b/examples/svelte/atoms/src/main.ts new file mode 100644 index 00000000..928b6c52 --- /dev/null +++ b/examples/svelte/atoms/src/main.ts @@ -0,0 +1,8 @@ +import { mount } from 'svelte' +import App from './App.svelte' + +const app = mount(App, { + target: document.getElementById('app')!, +}) + +export default app diff --git a/examples/svelte/atoms/src/vite-env.d.ts b/examples/svelte/atoms/src/vite-env.d.ts new file mode 100644 index 00000000..4078e747 --- /dev/null +++ b/examples/svelte/atoms/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/examples/svelte/atoms/svelte.config.js b/examples/svelte/atoms/svelte.config.js new file mode 100644 index 00000000..8abe4369 --- /dev/null +++ b/examples/svelte/atoms/svelte.config.js @@ -0,0 +1,5 @@ +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' + +export default { + preprocess: vitePreprocess(), +} diff --git a/examples/svelte/atoms/tsconfig.json b/examples/svelte/atoms/tsconfig.json new file mode 100644 index 00000000..d9867cfa --- /dev/null +++ b/examples/svelte/atoms/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "@tsconfig/svelte/tsconfig.json", + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "resolveJsonModule": true, + "allowJs": true, + "checkJs": true, + "isolatedModules": true, + "moduleDetection": "force" + }, + "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/examples/svelte/atoms/tsconfig.node.json b/examples/svelte/atoms/tsconfig.node.json new file mode 100644 index 00000000..408b6903 --- /dev/null +++ b/examples/svelte/atoms/tsconfig.node.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noEmit": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/examples/svelte/atoms/vite.config.ts b/examples/svelte/atoms/vite.config.ts new file mode 100644 index 00000000..951a9ba4 --- /dev/null +++ b/examples/svelte/atoms/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' +import { svelte } from '@sveltejs/vite-plugin-svelte' + +export default defineConfig({ + plugins: [svelte()], +}) diff --git a/examples/svelte/simple/README.md b/examples/svelte/simple/README.md index e6cd94fc..b4e8e337 100644 --- a/examples/svelte/simple/README.md +++ b/examples/svelte/simple/README.md @@ -1,47 +1,6 @@ -# Svelte + TS + Vite +# Svelte Simple Example -This template should help get you started developing with Svelte and TypeScript in Vite. +To run this example: -## Recommended IDE Setup - -[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). - -## Need an official Svelte framework? - -Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more. - -## Technical considerations - -**Why use this over SvelteKit?** - -- It brings its own routing solution which might not be preferable for some users. -- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app. - -This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project. - -Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate. - -**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?** - -Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information. - -**Why include `.vscode/extensions.json`?** - -Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project. - -**Why enable `allowJs` in the TS template?** - -While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant. - -**Why is HMR not preserving my local component state?** - -HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr). - -If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR. - -```ts -// store.ts -// An extremely simple external store -import { writable } from 'svelte/store' -export default writable(0) -``` +- `npm install` +- `npm run dev` diff --git a/examples/svelte/simple/src/Display.svelte b/examples/svelte/simple/src/Display.svelte index b364c925..5d1a48aa 100644 --- a/examples/svelte/simple/src/Display.svelte +++ b/examples/svelte/simple/src/Display.svelte @@ -1,9 +1,9 @@ diff --git a/examples/svelte/store-actions/README.md b/examples/svelte/store-actions/README.md new file mode 100644 index 00000000..9b0ca088 --- /dev/null +++ b/examples/svelte/store-actions/README.md @@ -0,0 +1,12 @@ +# Svelte Store Actions Example + +This example demonstrates: + +- `useSelector` +- `_useStore` +- module-level `Store` actions + +To run this example: + +- `npm install` +- `npm run dev` diff --git a/examples/svelte/store-actions/index.html b/examples/svelte/store-actions/index.html new file mode 100644 index 00000000..f9a099cd --- /dev/null +++ b/examples/svelte/store-actions/index.html @@ -0,0 +1,12 @@ + + + + + + TanStack Store Svelte Store Actions Example App + + +
+ + + diff --git a/examples/svelte/store-actions/package.json b/examples/svelte/store-actions/package.json new file mode 100644 index 00000000..9bf1ffbc --- /dev/null +++ b/examples/svelte/store-actions/package.json @@ -0,0 +1,24 @@ +{ + "name": "@tanstack/store-example-svelte-store-actions", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --port=3050", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-check --tsconfig ./tsconfig.json && tsc -p tsconfig.node.json" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^7.0.0", + "@tsconfig/svelte": "^5.0.8", + "svelte": "^5.55.3", + "svelte-check": "^4.4.6", + "tslib": "^2.8.1", + "typescript": "6.0.2", + "vite": "^8.0.8" + }, + "dependencies": { + "@tanstack/svelte-store": "^0.11.0" + } +} diff --git a/examples/svelte/store-actions/src/App.svelte b/examples/svelte/store-actions/src/App.svelte new file mode 100644 index 00000000..eaddd233 --- /dev/null +++ b/examples/svelte/store-actions/src/App.svelte @@ -0,0 +1,51 @@ + + +
+ +

Svelte Store Actions

+

+ This example creates a module-level store with actions. Components read + state with useSelector and call mutations through + store.actions or the experimental _useStore + hook. +

+
+

Cats: {cats.current}

+ +
+
+

Dogs: {dogs.current}

+ +
+

Total votes: {total.current}

+
diff --git a/examples/svelte/store-actions/src/main.ts b/examples/svelte/store-actions/src/main.ts new file mode 100644 index 00000000..928b6c52 --- /dev/null +++ b/examples/svelte/store-actions/src/main.ts @@ -0,0 +1,8 @@ +import { mount } from 'svelte' +import App from './App.svelte' + +const app = mount(App, { + target: document.getElementById('app')!, +}) + +export default app diff --git a/examples/svelte/store-actions/src/vite-env.d.ts b/examples/svelte/store-actions/src/vite-env.d.ts new file mode 100644 index 00000000..4078e747 --- /dev/null +++ b/examples/svelte/store-actions/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/examples/svelte/store-actions/svelte.config.js b/examples/svelte/store-actions/svelte.config.js new file mode 100644 index 00000000..8abe4369 --- /dev/null +++ b/examples/svelte/store-actions/svelte.config.js @@ -0,0 +1,5 @@ +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' + +export default { + preprocess: vitePreprocess(), +} diff --git a/examples/svelte/store-actions/tsconfig.json b/examples/svelte/store-actions/tsconfig.json new file mode 100644 index 00000000..d9867cfa --- /dev/null +++ b/examples/svelte/store-actions/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "@tsconfig/svelte/tsconfig.json", + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "resolveJsonModule": true, + "allowJs": true, + "checkJs": true, + "isolatedModules": true, + "moduleDetection": "force" + }, + "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/examples/svelte/store-actions/tsconfig.node.json b/examples/svelte/store-actions/tsconfig.node.json new file mode 100644 index 00000000..408b6903 --- /dev/null +++ b/examples/svelte/store-actions/tsconfig.node.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noEmit": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/examples/svelte/store-actions/vite.config.ts b/examples/svelte/store-actions/vite.config.ts new file mode 100644 index 00000000..951a9ba4 --- /dev/null +++ b/examples/svelte/store-actions/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' +import { svelte } from '@sveltejs/vite-plugin-svelte' + +export default defineConfig({ + plugins: [svelte()], +}) diff --git a/examples/svelte/store-context/README.md b/examples/svelte/store-context/README.md new file mode 100644 index 00000000..e43aec87 --- /dev/null +++ b/examples/svelte/store-context/README.md @@ -0,0 +1,14 @@ +# Svelte Store Context Example + +This example demonstrates: + +- Svelte `setContext`/`getContext` +- `useSelector` +- `useValue` +- `useSetValue` +- `useAtom` + +To run this example: + +- `npm install` +- `npm run dev` diff --git a/examples/svelte/store-context/index.html b/examples/svelte/store-context/index.html new file mode 100644 index 00000000..01da3cc3 --- /dev/null +++ b/examples/svelte/store-context/index.html @@ -0,0 +1,12 @@ + + + + + + TanStack Store Svelte Store Context Example App + + +
+ + + diff --git a/examples/svelte/store-context/package.json b/examples/svelte/store-context/package.json new file mode 100644 index 00000000..e5e50572 --- /dev/null +++ b/examples/svelte/store-context/package.json @@ -0,0 +1,24 @@ +{ + "name": "@tanstack/store-example-svelte-store-context", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --port=3050", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-check --tsconfig ./tsconfig.json && tsc -p tsconfig.node.json" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^7.0.0", + "@tsconfig/svelte": "^5.0.8", + "svelte": "^5.55.3", + "svelte-check": "^4.4.6", + "tslib": "^2.8.1", + "typescript": "6.0.2", + "vite": "^8.0.8" + }, + "dependencies": { + "@tanstack/svelte-store": "^0.11.0" + } +} diff --git a/examples/svelte/store-context/src/App.svelte b/examples/svelte/store-context/src/App.svelte new file mode 100644 index 00000000..0df2155b --- /dev/null +++ b/examples/svelte/store-context/src/App.svelte @@ -0,0 +1,29 @@ + + +
+

Svelte Store Context

+

+ This example provides both atoms and stores through a single typed context + object, then consumes them from nested components. +

+ + +
diff --git a/examples/svelte/store-context/src/AtomSection.svelte b/examples/svelte/store-context/src/AtomSection.svelte new file mode 100644 index 00000000..847eb952 --- /dev/null +++ b/examples/svelte/store-context/src/AtomSection.svelte @@ -0,0 +1,28 @@ + + +
+

Nested Atom Components

+

Atom count: {count.current}

+
+ + +
+
+

Editable atom count: {editableCount.current}

+ +
+
diff --git a/examples/svelte/store-context/src/StoreSection.svelte b/examples/svelte/store-context/src/StoreSection.svelte new file mode 100644 index 00000000..9e86e01c --- /dev/null +++ b/examples/svelte/store-context/src/StoreSection.svelte @@ -0,0 +1,38 @@ + + +

Cats: {cats.current}

+

Dogs: {dogs.current}

+

Total votes: {total.current}

+
+ + +
diff --git a/examples/svelte/store-context/src/context.ts b/examples/svelte/store-context/src/context.ts new file mode 100644 index 00000000..0d15dab0 --- /dev/null +++ b/examples/svelte/store-context/src/context.ts @@ -0,0 +1,13 @@ +import type { Atom, Store } from '@tanstack/svelte-store' + +export type CounterStore = { + cats: number + dogs: number +} + +export type StoreContextValue = { + votesStore: Store + countAtom: Atom +} + +export const STORE_CONTEXT = Symbol('store-context') diff --git a/examples/svelte/store-context/src/main.ts b/examples/svelte/store-context/src/main.ts new file mode 100644 index 00000000..928b6c52 --- /dev/null +++ b/examples/svelte/store-context/src/main.ts @@ -0,0 +1,8 @@ +import { mount } from 'svelte' +import App from './App.svelte' + +const app = mount(App, { + target: document.getElementById('app')!, +}) + +export default app diff --git a/examples/svelte/store-context/src/vite-env.d.ts b/examples/svelte/store-context/src/vite-env.d.ts new file mode 100644 index 00000000..4078e747 --- /dev/null +++ b/examples/svelte/store-context/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/examples/svelte/store-context/svelte.config.js b/examples/svelte/store-context/svelte.config.js new file mode 100644 index 00000000..8abe4369 --- /dev/null +++ b/examples/svelte/store-context/svelte.config.js @@ -0,0 +1,5 @@ +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' + +export default { + preprocess: vitePreprocess(), +} diff --git a/examples/svelte/store-context/tsconfig.json b/examples/svelte/store-context/tsconfig.json new file mode 100644 index 00000000..d9867cfa --- /dev/null +++ b/examples/svelte/store-context/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "@tsconfig/svelte/tsconfig.json", + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "resolveJsonModule": true, + "allowJs": true, + "checkJs": true, + "isolatedModules": true, + "moduleDetection": "force" + }, + "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/examples/svelte/store-context/tsconfig.node.json b/examples/svelte/store-context/tsconfig.node.json new file mode 100644 index 00000000..408b6903 --- /dev/null +++ b/examples/svelte/store-context/tsconfig.node.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noEmit": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/examples/svelte/store-context/vite.config.ts b/examples/svelte/store-context/vite.config.ts new file mode 100644 index 00000000..951a9ba4 --- /dev/null +++ b/examples/svelte/store-context/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' +import { svelte } from '@sveltejs/vite-plugin-svelte' + +export default defineConfig({ + plugins: [svelte()], +}) diff --git a/examples/svelte/stores/README.md b/examples/svelte/stores/README.md new file mode 100644 index 00000000..5a2eb4b0 --- /dev/null +++ b/examples/svelte/stores/README.md @@ -0,0 +1,12 @@ +# Svelte Store Hooks Example + +This example demonstrates: + +- `useSelector` +- `store.setState` +- module-level `Store` + +To run this example: + +- `npm install` +- `npm run dev` diff --git a/examples/svelte/stores/index.html b/examples/svelte/stores/index.html new file mode 100644 index 00000000..1e30df07 --- /dev/null +++ b/examples/svelte/stores/index.html @@ -0,0 +1,12 @@ + + + + + + TanStack Store Svelte Stores Example App + + +
+ + + diff --git a/examples/svelte/stores/package.json b/examples/svelte/stores/package.json new file mode 100644 index 00000000..bda27b0f --- /dev/null +++ b/examples/svelte/stores/package.json @@ -0,0 +1,24 @@ +{ + "name": "@tanstack/store-example-svelte-stores", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --port=3050", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-check --tsconfig ./tsconfig.json && tsc -p tsconfig.node.json" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^7.0.0", + "@tsconfig/svelte": "^5.0.8", + "svelte": "^5.55.3", + "svelte-check": "^4.4.6", + "tslib": "^2.8.1", + "typescript": "6.0.2", + "vite": "^8.0.8" + }, + "dependencies": { + "@tanstack/svelte-store": "^0.11.0" + } +} diff --git a/examples/svelte/stores/src/App.svelte b/examples/svelte/stores/src/App.svelte new file mode 100644 index 00000000..5e3c6606 --- /dev/null +++ b/examples/svelte/stores/src/App.svelte @@ -0,0 +1,44 @@ + + +
+

Svelte Store Hooks

+

+ This example creates a module-level store. Components read state with + `useSelector` and update it directly with `store.setState`. +

+

Cats: {cats.current}

+

Dogs: {dogs.current}

+

Total votes: {total.current}

+
+ + +
+
diff --git a/examples/svelte/stores/src/main.ts b/examples/svelte/stores/src/main.ts new file mode 100644 index 00000000..928b6c52 --- /dev/null +++ b/examples/svelte/stores/src/main.ts @@ -0,0 +1,8 @@ +import { mount } from 'svelte' +import App from './App.svelte' + +const app = mount(App, { + target: document.getElementById('app')!, +}) + +export default app diff --git a/examples/svelte/stores/src/vite-env.d.ts b/examples/svelte/stores/src/vite-env.d.ts new file mode 100644 index 00000000..4078e747 --- /dev/null +++ b/examples/svelte/stores/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/examples/svelte/stores/svelte.config.js b/examples/svelte/stores/svelte.config.js new file mode 100644 index 00000000..8abe4369 --- /dev/null +++ b/examples/svelte/stores/svelte.config.js @@ -0,0 +1,5 @@ +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' + +export default { + preprocess: vitePreprocess(), +} diff --git a/examples/svelte/stores/tsconfig.json b/examples/svelte/stores/tsconfig.json new file mode 100644 index 00000000..d9867cfa --- /dev/null +++ b/examples/svelte/stores/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "@tsconfig/svelte/tsconfig.json", + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "resolveJsonModule": true, + "allowJs": true, + "checkJs": true, + "isolatedModules": true, + "moduleDetection": "force" + }, + "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/examples/svelte/stores/tsconfig.node.json b/examples/svelte/stores/tsconfig.node.json new file mode 100644 index 00000000..408b6903 --- /dev/null +++ b/examples/svelte/stores/tsconfig.node.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noEmit": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/examples/svelte/stores/vite.config.ts b/examples/svelte/stores/vite.config.ts new file mode 100644 index 00000000..951a9ba4 --- /dev/null +++ b/examples/svelte/stores/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' +import { svelte } from '@sveltejs/vite-plugin-svelte' + +export default defineConfig({ + plugins: [svelte()], +}) diff --git a/examples/vue/atoms/README.md b/examples/vue/atoms/README.md new file mode 100644 index 00000000..522cac99 --- /dev/null +++ b/examples/vue/atoms/README.md @@ -0,0 +1,12 @@ +# Vue Atom Hooks Example + +This example demonstrates: + +- `useValue` +- `useSetValue` +- `useAtom` + +To run this example: + +- `npm install` +- `npm run dev` diff --git a/examples/vue/atoms/index.html b/examples/vue/atoms/index.html new file mode 100644 index 00000000..d6750447 --- /dev/null +++ b/examples/vue/atoms/index.html @@ -0,0 +1,12 @@ + + + + + + TanStack Store Vue Atoms Example App + + +
+ + + diff --git a/examples/vue/atoms/package.json b/examples/vue/atoms/package.json new file mode 100644 index 00000000..02b792ee --- /dev/null +++ b/examples/vue/atoms/package.json @@ -0,0 +1,22 @@ +{ + "name": "@tanstack/store-example-vue-atoms", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port=3050", + "build": "vite build", + "build:dev": "vite build -m development", + "test:types": "vue-tsc", + "serve": "vite preview" + }, + "dependencies": { + "@tanstack/vue-store": "^0.10.0", + "vue": "^3.5.32" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.5", + "typescript": "6.0.2", + "vite": "^8.0.8", + "vue-tsc": "^3.2.6" + } +} diff --git a/examples/vue/atoms/src/App.vue b/examples/vue/atoms/src/App.vue new file mode 100644 index 00000000..ffbbfed2 --- /dev/null +++ b/examples/vue/atoms/src/App.vue @@ -0,0 +1,35 @@ + + + diff --git a/examples/vue/atoms/src/main.ts b/examples/vue/atoms/src/main.ts new file mode 100644 index 00000000..01433bca --- /dev/null +++ b/examples/vue/atoms/src/main.ts @@ -0,0 +1,4 @@ +import { createApp } from 'vue' +import App from './App.vue' + +createApp(App).mount('#app') diff --git a/examples/vue/atoms/src/shims-vue.d.ts b/examples/vue/atoms/src/shims-vue.d.ts new file mode 100644 index 00000000..ac1ded79 --- /dev/null +++ b/examples/vue/atoms/src/shims-vue.d.ts @@ -0,0 +1,5 @@ +declare module '*.vue' { + import { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} diff --git a/examples/vue/atoms/tsconfig.json b/examples/vue/atoms/tsconfig.json new file mode 100644 index 00000000..2dfc1e6b --- /dev/null +++ b/examples/vue/atoms/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"] +} diff --git a/examples/vue/atoms/vite.config.ts b/examples/vue/atoms/vite.config.ts new file mode 100644 index 00000000..c40aa3c3 --- /dev/null +++ b/examples/vue/atoms/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], +}) diff --git a/examples/vue/simple/README.md b/examples/vue/simple/README.md index 28462a4a..4143f8f7 100644 --- a/examples/vue/simple/README.md +++ b/examples/vue/simple/README.md @@ -1,6 +1,6 @@ -# Basic example +# Vue Simple Example To run this example: -- `npm install` or `yarn` or `pnpm i` -- `npm run dev` or `yarn dev` or `pnpm dev` +- `npm install` +- `npm run dev` diff --git a/examples/vue/simple/src/Display.vue b/examples/vue/simple/src/Display.vue index ac211d10..9f95603e 100644 --- a/examples/vue/simple/src/Display.vue +++ b/examples/vue/simple/src/Display.vue @@ -1,9 +1,9 @@ diff --git a/examples/vue/store-actions/README.md b/examples/vue/store-actions/README.md new file mode 100644 index 00000000..48758d4e --- /dev/null +++ b/examples/vue/store-actions/README.md @@ -0,0 +1,12 @@ +# Vue Store Actions Example + +This example demonstrates: + +- `useSelector` +- `_useStore` +- module-level `Store` actions + +To run this example: + +- `npm install` +- `npm run dev` diff --git a/examples/vue/store-actions/index.html b/examples/vue/store-actions/index.html new file mode 100644 index 00000000..5bc92e3a --- /dev/null +++ b/examples/vue/store-actions/index.html @@ -0,0 +1,12 @@ + + + + + + TanStack Store Vue Store Actions Example App + + +
+ + + diff --git a/examples/vue/store-actions/package.json b/examples/vue/store-actions/package.json new file mode 100644 index 00000000..4448bace --- /dev/null +++ b/examples/vue/store-actions/package.json @@ -0,0 +1,22 @@ +{ + "name": "@tanstack/store-example-vue-store-actions", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port=3050", + "build": "vite build", + "build:dev": "vite build -m development", + "test:types": "vue-tsc", + "serve": "vite preview" + }, + "dependencies": { + "@tanstack/vue-store": "^0.10.0", + "vue": "^3.5.32" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.5", + "typescript": "6.0.2", + "vite": "^8.0.8", + "vue-tsc": "^3.2.6" + } +} diff --git a/examples/vue/store-actions/src/App.vue b/examples/vue/store-actions/src/App.vue new file mode 100644 index 00000000..942abe52 --- /dev/null +++ b/examples/vue/store-actions/src/App.vue @@ -0,0 +1,53 @@ + + + diff --git a/examples/vue/store-actions/src/main.ts b/examples/vue/store-actions/src/main.ts new file mode 100644 index 00000000..01433bca --- /dev/null +++ b/examples/vue/store-actions/src/main.ts @@ -0,0 +1,4 @@ +import { createApp } from 'vue' +import App from './App.vue' + +createApp(App).mount('#app') diff --git a/examples/vue/store-actions/src/shims-vue.d.ts b/examples/vue/store-actions/src/shims-vue.d.ts new file mode 100644 index 00000000..ac1ded79 --- /dev/null +++ b/examples/vue/store-actions/src/shims-vue.d.ts @@ -0,0 +1,5 @@ +declare module '*.vue' { + import { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} diff --git a/examples/vue/store-actions/tsconfig.json b/examples/vue/store-actions/tsconfig.json new file mode 100644 index 00000000..2dfc1e6b --- /dev/null +++ b/examples/vue/store-actions/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"] +} diff --git a/examples/vue/store-actions/vite.config.ts b/examples/vue/store-actions/vite.config.ts new file mode 100644 index 00000000..c40aa3c3 --- /dev/null +++ b/examples/vue/store-actions/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], +}) diff --git a/examples/vue/store-context/README.md b/examples/vue/store-context/README.md new file mode 100644 index 00000000..6d07f72d --- /dev/null +++ b/examples/vue/store-context/README.md @@ -0,0 +1,14 @@ +# Vue Store Context Example + +This example demonstrates: + +- Vue `provide`/`inject` +- `useSelector` +- `useValue` +- `useSetValue` +- `useAtom` + +To run this example: + +- `npm install` +- `npm run dev` diff --git a/examples/vue/store-context/index.html b/examples/vue/store-context/index.html new file mode 100644 index 00000000..74eaf634 --- /dev/null +++ b/examples/vue/store-context/index.html @@ -0,0 +1,12 @@ + + + + + + TanStack Store Vue Store Context Example App + + +
+ + + diff --git a/examples/vue/store-context/package.json b/examples/vue/store-context/package.json new file mode 100644 index 00000000..03e1a0f9 --- /dev/null +++ b/examples/vue/store-context/package.json @@ -0,0 +1,22 @@ +{ + "name": "@tanstack/store-example-vue-store-context", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port=3050", + "build": "vite build", + "build:dev": "vite build -m development", + "test:types": "vue-tsc", + "serve": "vite preview" + }, + "dependencies": { + "@tanstack/vue-store": "^0.10.0", + "vue": "^3.5.32" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.5", + "typescript": "6.0.2", + "vite": "^8.0.8", + "vue-tsc": "^3.2.6" + } +} diff --git a/examples/vue/store-context/src/App.vue b/examples/vue/store-context/src/App.vue new file mode 100644 index 00000000..5938e9a3 --- /dev/null +++ b/examples/vue/store-context/src/App.vue @@ -0,0 +1,166 @@ + + + diff --git a/examples/vue/store-context/src/main.ts b/examples/vue/store-context/src/main.ts new file mode 100644 index 00000000..01433bca --- /dev/null +++ b/examples/vue/store-context/src/main.ts @@ -0,0 +1,4 @@ +import { createApp } from 'vue' +import App from './App.vue' + +createApp(App).mount('#app') diff --git a/examples/vue/store-context/src/shims-vue.d.ts b/examples/vue/store-context/src/shims-vue.d.ts new file mode 100644 index 00000000..ac1ded79 --- /dev/null +++ b/examples/vue/store-context/src/shims-vue.d.ts @@ -0,0 +1,5 @@ +declare module '*.vue' { + import { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} diff --git a/examples/vue/store-context/tsconfig.json b/examples/vue/store-context/tsconfig.json new file mode 100644 index 00000000..2dfc1e6b --- /dev/null +++ b/examples/vue/store-context/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"] +} diff --git a/examples/vue/store-context/vite.config.ts b/examples/vue/store-context/vite.config.ts new file mode 100644 index 00000000..c40aa3c3 --- /dev/null +++ b/examples/vue/store-context/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], +}) diff --git a/examples/vue/stores/README.md b/examples/vue/stores/README.md new file mode 100644 index 00000000..66ce8b1f --- /dev/null +++ b/examples/vue/stores/README.md @@ -0,0 +1,12 @@ +# Vue Store Hooks Example + +This example demonstrates: + +- `useSelector` +- `store.setState` +- module-level `Store` + +To run this example: + +- `npm install` +- `npm run dev` diff --git a/examples/vue/stores/index.html b/examples/vue/stores/index.html new file mode 100644 index 00000000..351679fc --- /dev/null +++ b/examples/vue/stores/index.html @@ -0,0 +1,12 @@ + + + + + + TanStack Store Vue Stores Example App + + +
+ + + diff --git a/examples/vue/stores/package.json b/examples/vue/stores/package.json new file mode 100644 index 00000000..1e814dc2 --- /dev/null +++ b/examples/vue/stores/package.json @@ -0,0 +1,22 @@ +{ + "name": "@tanstack/store-example-vue-stores", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port=3050", + "build": "vite build", + "build:dev": "vite build -m development", + "test:types": "vue-tsc", + "serve": "vite preview" + }, + "dependencies": { + "@tanstack/vue-store": "^0.10.0", + "vue": "^3.5.32" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.5", + "typescript": "6.0.2", + "vite": "^8.0.8", + "vue-tsc": "^3.2.6" + } +} diff --git a/examples/vue/stores/src/App.vue b/examples/vue/stores/src/App.vue new file mode 100644 index 00000000..6949019c --- /dev/null +++ b/examples/vue/stores/src/App.vue @@ -0,0 +1,46 @@ + + + diff --git a/examples/vue/stores/src/main.ts b/examples/vue/stores/src/main.ts new file mode 100644 index 00000000..01433bca --- /dev/null +++ b/examples/vue/stores/src/main.ts @@ -0,0 +1,4 @@ +import { createApp } from 'vue' +import App from './App.vue' + +createApp(App).mount('#app') diff --git a/examples/vue/stores/src/shims-vue.d.ts b/examples/vue/stores/src/shims-vue.d.ts new file mode 100644 index 00000000..ac1ded79 --- /dev/null +++ b/examples/vue/stores/src/shims-vue.d.ts @@ -0,0 +1,5 @@ +declare module '*.vue' { + import { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} diff --git a/examples/vue/stores/tsconfig.json b/examples/vue/stores/tsconfig.json new file mode 100644 index 00000000..2dfc1e6b --- /dev/null +++ b/examples/vue/stores/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"] +} diff --git a/examples/vue/stores/vite.config.ts b/examples/vue/stores/vite.config.ts new file mode 100644 index 00000000..c40aa3c3 --- /dev/null +++ b/examples/vue/stores/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], +}) diff --git a/packages/angular-store/src/_injectStore.ts b/packages/angular-store/src/_injectStore.ts new file mode 100644 index 00000000..2a6f9714 --- /dev/null +++ b/packages/angular-store/src/_injectStore.ts @@ -0,0 +1,41 @@ +import { injectSelector } from './injectSelector' +import type { Signal } from '@angular/core' +import type { Store, StoreActionMap } from '@tanstack/store' +import type { InjectSelectorOptions } from './injectSelector' + +/** + * Experimental combined read+write injection function for stores, mirroring + * injectAtom's pattern. + * + * Returns `[signal, actions]` when the store has an actions factory, or + * `[signal, setState]` for plain stores. + * + * @example + * ```ts + * // Store with actions + * readonly result = _injectStore(petStore, (s) => s.cats) + * // result[0] is Signal, result[1] is actions + * + * // Store without actions + * readonly result = _injectStore(plainStore, (s) => s) + * // result[0] is Signal, result[1] is setState + * ``` + */ +export function _injectStore< + TState, + TActions extends StoreActionMap, + TSelected = NoInfer, +>( + store: Store, + selector: (state: NoInfer) => TSelected, + options?: InjectSelectorOptions, +): [ + Signal, + [TActions] extends [never] ? Store['setState'] : TActions, +] { + const selected = injectSelector(store, selector, options) + const actionsOrSetState = + (store.actions as StoreActionMap | undefined) ?? store.setState + + return [selected, actionsOrSetState] as any +} diff --git a/packages/angular-store/src/createStoreContext.ts b/packages/angular-store/src/createStoreContext.ts new file mode 100644 index 00000000..e724713c --- /dev/null +++ b/packages/angular-store/src/createStoreContext.ts @@ -0,0 +1,71 @@ +import { InjectionToken, inject } from '@angular/core' +import type { Provider } from '@angular/core' + +/** + * Creates a typed Angular dependency-injection context for sharing a bundle of + * atoms and stores with a component subtree. + * + * The returned `provideStoreContext` function accepts a factory that creates the + * context value. Using a factory (rather than a static value) ensures each + * component instance — and each SSR request — receives its own state, avoiding + * cross-request pollution. + * + * Consumers call `injectStoreContext()` inside an injection context (typically a + * constructor or field initializer) to retrieve the contextual atoms and stores, + * then compose them with existing hooks like {@link injectSelector}, + * {@link injectValue}, and {@link injectAtom}. + * + * @example + * ```ts + * const { provideStoreContext, injectStoreContext } = createStoreContext<{ + * countAtom: Atom + * totalsStore: Store<{ count: number }> + * }>() + * + * // Parent component provides the context + * @Component({ + * providers: [ + * provideStoreContext(() => ({ + * countAtom: createAtom(0), + * totalsStore: new Store({ count: 0 }), + * })), + * ], + * template: ``, + * }) + * class ParentComponent {} + * + * // Child component consumes the context + * @Component({ template: `{{ count() }}` }) + * class ChildComponent { + * private ctx = injectStoreContext() + * count = injectValue(this.ctx.countAtom) + * } + * ``` + * + * @throws When `injectStoreContext()` is called without a matching + * `provideStoreContext()` in a parent component's providers. + */ +export function createStoreContext(): { + provideStoreContext: (factory: () => TValue) => Provider + injectStoreContext: () => TValue +} { + const token = new InjectionToken('StoreContext') + + function provideStoreContext(factory: () => TValue): Provider { + return { provide: token, useFactory: factory } + } + + function injectStoreContext(): TValue { + const value = inject(token, { optional: true }) + + if (value === null) { + throw new Error( + "Missing StoreContext provider. Add provideStoreContext() to a parent component's providers array.", + ) + } + + return value + } + + return { provideStoreContext, injectStoreContext } +} diff --git a/packages/angular-store/src/index.ts b/packages/angular-store/src/index.ts index 746bfed2..0164d1cd 100644 --- a/packages/angular-store/src/index.ts +++ b/packages/angular-store/src/index.ts @@ -1,108 +1,10 @@ -import { - DestroyRef, - Injector, - assertInInjectionContext, - inject, - linkedSignal, - runInInjectionContext, -} from '@angular/core' -import type { Atom, ReadonlyAtom } from '@tanstack/store' -import type { CreateSignalOptions, Signal } from '@angular/core' - -type StoreContext = Record - export * from '@tanstack/store' -export function injectStore>( - store: Atom, - selector?: (state: NoInfer) => TSelected, - options?: CreateSignalOptions & { injector?: Injector }, -): Signal -export function injectStore>( - store: Atom | ReadonlyAtom, - selector?: (state: NoInfer) => TSelected, - options?: CreateSignalOptions & { injector?: Injector }, -): Signal -export function injectStore< - TState extends StoreContext, - TSelected = NoInfer, ->( - store: Atom | ReadonlyAtom, - selector: (state: NoInfer) => TSelected = (d) => - d as unknown as TSelected, - options: CreateSignalOptions & { injector?: Injector } = { - equal: shallow, - }, -): Signal { - !options.injector && assertInInjectionContext(injectStore) - - if (!options.injector) { - options.injector = inject(Injector) - } - - return runInInjectionContext(options.injector, () => { - const destroyRef = inject(DestroyRef) - const slice = linkedSignal(() => selector(store.get()), options) - - const { unsubscribe } = store.subscribe((s) => { - slice.set(selector(s)) - }) - - destroyRef.onDestroy(() => { - unsubscribe() - }) - - return slice.asReadonly() - }) -} - -function shallow(objA: T, objB: T) { - if (Object.is(objA, objB)) { - return true - } - - if ( - typeof objA !== 'object' || - objA === null || - typeof objB !== 'object' || - objB === null - ) { - return false - } - - if (objA instanceof Map && objB instanceof Map) { - if (objA.size !== objB.size) return false - for (const [k, v] of objA) { - if (!objB.has(k) || !Object.is(v, objB.get(k))) return false - } - return true - } - - if (objA instanceof Set && objB instanceof Set) { - if (objA.size !== objB.size) return false - for (const v of objA) { - if (!objB.has(v)) return false - } - return true - } - - if (objA instanceof Date && objB instanceof Date) { - if (objA.getTime() !== objB.getTime()) return false - return true - } +export * from './injectSelector' +export * from './injectValue' - const keysA = Object.keys(objA) - if (keysA.length !== Object.keys(objB).length) { - return false - } +export * from './injectAtom' +export * from './injectStore' // @deprecated in favor of injectSelector +export * from './_injectStore' - for (const key of keysA) { - if ( - !Object.prototype.hasOwnProperty.call(objB, key) || - !Object.is(objA[key as keyof T], objB[key as keyof T]) - ) { - return false - } - } - return true -} +export * from './createStoreContext' diff --git a/packages/angular-store/src/injectAtom.ts b/packages/angular-store/src/injectAtom.ts new file mode 100644 index 00000000..ef28257e --- /dev/null +++ b/packages/angular-store/src/injectAtom.ts @@ -0,0 +1,52 @@ +import { injectValue } from './injectValue' +import type { Atom } from '@tanstack/store' +import type { InjectSelectorOptions } from './injectSelector' + +/** + * A callable signal that reads the current atom value when invoked and + * exposes a `.set` method matching the atom's native setter contract. + * + * This is the Angular-idiomatic return type for {@link injectAtom}. It can + * be used as a class property and called directly in templates. + * + * @example + * ```ts + * readonly count = injectAtom(countAtom) + * + * // read in template: {{ count() }} + * // write in class: this.count.set(5) + * // this.count.set(prev => prev + 1) + * ``` + */ +export interface WritableAtomSignal { + /** Read the current value. */ + (): T + /** Set the atom value (accepts a direct value or an updater function). */ + set: Atom['set'] +} + +/** + * Returns a {@link WritableAtomSignal} that reads the current atom value when + * called and exposes a `.set` method for updates. + * + * Use this when a component needs to both read and update the same writable + * atom. + * + * @example + * ```ts + * readonly count = injectAtom(countAtom) + * + * increment() { + * this.count.set((prev) => prev + 1) + * } + * ``` + */ +export function injectAtom( + atom: Atom, + options?: InjectSelectorOptions, +): WritableAtomSignal { + const value = injectValue(atom, options) + const atomSignal = (() => value()) as WritableAtomSignal + atomSignal.set = atom.set + return atomSignal +} diff --git a/packages/angular-store/src/injectSelector.ts b/packages/angular-store/src/injectSelector.ts new file mode 100644 index 00000000..d9b0e249 --- /dev/null +++ b/packages/angular-store/src/injectSelector.ts @@ -0,0 +1,100 @@ +import { + DestroyRef, + Injector, + assertInInjectionContext, + inject, + linkedSignal, + runInInjectionContext, +} from '@angular/core' +import type { CreateSignalOptions, Signal } from '@angular/core' + +export interface InjectSelectorOptions extends Omit< + CreateSignalOptions, + 'equal' +> { + compare?: (a: TSelected, b: TSelected) => boolean + injector?: Injector +} + +export type SelectionSource = { + get: () => T + subscribe: (listener: (value: T) => void) => { + unsubscribe: () => void + } +} + +function defaultCompare(a: T, b: T) { + return a === b +} + +function resolveInjector( + fn: (...args: Array) => unknown, + injector?: Injector, +) { + if (!injector) { + assertInInjectionContext(fn) + return inject(Injector) + } + + return injector +} + +function createReadonlySelectionSignal( + source: SelectionSource, + selector: (state: NoInfer) => TSelected, + options?: InjectSelectorOptions, +): Signal { + const injector = resolveInjector( + createReadonlySelectionSignal, + options?.injector, + ) + + return runInInjectionContext(injector, () => { + const destroyRef = inject(DestroyRef) + const compare = options?.compare ?? defaultCompare + const { + injector: _injector, + compare: _compare, + ...signalOptions + } = options ?? {} + const slice = linkedSignal(() => selector(source.get()), { + ...signalOptions, + equal: compare, + }) + + const { unsubscribe } = source.subscribe((state) => { + slice.set(selector(state)) + }) + + destroyRef.onDestroy(() => { + unsubscribe() + }) + + return slice.asReadonly() + }) +} + +/** + * Selects a slice of state from an atom or store and returns it as an Angular + * signal. + * + * This is the primary Angular read hook for TanStack Store. + * + * @example + * ```ts + * readonly count = injectSelector(counterStore, (state) => state.count) + * ``` + * + * @example + * ```ts + * readonly doubled = injectSelector(countAtom, (value) => value * 2) + * ``` + */ +export function injectSelector>( + source: SelectionSource, + selector: (state: NoInfer) => TSelected = (d) => + d as unknown as TSelected, + options?: InjectSelectorOptions, +): Signal { + return createReadonlySelectionSignal(source, selector, options) +} diff --git a/packages/angular-store/src/injectStore.ts b/packages/angular-store/src/injectStore.ts new file mode 100644 index 00000000..360492d1 --- /dev/null +++ b/packages/angular-store/src/injectStore.ts @@ -0,0 +1,38 @@ +import { injectSelector } from './injectSelector' +import type { CreateSignalOptions, Injector, Signal } from '@angular/core' +import type { SelectionSource } from './injectSelector' + +type CompatibilityInjectStoreOptions = + CreateSignalOptions & { + injector?: Injector + } + +/** + * Deprecated alias for {@link injectSelector}. + * + * @example + * ```ts + * readonly count = injectStore(counterStore, (state) => state.count) + * ``` + * + * @deprecated Use `injectSelector` instead. + */ +export function injectStore>( + store: SelectionSource, + selector?: (state: NoInfer) => TSelected, + options?: CompatibilityInjectStoreOptions, +): Signal +export function injectStore>( + store: SelectionSource, + selector: (state: NoInfer) => TSelected = (d) => + d as unknown as TSelected, + options?: CompatibilityInjectStoreOptions, +): Signal { + const { equal, injector, ...signalOptions } = options ?? {} + + return injectSelector(store, selector, { + ...signalOptions, + compare: equal, + injector, + }) +} diff --git a/packages/angular-store/src/injectValue.ts b/packages/angular-store/src/injectValue.ts new file mode 100644 index 00000000..ea20b78c --- /dev/null +++ b/packages/angular-store/src/injectValue.ts @@ -0,0 +1,30 @@ +import { injectSelector } from './injectSelector' +import type { Signal } from '@angular/core' +import type { Atom, ReadonlyAtom, ReadonlyStore, Store } from '@tanstack/store' +import type { InjectSelectorOptions } from './injectSelector' + +/** + * Returns the current value signal for an atom or store. + * + * This is the whole-value counterpart to {@link injectSelector}. + * + * @example + * ```ts + * readonly count = injectValue(countAtom) + * ``` + * + * @example + * ```ts + * readonly state = injectValue(counterStore) + * ``` + */ +export function injectValue( + source: + | Atom + | ReadonlyAtom + | Store + | ReadonlyStore, + options?: InjectSelectorOptions, +): Signal { + return injectSelector(source, (value) => value, options) +} diff --git a/packages/angular-store/tests/index.test.ts b/packages/angular-store/tests/index.test.ts index 88da8df7..b25558db 100644 --- a/packages/angular-store/tests/index.test.ts +++ b/packages/angular-store/tests/index.test.ts @@ -2,11 +2,124 @@ import { describe, expect, test } from 'vitest' import { Component, effect } from '@angular/core' import { TestBed } from '@angular/core/testing' import { By } from '@angular/platform-browser' -import { createStore } from '@tanstack/store' -import { injectStore } from '../src/index' +import { Store, createAtom, createStore } from '@tanstack/store' +import { + _injectStore, + createStoreContext, + injectAtom, + injectSelector, + injectStore, + injectValue, +} from '../src/index' +import type { Atom } from '@tanstack/store' -describe('injectStore', () => { - test(`allows us to select state using a selector`, () => { +describe('atom hooks', () => { + test('injectValue reads mutable atom state and rerenders when updated', () => { + const atom = createAtom(0) + + @Component({ + template: ` +
+

Value: {{ value() }}

+ +
+ `, + standalone: true, + }) + class MyCmp { + value = injectValue(atom) + + update() { + atom.set((prev) => prev + 1) + } + } + + const fixture = TestBed.createComponent(MyCmp) + fixture.detectChanges() + + expect(fixture.nativeElement.textContent).toContain('Value: 0') + + fixture.debugElement + .query(By.css('button#update')) + .triggerEventHandler('click', null) + fixture.detectChanges() + + expect(fixture.nativeElement.textContent).toContain('Value: 1') + }) + + test('injectAtom returns a callable signal with a set method', () => { + const atom = createAtom(0) + + @Component({ + template: ` +
+

Value: {{ count() }}

+ +
+ `, + standalone: true, + }) + class MyCmp { + count = injectAtom(atom) + + add() { + this.count.set((prev) => prev + 5) + } + } + + const fixture = TestBed.createComponent(MyCmp) + fixture.detectChanges() + + expect(fixture.nativeElement.textContent).toContain('Value: 0') + + fixture.debugElement + .query(By.css('button#add')) + .triggerEventHandler('click', null) + fixture.detectChanges() + + expect(fixture.nativeElement.textContent).toContain('Value: 5') + }) + + test('injectAtom set accepts a direct value', () => { + const atom = createAtom(0) + + @Component({ + template: ` +
+

Value: {{ count() }}

+ +
+ `, + standalone: true, + }) + class MyCmp { + count = injectAtom(atom) + + constructor() { + this.count.set(42) + } + + reset() { + this.count.set(0) + } + } + + const fixture = TestBed.createComponent(MyCmp) + fixture.detectChanges() + + expect(fixture.nativeElement.textContent).toContain('Value: 42') + + fixture.debugElement + .query(By.css('button#reset')) + .triggerEventHandler('click', null) + fixture.detectChanges() + + expect(fixture.nativeElement.textContent).toContain('Value: 0') + }) +}) + +describe('selector hooks', () => { + test('allows us to select state using a selector', () => { const store = createStore({ select: 0, ignored: 1 }) @Component({ @@ -14,14 +127,61 @@ describe('injectStore', () => { standalone: true, }) class MyCmp { - storeVal = injectStore(store, (state) => state.select) + storeVal = injectSelector(store, (state) => state.select) + } + + const fixture = TestBed.createComponent(MyCmp) + fixture.detectChanges() + + expect(fixture.nativeElement.textContent).toContain('Store: 0') + }) + + test('injectValue reads writable and readonly store state', () => { + const baseStore = createStore(1) + const readonlyStore = createStore(() => ({ value: baseStore.state * 2 })) + + @Component({ + template: ` +
+

{{ value() }}

+

{{ readonlyValue().value }}

+ +
+ `, + standalone: true, + }) + class MyCmp { + value = injectValue(baseStore) + readonlyValue = injectValue(readonlyStore) + + update() { + baseStore.setState((prev) => prev + 1) + } } const fixture = TestBed.createComponent(MyCmp) fixture.detectChanges() - const element = fixture.nativeElement - expect(element.textContent).toContain('Store: 0') + expect( + fixture.debugElement.query(By.css('p#value')).nativeElement.textContent, + ).toContain('1') + expect( + fixture.debugElement.query(By.css('p#readonly')).nativeElement + .textContent, + ).toContain('2') + + fixture.debugElement + .query(By.css('button#update')) + .triggerEventHandler('click', null) + fixture.detectChanges() + + expect( + fixture.debugElement.query(By.css('p#value')).nativeElement.textContent, + ).toContain('2') + expect( + fixture.debugElement.query(By.css('p#readonly')).nativeElement + .textContent, + ).toContain('4') }) test('only triggers a re-render when selector state is updated', () => { @@ -43,11 +203,11 @@ describe('injectStore', () => { standalone: true, }) class MyCmp { - storeVal = injectStore(store, (state) => state.select) + storeVal = injectSelector(store, (state) => state.select) constructor() { effect(() => { - console.log(this.storeVal()) + this.storeVal() count++ }) } @@ -70,27 +230,159 @@ describe('injectStore', () => { const fixture = TestBed.createComponent(MyCmp) fixture.detectChanges() - const element = fixture.nativeElement - const debugElement = fixture.debugElement - - expect(element.textContent).toContain('Store: 0') + expect(fixture.nativeElement.textContent).toContain('Store: 0') expect(count).toEqual(1) - debugElement + fixture.debugElement .query(By.css('button#updateSelect')) .triggerEventHandler('click', null) + fixture.detectChanges() + expect(fixture.nativeElement.textContent).toContain('Store: 10') + expect(count).toEqual(2) + fixture.debugElement + .query(By.css('button#updateIgnored')) + .triggerEventHandler('click', null) fixture.detectChanges() - expect(element.textContent).toContain('Store: 10') + expect(fixture.nativeElement.textContent).toContain('Store: 10') expect(count).toEqual(2) + }) + + test('injectSelector allows specifying a custom equality function', () => { + const store = createStore({ + array: [ + { select: 0, ignore: 1 }, + { select: 0, ignore: 1 }, + ], + }) + let count = 0 + + @Component({ + template: ` +
+

{{ sum() }}

+ + +
+ `, + standalone: true, + }) + class MyCmp { + sum = injectSelector( + store, + (state) => + state.array + .map(({ ignore, ...rest }) => rest) + .reduce((total, item) => total + item.select, 0), + { + compare: (prev, next) => prev === next, + }, + ) + + constructor() { + effect(() => { + this.sum() + count++ + }) + } + + updateSelect() { + store.setState((v) => ({ + array: v.array.map((item) => ({ + ...item, + select: item.select + 5, + })), + })) + } + + updateIgnored() { + store.setState((v) => ({ + array: v.array.map((item) => ({ + ...item, + ignore: item.ignore + 1, + })), + })) + } + } + + const fixture = TestBed.createComponent(MyCmp) + fixture.detectChanges() + + expect(fixture.nativeElement.textContent).toContain('0') + expect(count).toBe(1) - debugElement + fixture.debugElement .query(By.css('button#updateIgnored')) .triggerEventHandler('click', null) + fixture.detectChanges() + expect(count).toBe(1) + fixture.debugElement + .query(By.css('button#updateSelect')) + .triggerEventHandler('click', null) fixture.detectChanges() - expect(element.textContent).toContain('Store: 10') - expect(count).toEqual(2) + expect(fixture.nativeElement.textContent).toContain('10') + expect(count).toBe(2) + }) + + test('injectSelector works with mounted derived stores', () => { + const store = createStore(0) + const derived = createStore(() => ({ val: store.state * 2 })) + + @Component({ + template: ` +
+

{{ derivedVal() }}

+ +
+ `, + standalone: true, + }) + class MyCmp { + derivedVal = injectSelector(derived, (state) => state.val) + + update() { + store.setState((prev) => prev + 1) + } + } + + const fixture = TestBed.createComponent(MyCmp) + fixture.detectChanges() + expect( + fixture.debugElement.query(By.css('p#derived')).nativeElement.textContent, + ).toContain('0') + + fixture.debugElement + .query(By.css('button#update')) + .triggerEventHandler('click', null) + fixture.detectChanges() + + expect( + fixture.debugElement.query(By.css('p#derived')).nativeElement.textContent, + ).toContain('2') + }) +}) + +describe('injectStore', () => { + test('is a compatibility alias for injectSelector', () => { + const store = createStore({ select: 0 }) + + @Component({ + template: `

Store: {{ storeVal() }}

`, + standalone: true, + }) + class MyCmp { + storeVal = injectStore(store, (state) => state.select) + } + + const fixture = TestBed.createComponent(MyCmp) + fixture.detectChanges() + + expect(fixture.nativeElement.textContent).toContain('Store: 0') }) }) @@ -108,13 +400,7 @@ describe('dataType', () => { standalone: true, }) class MyCmp { - storeVal = injectStore(store, (state) => state.date) - - constructor() { - effect(() => { - console.log(this.storeVal()) - }) - } + storeVal = injectSelector(store, (state) => state.date) updateDate() { store.setState((v) => ({ @@ -127,19 +413,188 @@ describe('dataType', () => { const fixture = TestBed.createComponent(MyCmp) fixture.detectChanges() - const debugElement = fixture.debugElement - expect( - debugElement.query(By.css('p#displayStoreVal')).nativeElement.textContent, + fixture.debugElement.query(By.css('p#displayStoreVal')).nativeElement + .textContent, ).toContain(new Date('2025-03-29T21:06:30.401Z')) - debugElement + fixture.debugElement .query(By.css('button#updateDate')) .triggerEventHandler('click', null) - fixture.detectChanges() expect( - debugElement.query(By.css('p#displayStoreVal')).nativeElement.textContent, + fixture.debugElement.query(By.css('p#displayStoreVal')).nativeElement + .textContent, ).toContain(new Date('2025-03-29T21:06:40.401Z')) }) }) + +describe('_injectStore', () => { + test('returns selected state and actions for stores with actions', () => { + const store = createStore({ count: 0 }, ({ setState }) => ({ + inc: () => setState((prev) => ({ count: prev.count + 1 })), + })) + + @Component({ + template: ` +
+

{{ count() }}

+ +
+ `, + standalone: true, + }) + class MyCmp { + private result = _injectStore(store, (state) => state.count) + count = this.result[0] + actions = this.result[1] + + inc() { + this.actions.inc() + } + } + + const fixture = TestBed.createComponent(MyCmp) + fixture.detectChanges() + + expect( + fixture.debugElement.query(By.css('p#count')).nativeElement.textContent, + ).toContain('0') + + fixture.debugElement + .query(By.css('button#inc')) + .triggerEventHandler('click', null) + fixture.detectChanges() + + expect( + fixture.debugElement.query(By.css('p#count')).nativeElement.textContent, + ).toContain('1') + }) + + test('returns selected state and setState for plain stores', () => { + const store = createStore(0) + + @Component({ + template: ` +
+

{{ value() }}

+ +
+ `, + standalone: true, + }) + class MyCmp { + private result = _injectStore(store, (state) => state) + value = this.result[0] + setState = this.result[1] + + inc() { + this.setState((prev) => prev + 1) + } + } + + const fixture = TestBed.createComponent(MyCmp) + fixture.detectChanges() + + expect( + fixture.debugElement.query(By.css('p#value')).nativeElement.textContent, + ).toContain('0') + + fixture.debugElement + .query(By.css('button#inc')) + .triggerEventHandler('click', null) + fixture.detectChanges() + + expect( + fixture.debugElement.query(By.css('p#value')).nativeElement.textContent, + ).toContain('1') + }) +}) + +describe('createStoreContext', () => { + test('provides and injects a typed store context', () => { + const { provideStoreContext, injectStoreContext } = createStoreContext<{ + countAtom: Atom + petStore: Store<{ cats: number; dogs: number }> + }>() + + @Component({ + template: ` +
+

{{ count() }}

+

{{ cats() }}

+ + +
+ `, + standalone: true, + providers: [ + provideStoreContext(() => ({ + countAtom: createAtom(10), + petStore: new Store({ cats: 2, dogs: 3 }), + })), + ], + }) + class MyCmp { + private ctx = injectStoreContext() + count = injectValue(this.ctx.countAtom) + cats = injectSelector(this.ctx.petStore, (s) => s.cats) + + inc() { + this.ctx.countAtom.set((prev) => prev + 1) + } + + addCat() { + this.ctx.petStore.setState((prev) => ({ + ...prev, + cats: prev.cats + 1, + })) + } + } + + const fixture = TestBed.createComponent(MyCmp) + fixture.detectChanges() + + expect( + fixture.debugElement.query(By.css('p#count')).nativeElement.textContent, + ).toContain('10') + expect( + fixture.debugElement.query(By.css('p#cats')).nativeElement.textContent, + ).toContain('2') + + fixture.debugElement + .query(By.css('button#inc')) + .triggerEventHandler('click', null) + fixture.detectChanges() + expect( + fixture.debugElement.query(By.css('p#count')).nativeElement.textContent, + ).toContain('11') + + fixture.debugElement + .query(By.css('button#addCat')) + .triggerEventHandler('click', null) + fixture.detectChanges() + expect( + fixture.debugElement.query(By.css('p#cats')).nativeElement.textContent, + ).toContain('3') + }) + + test('throws when injectStoreContext is called without a provider', () => { + const { injectStoreContext } = createStoreContext<{ + countAtom: Atom + }>() + + @Component({ + template: `

{{ count() }}

`, + standalone: true, + }) + class MyCmp { + private ctx = injectStoreContext() + count = injectValue(this.ctx.countAtom) + } + + expect(() => TestBed.createComponent(MyCmp)).toThrow( + /Missing StoreContext provider/, + ) + }) +}) diff --git a/packages/angular-store/tests/test.test-d.ts b/packages/angular-store/tests/test.test-d.ts index 430a75a7..8db1b503 100644 --- a/packages/angular-store/tests/test.test-d.ts +++ b/packages/angular-store/tests/test.test-d.ts @@ -1,16 +1,95 @@ import { expectTypeOf, test } from 'vitest' -import { createStore } from '@tanstack/store' -import { injectStore } from '../src' +import { createAtom, createStore } from '@tanstack/store' +import { + _injectStore, + createStoreContext, + injectAtom, + injectSelector, + injectStore, + injectValue, +} from '../src' import type { Signal } from '@angular/core' +import type { Atom, Store } from '@tanstack/store' +import type { WritableAtomSignal } from '../src' -test('injectStore works with derived state', () => { +test('injectSelector works with derived state', () => { const store = createStore(12) const derived = createStore(() => store.state * 2) - const val = injectStore(derived, (state) => { + const val = injectSelector(derived, (state) => { expectTypeOf(state).toEqualTypeOf() return state }) expectTypeOf(val).toEqualTypeOf>() }) + +test('injectValue infers value from mutable and readonly sources', () => { + const writableAtom = createAtom(12) + const readonlyAtom = createAtom(() => 24) + const writableStore = createStore(12) + const readonlyStore = createStore(() => 24) + + expectTypeOf(injectValue(writableAtom)).toEqualTypeOf>() + expectTypeOf(injectValue(readonlyAtom)).toEqualTypeOf>() + expectTypeOf(injectValue(writableStore)).toEqualTypeOf>() + expectTypeOf(injectValue(readonlyStore)).toEqualTypeOf>() +}) + +test('injectAtom returns a WritableAtomSignal', () => { + const writableAtom = createAtom(12) + const readonlyAtom = createAtom(() => 24) + + const atomSignal = injectAtom(writableAtom) + + expectTypeOf(atomSignal).toEqualTypeOf>() + expectTypeOf(atomSignal()).toEqualTypeOf() + expectTypeOf(atomSignal.set).toEqualTypeOf['set']>() + + // @ts-expect-error readonly atoms cannot be used with injectAtom + injectAtom(readonlyAtom) +}) + +test('injectStore matches injectSelector types for compatibility', () => { + const store = createStore(12) + const selectorValue = injectSelector(store, (state) => state) + const compatValue = injectStore(store, (state) => state) + + expectTypeOf(selectorValue).toEqualTypeOf>() + expectTypeOf(compatValue).toEqualTypeOf>() +}) + +test('createStoreContext preserves typed context shape', () => { + const { provideStoreContext, injectStoreContext } = createStoreContext<{ + countAtom: Atom + petStore: Store<{ cats: number }> + }>() + + expectTypeOf(provideStoreContext).toBeFunction() + expectTypeOf(injectStoreContext).toBeFunction() + + const ctx = injectStoreContext() + + expectTypeOf(ctx.countAtom).toEqualTypeOf>() + expectTypeOf(ctx.petStore).toEqualTypeOf>() +}) + +test('_injectStore returns actions for stores with actions', () => { + const store = createStore({ count: 0 }, ({ setState }) => ({ + inc: () => setState((prev) => ({ count: prev.count + 1 })), + })) + + const [selected, actions] = _injectStore(store, (state) => state.count) + + expectTypeOf(selected).toEqualTypeOf>() + expectTypeOf(actions.inc).toBeFunction() +}) + +test('_injectStore returns setState for plain stores', () => { + const store = createStore(0) + + const [selected, setState] = _injectStore(store, (state) => state) + + expectTypeOf(selected).toEqualTypeOf>() + expectTypeOf(setState).toEqualTypeOf['setState']>() +}) diff --git a/packages/preact-store/src/_useStore.ts b/packages/preact-store/src/_useStore.ts new file mode 100644 index 00000000..89be003a --- /dev/null +++ b/packages/preact-store/src/_useStore.ts @@ -0,0 +1,43 @@ +import { useMemo } from 'preact/hooks' +import { useSelector } from './useSelector' +import type { Store, StoreActionMap } from '@tanstack/store' +import type { UseSelectorOptions } from './useSelector' // eslint-disable-line no-duplicate-imports + +/** + * Experimental combined read+write hook for stores, mirroring useAtom's tuple + * pattern. + * + * Returns `[selected, actions]` when the store has an actions factory, or + * `[selected, setState]` for plain stores. + * + * @example + * ```tsx + * // Store with actions + * const [cats, { addCat }] = _useStore(petStore, (s) => s.cats) + * + * // Store without actions + * const [count, setState] = _useStore(plainStore, (s) => s) + * setState((prev) => prev + 1) + * ``` + */ +/* eslint-disable react-hooks/rules-of-hooks -- experimental API with underscore prefix */ +export function _useStore< + TState, + TActions extends StoreActionMap, + TSelected = NoInfer, +>( + store: Store, + selector: (state: NoInfer) => TSelected, + options?: UseSelectorOptions, +): [ + TSelected, + [TActions] extends [never] ? Store['setState'] : TActions, +] { + const selected = useSelector(store, selector, options) + const actionsOrSetState = useMemo( + () => (store.actions as StoreActionMap | undefined) ?? store.setState, + [store], + ) + + return [selected, actionsOrSetState] as any +} diff --git a/packages/preact-store/src/createStoreContext.tsx b/packages/preact-store/src/createStoreContext.tsx new file mode 100644 index 00000000..097cfcc7 --- /dev/null +++ b/packages/preact-store/src/createStoreContext.tsx @@ -0,0 +1,72 @@ +// eslint-disable-next-line import/consistent-type-specifier-style +import { type ComponentChildren, createContext } from 'preact' +import { useContext } from 'preact/hooks' + +/** + * Creates a typed Preact context for sharing a bundle of atoms and stores with + * a subtree. + * + * The returned `StoreProvider` only transports the provided object through + * Preact context. Consumers destructure the contextual atoms and stores, then + * compose them with the existing hooks like {@link useSelector}, + * {@link useValue}, {@link useSetValue}, and {@link useAtom}. + * + * The object shape is preserved exactly, so keyed atoms and stores remain fully + * typed when read back with `useStoreContext()`. + * + * @example + * ```tsx + * const { StoreProvider, useStoreContext } = createStoreContext<{ + * countAtom: Atom + * totalsStore: Store<{ count: number }> + * }>() + * + * function CountButton() { + * const { countAtom, totalsStore } = useStoreContext() + * const count = useValue(countAtom) + * const total = useSelector(totalsStore, (state) => state.count) + * + * return ( + * + * ) + * } + * ``` + * + * @throws When `useStoreContext()` is called outside the matching `StoreProvider`. + */ +export function createStoreContext() { + const Context = createContext(null) + Context.displayName = 'StoreContext' + + function StoreProvider({ + children, + value, + }: { + children?: ComponentChildren + value: TValue + }) { + return {children} + } + + function useStoreContext() { + const value = useContext(Context) + + if (value === null) { + throw new Error('Missing StoreProvider for StoreContext') + } + + return value + } + + return { + StoreProvider, + useStoreContext, + } +} diff --git a/packages/preact-store/src/index.ts b/packages/preact-store/src/index.ts index c4572691..4bcfb415 100644 --- a/packages/preact-store/src/index.ts +++ b/packages/preact-store/src/index.ts @@ -1,171 +1,12 @@ -import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks' -import type { Atom, ReadonlyAtom } from '@tanstack/store' - export * from '@tanstack/store' -type InternalStore = { - _value: any - _getSnapshot: () => any -} - -type StoreRef = { - _instance: InternalStore -} - -/** - * This is taken from https://github.com/preactjs/preact/blob/main/compat/src/hooks.js#L8-L54 - * which is taken from https://github.com/facebook/react/blob/main/packages/use-sync-external-store/src/useSyncExternalStoreShimClient.js#L84 - * on a high level this cuts out the warnings, ... and attempts a smaller implementation. - * This way we don't have to import preact/compat with side effects - */ -function useSyncExternalStore( - subscribe: (onStoreChange: () => void) => () => void, - getSnapshot: () => any, -) { - const value = getSnapshot() - - const [{ _instance }, forceUpdate] = useState({ - _instance: { _value: value, _getSnapshot: getSnapshot }, - }) - - useLayoutEffect(() => { - _instance._value = value - _instance._getSnapshot = getSnapshot - - if (didSnapshotChange(_instance)) { - forceUpdate({ _instance }) - } - }, [subscribe, value, getSnapshot]) - - useEffect(() => { - if (didSnapshotChange(_instance)) { - forceUpdate({ _instance }) - } - - return subscribe(() => { - if (didSnapshotChange(_instance)) { - forceUpdate({ _instance }) - } - }) - }, [subscribe]) - - return value -} - -function didSnapshotChange(inst: { - _getSnapshot: () => any - _value: any -}): boolean { - const latestGetSnapshot = inst._getSnapshot - const prevValue = inst._value - try { - const nextValue = latestGetSnapshot() - return !Object.is(prevValue, nextValue) - // eslint-disable-next-line no-unused-vars - } catch (_error) { - return true - } -} - -type EqualityFn = (objA: T, objB: T) => boolean -interface UseStoreOptions { - equal?: EqualityFn -} - -function useSyncExternalStoreWithSelector( - subscribe: (onStoreChange: () => void) => () => void, - getSnapshot: () => TSnapshot, - selector: (snapshot: TSnapshot) => TSelected, - isEqual: (a: TSelected, b: TSelected) => boolean, -): TSelected { - const selectedSnapshotRef = useRef() - - const getSelectedSnapshot = () => { - const snapshot = getSnapshot() - const selected = selector(snapshot) - - if ( - selectedSnapshotRef.current === undefined || - !isEqual(selectedSnapshotRef.current, selected) - ) { - selectedSnapshotRef.current = selected - } - - return selectedSnapshotRef.current - } - - return useSyncExternalStore(subscribe, getSelectedSnapshot) -} - -export function useStore>( - store: Atom | ReadonlyAtom, - selector: (state: NoInfer) => TSelected = (d) => d as any, - options: UseStoreOptions = {}, -): TSelected { - const equal = options.equal ?? shallow - const slice = useSyncExternalStoreWithSelector( - (onStoreChange) => store.subscribe(onStoreChange).unsubscribe, - () => store.get(), - selector, - equal, - ) - - return slice -} - -export function shallow(objA: T, objB: T) { - if (Object.is(objA, objB)) { - return true - } - - if ( - typeof objA !== 'object' || - objA === null || - typeof objB !== 'object' || - objB === null - ) { - return false - } - - if (objA instanceof Map && objB instanceof Map) { - if (objA.size !== objB.size) return false - for (const [k, v] of objA) { - if (!objB.has(k) || !Object.is(v, objB.get(k))) return false - } - return true - } - - if (objA instanceof Set && objB instanceof Set) { - if (objA.size !== objB.size) return false - for (const v of objA) { - if (!objB.has(v)) return false - } - return true - } - - if (objA instanceof Date && objB instanceof Date) { - if (objA.getTime() !== objB.getTime()) return false - return true - } - - const keysA = getOwnKeys(objA) - if (keysA.length !== getOwnKeys(objB).length) { - return false - } +export * from './createStoreContext' +export * from './useCreateAtom' +export * from './useCreateStore' - for (const key of keysA) { - if ( - !Object.prototype.hasOwnProperty.call(objB, key as string) || - !Object.is(objA[key as keyof T], objB[key as keyof T]) - ) { - return false - } - } - return true -} +export * from './useValue' +export * from './useSelector' -function getOwnKeys(obj: object): Array { - return (Object.keys(obj) as Array).concat( - Object.getOwnPropertySymbols(obj), - ) -} +export * from './useAtom' +export * from './useStore' // @deprecated in favor of useSelector +export * from './_useStore' diff --git a/packages/preact-store/src/useAtom.ts b/packages/preact-store/src/useAtom.ts new file mode 100644 index 00000000..7492d000 --- /dev/null +++ b/packages/preact-store/src/useAtom.ts @@ -0,0 +1,29 @@ +import { useValue } from './useValue' +import type { Atom } from '@tanstack/store' +import type { UseSelectorOptions } from './useSelector' + +/** + * Returns the current atom value together with a stable setter. + * + * Use this when a component needs to both read and update the same writable + * atom. + * + * @example + * ```tsx + * const [count, setCount] = useAtom(countAtom) + * + * return ( + * + * ) + * ``` + */ +export function useAtom( + atom: Atom, + options?: UseSelectorOptions, +): [TValue, Atom['set']] { + const value = useValue(atom, options) + + return [value, atom.set] +} diff --git a/packages/preact-store/src/useCreateAtom.ts b/packages/preact-store/src/useCreateAtom.ts new file mode 100644 index 00000000..08b3e2be --- /dev/null +++ b/packages/preact-store/src/useCreateAtom.ts @@ -0,0 +1,48 @@ +import { useState } from 'preact/hooks' +import { createAtom } from '@tanstack/store' +// eslint-disable-next-line no-duplicate-imports +import type { Atom, AtomOptions, ReadonlyAtom } from '@tanstack/store' + +/** + * Creates a stable atom instance for the lifetime of the component. + * + * Pass an initial value to create a writable atom, or a getter function to + * create a readonly derived atom. This mirrors {@link createAtom}, but only + * creates the atom once per component mount. + * + * @example + * ```tsx + * function Counter() { + * const countAtom = useCreateAtom(0) + * const [count, setCount] = useAtom(countAtom) + * + * return ( + * + * ) + * } + * ``` + */ +export function useCreateAtom( + getValue: (prev?: NoInfer) => T, + options?: AtomOptions, +): ReadonlyAtom +export function useCreateAtom( + initialValue: T, + options?: AtomOptions, +): Atom +export function useCreateAtom( + valueOrFn: T | ((prev?: T) => T), + options?: AtomOptions, +): Atom | ReadonlyAtom { + const [atom] = useState | ReadonlyAtom>(() => { + if (typeof valueOrFn === 'function') { + return createAtom(valueOrFn as (prev?: NoInfer) => T, options) + } + + return createAtom(valueOrFn, options) + }) + + return atom +} diff --git a/packages/preact-store/src/useCreateStore.ts b/packages/preact-store/src/useCreateStore.ts new file mode 100644 index 00000000..1f518238 --- /dev/null +++ b/packages/preact-store/src/useCreateStore.ts @@ -0,0 +1,65 @@ +import { useState } from 'preact/hooks' +import { createStore } from '@tanstack/store' +// eslint-disable-next-line no-duplicate-imports +import type { + ReadonlyStore, + Store, + StoreActionMap, + StoreActionsFactory, +} from '@tanstack/store' + +type NonFunction = T extends (...args: Array) => any ? never : T + +/** + * Creates a stable store instance for the lifetime of the component. + * + * Pass an initial value to create a writable store, or a getter function to + * create a readonly derived store. This mirrors {@link createStore}, but only + * creates the store once per component mount. + * + * @example + * ```tsx + * function Counter() { + * const counterStore = useCreateStore({ count: 0 }) + * const count = useSelector(counterStore, (state) => state.count) + * const setState = useSetValue(counterStore) + * + * return ( + * + * ) + * } + * ``` + */ +export function useCreateStore( + getValue: (prev?: NoInfer) => T, +): ReadonlyStore +export function useCreateStore(initialValue: T): Store +export function useCreateStore( + initialValue: NonFunction, + actions: StoreActionsFactory, +): Store +export function useCreateStore( + valueOrFn: T | ((prev?: T) => T), + actions?: StoreActionsFactory, +): Store | Store | ReadonlyStore { + const [store] = useState | Store | ReadonlyStore>( + () => { + if (typeof valueOrFn === 'function') { + return createStore(valueOrFn as (prev?: NoInfer) => T) + } + + if (actions) { + return createStore(valueOrFn as NonFunction, actions) + } + + return createStore(valueOrFn) + }, + ) + + return store +} diff --git a/packages/preact-store/src/useSelector.ts b/packages/preact-store/src/useSelector.ts new file mode 100644 index 00000000..fc833e25 --- /dev/null +++ b/packages/preact-store/src/useSelector.ts @@ -0,0 +1,150 @@ +import { + useCallback, + useEffect, + useLayoutEffect, + useRef, + useState, +} from 'preact/hooks' + +export interface UseSelectorOptions { + compare?: (a: TSelected, b: TSelected) => boolean +} + +type InternalStore = { + _value: any + _getSnapshot: () => any +} + +type StoreRef = { + _instance: InternalStore +} + +type SelectionSource = { + get: () => T + subscribe: (listener: (value: T) => void) => { + unsubscribe: () => void + } +} + +function defaultCompare(a: T, b: T) { + return a === b +} + +/** + * This is taken from https://github.com/preactjs/preact/blob/main/compat/src/hooks.js#L8-L54 + * which is taken from https://github.com/facebook/react/blob/main/packages/use-sync-external-store/src/useSyncExternalStoreShimClient.js#L84 + * on a high level this cuts out the warnings, ... and attempts a smaller implementation. + * This way we don't have to import preact/compat with side effects + */ +function useSyncExternalStore( + subscribe: (onStoreChange: () => void) => () => void, + getSnapshot: () => any, +) { + const value = getSnapshot() + + const [{ _instance }, forceUpdate] = useState({ + _instance: { _value: value, _getSnapshot: getSnapshot }, + }) + + useLayoutEffect(() => { + _instance._value = value + _instance._getSnapshot = getSnapshot + + if (didSnapshotChange(_instance)) { + forceUpdate({ _instance }) + } + }, [_instance, subscribe, value, getSnapshot]) + + useEffect(() => { + if (didSnapshotChange(_instance)) { + forceUpdate({ _instance }) + } + + return subscribe(() => { + if (didSnapshotChange(_instance)) { + forceUpdate({ _instance }) + } + }) + }, [_instance, subscribe]) + + return value +} + +function didSnapshotChange(inst: InternalStore): boolean { + const latestGetSnapshot = inst._getSnapshot + const prevValue = inst._value + try { + const nextValue = latestGetSnapshot() + return !Object.is(prevValue, nextValue) + // eslint-disable-next-line no-unused-vars + } catch (_error) { + return true + } +} + +function useSyncExternalStoreWithSelector( + subscribe: (onStoreChange: () => void) => () => void, + getSnapshot: () => TSnapshot, + selector: (snapshot: TSnapshot) => TSelected, + compare: (a: TSelected, b: TSelected) => boolean, +): TSelected { + const selectedSnapshotRef = useRef() + + const getSelectedSnapshot = () => { + const snapshot = getSnapshot() + const selected = selector(snapshot) + + if ( + selectedSnapshotRef.current === undefined || + !compare(selectedSnapshotRef.current, selected) + ) { + selectedSnapshotRef.current = selected + } + + return selectedSnapshotRef.current + } + + return useSyncExternalStore(subscribe, getSelectedSnapshot) +} + +/** + * Selects a slice of state from an atom or store and subscribes the component + * to that selection. + * + * This is the primary Preact read hook for TanStack Store. Use it when a + * component only needs part of a source value. + * + * @example + * ```tsx + * const count = useSelector(counterStore, (state) => state.count) + * ``` + * + * @example + * ```tsx + * const doubled = useSelector(countAtom, (value) => value * 2) + * ``` + */ +export function useSelector( + source: SelectionSource, + selector: (snapshot: TSource) => TSelected, + options?: UseSelectorOptions, +): TSelected { + const compare = options?.compare ?? defaultCompare + + const subscribe = useCallback( + (handleStoreChange: () => void) => { + const { unsubscribe } = source.subscribe(handleStoreChange) + return unsubscribe + }, + [source], + ) + + const getSnapshot = useCallback(() => source.get(), [source]) + + return useSyncExternalStoreWithSelector( + subscribe, + getSnapshot, + selector, + compare, + ) +} diff --git a/packages/preact-store/src/useStore.ts b/packages/preact-store/src/useStore.ts new file mode 100644 index 00000000..b08021a1 --- /dev/null +++ b/packages/preact-store/src/useStore.ts @@ -0,0 +1,22 @@ +import { useSelector } from './useSelector' + +/** + * Deprecated alias for {@link useSelector}. + * + * @example + * ```tsx + * const count = useStore(counterStore, (state) => state.count) + * ``` + * + * @deprecated Use `useSelector` instead. + */ +export const useStore = ( + source: { + get: () => TSource + subscribe: (listener: (value: TSource) => void) => { + unsubscribe: () => void + } + }, + selector: (snapshot: TSource) => TSelected, + compare?: (a: TSelected, b: TSelected) => boolean, +) => useSelector(source, selector, { compare }) diff --git a/packages/preact-store/src/useValue.ts b/packages/preact-store/src/useValue.ts new file mode 100644 index 00000000..a0e037e8 --- /dev/null +++ b/packages/preact-store/src/useValue.ts @@ -0,0 +1,31 @@ +import { useSelector } from './useSelector' +import type { Atom, ReadonlyAtom, ReadonlyStore, Store } from '@tanstack/store' +// eslint-disable-next-line no-duplicate-imports +import type { UseSelectorOptions } from './useSelector' + +/** + * Subscribes to an atom or store and returns its current value. + * + * This is the whole-value counterpart to {@link useSelector}. Use it when the + * component needs the entire current value from a source. + * + * @example + * ```tsx + * const count = useValue(countAtom) + * ``` + * + * @example + * ```tsx + * const state = useValue(counterStore) + * ``` + */ +export function useValue( + source: + | Atom + | ReadonlyAtom + | Store + | ReadonlyStore, + options?: UseSelectorOptions, +): TValue { + return useSelector(source, (value) => value, options) +} diff --git a/packages/preact-store/tests/index.test.tsx b/packages/preact-store/tests/index.test.tsx index 8012ddf9..2f66e036 100644 --- a/packages/preact-store/tests/index.test.tsx +++ b/packages/preact-store/tests/index.test.tsx @@ -1,20 +1,388 @@ -import { describe, expect, it, test, vi } from 'vitest' -import { render, waitFor } from '@testing-library/preact' +import { act, render, renderHook, waitFor } from '@testing-library/preact' import { userEvent } from '@testing-library/user-event' -import { createStore } from '@tanstack/store' -import { shallow, useStore } from '../src/index' +import { describe, expect, it, test, vi } from 'vitest' +import { createAtom, createStore } from '@tanstack/store' +import { + _useStore, + createStoreContext, + shallow, + useAtom, + useCreateAtom, + useCreateStore, + useSelector, + useStore, + useValue, +} from '../src/index' const user = userEvent.setup() -describe('useStore', () => { - it('allows us to select state using a selector', () => { +describe('atom hooks', () => { + it('useCreateAtom creates a stable atom instance across rerenders', () => { + const { result, rerender } = renderHook(() => useCreateAtom(0)) + const atom = result.current + + act(() => { + atom.set(1) + }) + + rerender() + + expect(result.current).toBe(atom) + expect(result.current.get()).toBe(1) + }) + + it('useValue reads mutable atom state and rerenders when updated', async () => { + const atom = createAtom(0) + + function Comp() { + const value = useValue(atom) + + return ( +
+

Value: {value}

+ +
+ ) + } + + const { getByText } = render() + + expect(getByText('Value: 0')).toBeInTheDocument() + + await user.click(getByText('Update')) + + await waitFor(() => expect(getByText('Value: 1')).toBeInTheDocument()) + }) + + it('useValue reads readonly atom state', async () => { + const countAtom = createAtom(1) + const doubledAtom = createAtom(() => countAtom.get() * 2) + + function Comp() { + const value = useValue(doubledAtom) + + return ( +
+

Value: {value}

+ +
+ ) + } + + const { getByText } = render() + + expect(getByText('Value: 2')).toBeInTheDocument() + + await user.click(getByText('Update')) + + await waitFor(() => expect(getByText('Value: 4')).toBeInTheDocument()) + }) + + it('useValue respects custom compare', async () => { + const atom = createAtom({ + select: 0, + ignored: 1, + }) + const renderSpy = vi.fn() + + function Comp() { + const value = useValue(atom, { + compare: (prev, next) => prev.select === next.select, + }) + renderSpy() + + return ( +
+

Renders: {renderSpy.mock.calls.length}

+

Value: {value.select}

+ + +
+ ) + } + + const { getByText } = render() + + expect(getByText('Renders: 1')).toBeInTheDocument() + expect(getByText('Value: 0')).toBeInTheDocument() + + await user.click(getByText('Update ignored')) + expect(getByText('Renders: 1')).toBeInTheDocument() + + await user.click(getByText('Update select')) + + await waitFor(() => expect(getByText('Value: 1')).toBeInTheDocument()) + expect(getByText('Renders: 2')).toBeInTheDocument() + }) + + it('useAtom returns the current value and setter', () => { + const atom = createAtom(0) + const { result } = renderHook(() => useAtom(atom)) + + expect(result.current[0]).toBe(0) + + act(() => { + result.current[1]((prev) => prev + 5) + }) + + expect(result.current[0]).toBe(5) + }) +}) + +describe('store contexts', () => { + it('provides bundled writable atoms and stores', async () => { + const countAtom = createAtom(0) + const totalStore = createStore({ count: 0 }) + const { StoreProvider, useStoreContext } = createStoreContext<{ + countAtom: typeof countAtom + totalStore: typeof totalStore + }>() + + function Comp() { + const { countAtom: currentAtom, totalStore: currentStore } = + useStoreContext() + const value = useValue(currentAtom) + const total = useSelector(currentStore, (state) => state.count) + + return ( +
+

Value: {value}

+

Total: {total}

+ + +
+ ) + } + + const { getByText } = render( + + + , + ) + + expect(getByText('Value: 0')).toBeInTheDocument() + expect(getByText('Total: 0')).toBeInTheDocument() + + await user.click(getByText('Update')) + await user.click(getByText('Update total')) + + await waitFor(() => expect(getByText('Value: 1')).toBeInTheDocument()) + await waitFor(() => expect(getByText('Total: 1')).toBeInTheDocument()) + }) + + it('supports readonly atoms and stores in the same context', async () => { + const baseAtom = createAtom(1) + const readonlyAtom = createAtom(() => baseAtom.get() * 2) + const baseStore = createStore(1) + const readonlyStore = createStore(() => ({ value: baseStore.state * 2 })) + const { StoreProvider, useStoreContext } = createStoreContext<{ + readonlyAtom: typeof readonlyAtom + readonlyStore: typeof readonlyStore + }>() + + function Comp() { + const { readonlyAtom: currentAtom, readonlyStore: currentStore } = + useStoreContext() + const atomValue = useValue(currentAtom) + const storeValue = useSelector(currentStore, (state) => state.value) + + return ( +
+

Atom: {atomValue}

+

Store: {storeValue}

+
+ ) + } + + const { getByText } = render( + + + , + ) + + expect(getByText('Atom: 2')).toBeInTheDocument() + expect(getByText('Store: 2')).toBeInTheDocument() + + act(() => { + baseAtom.set((prev) => prev + 1) + baseStore.setState((prev) => prev + 1) + }) + + await waitFor(() => expect(getByText('Atom: 4')).toBeInTheDocument()) + await waitFor(() => expect(getByText('Store: 4')).toBeInTheDocument()) + }) + + it('works with useAtom against contextual atoms', async () => { + const countAtom = createAtom(0) + const { StoreProvider, useStoreContext } = createStoreContext<{ + countAtom: typeof countAtom + }>() + + function Comp() { + const { countAtom: atom } = useStoreContext() + const [value, setValue] = useAtom(atom) + + return ( +
+

Value: {value}

+ +
+ ) + } + + const { getByText } = render( + + + , + ) + + await user.click(getByText('Add 5')) + + await waitFor(() => expect(getByText('Value: 5')).toBeInTheDocument()) + }) + + it('throws a clear error when a store provider is missing', () => { + const { useStoreContext } = createStoreContext<{ countAtom: number }>() + + function Comp() { + useStoreContext() + return null + } + + expect(() => render()).toThrowError( + 'Missing StoreProvider for StoreContext', + ) + }) + + it('nested providers override parent values', async () => { + const outerAtom = createAtom(1) + const innerAtom = createAtom(5) + const { StoreProvider, useStoreContext } = createStoreContext<{ + countAtom: typeof outerAtom + }>() + + function Value() { + const { countAtom: atom } = useStoreContext() + const value = useValue(atom) + + return

Value: {value}

+ } + + const { getAllByText } = render( + + + + + + , + ) + + expect(getAllByText(/Value:/).map((node) => node.textContent)).toEqual([ + 'Value: 1', + 'Value: 5', + ]) + + act(() => { + innerAtom.set(7) + }) + + await waitFor(() => + expect(getAllByText(/Value:/).map((node) => node.textContent)).toEqual([ + 'Value: 1', + 'Value: 7', + ]), + ) + }) +}) + +describe('store hooks', () => { + it('useCreateStore creates a stable store instance across rerenders', () => { + const { result, rerender } = renderHook(() => useCreateStore(0)) + const store = result.current + + act(() => { + store.setState((prev) => prev + 1) + }) + + rerender() + + expect(result.current).toBe(store) + expect(result.current.state).toBe(1) + }) + + it('useCreateStore supports actions and keeps them stable', () => { + const { result, rerender } = renderHook(() => + useCreateStore({ count: 0 }, ({ get, setState }) => ({ + inc: () => setState((prev) => ({ count: prev.count + 1 })), + current: () => get().count, + })), + ) + const store = result.current + const actions = store.actions + + act(() => { + store.actions.inc() + }) + + rerender() + + expect(result.current).toBe(store) + expect(result.current.actions).toBe(actions) + expect(result.current.actions.current()).toBe(1) + }) + + it('useSelector allows us to select state using a selector', () => { const store = createStore({ select: 0, ignored: 1, }) function Comp() { - const storeVal = useStore(store, (state) => state.select) + const storeVal = useSelector(store, (state) => state.select) return

Store: {storeVal}

} @@ -23,8 +391,40 @@ describe('useStore', () => { expect(getByText('Store: 0')).toBeInTheDocument() }) - // This should ideally test the custom uSES hook - it('only triggers a re-render when selector state is updated', async () => { + it('useValue reads writable and readonly store state', async () => { + const baseStore = createStore(1) + const readonlyStore = createStore(() => ({ value: baseStore.state * 2 })) + + function Comp() { + const value = useValue(baseStore) + const readonlyValue = useValue(readonlyStore) + + return ( +
+

Value: {value}

+

Readonly: {readonlyValue.value}

+ +
+ ) + } + + const { getByText } = render() + + expect(getByText('Value: 1')).toBeInTheDocument() + expect(getByText('Readonly: 2')).toBeInTheDocument() + + await user.click(getByText('Update')) + + await waitFor(() => expect(getByText('Value: 2')).toBeInTheDocument()) + await waitFor(() => expect(getByText('Readonly: 4')).toBeInTheDocument()) + }) + + it('useSelector only triggers a re-render when selector state is updated', async () => { const store = createStore({ select: 0, ignored: 1, @@ -32,7 +432,7 @@ describe('useStore', () => { const renderSpy = vi.fn() function Comp() { - const storeVal = useStore(store, (state) => state.select) + const storeVal = useSelector(store, (state) => state.select) renderSpy() return ( @@ -78,7 +478,7 @@ describe('useStore', () => { expect(getByText('Number rendered: 2')).toBeInTheDocument() }) - it('allow specifying custom equality function', async () => { + it('useSelector allows specifying a custom equality function', async () => { const store = createStore({ array: [ { select: 0, ignore: 1 }, @@ -92,10 +492,10 @@ describe('useStore', () => { const renderSpy = vi.fn() function Comp() { - const storeVal = useStore( + const storeVal = useSelector( store, (state) => state.array.map(({ ignore, ...rest }) => rest), - { equal: deepEqual }, + { compare: deepEqual }, ) renderSpy() @@ -150,13 +550,12 @@ describe('useStore', () => { expect(getByText('Number rendered: 2')).toBeInTheDocument() }) - it('works with mounted derived stores', async () => { + it('useSelector works with mounted derived stores', async () => { const store = createStore(0) - - const derived = createStore(() => store.state * 2) + const derived = createStore(() => ({ val: store.state * 2 })) function Comp() { - const derivedVal = useStore(derived, (state) => state) + const derivedVal = useSelector(derived, (state) => state.val) return (
@@ -177,6 +576,113 @@ describe('useStore', () => { }) }) +describe('useStore', () => { + it('is a compatibility alias for useSelector', async () => { + const store = createStore(0) + + function Comp() { + const value = useStore(store, (state) => state) + + return ( +
+

Value: {value}

+ +
+ ) + } + + const { getByText } = render() + + expect(getByText('Value: 0')).toBeInTheDocument() + + await user.click(getByText('Update')) + + await waitFor(() => expect(getByText('Value: 1')).toBeInTheDocument()) + }) + + it('supports atom sources through the deprecated alias', async () => { + const atom = createAtom(0) + + function Comp() { + const value = useStore(atom, (state) => state) + + return ( +
+

Value: {value}

+ +
+ ) + } + + const { getByText } = render() + + expect(getByText('Value: 0')).toBeInTheDocument() + + await user.click(getByText('Update')) + + await waitFor(() => expect(getByText('Value: 1')).toBeInTheDocument()) + }) +}) + +describe('_useStore', () => { + it('returns selected state and actions for stores with actions', async () => { + const store = createStore({ count: 0 }, ({ setState }) => ({ + inc: () => setState((prev) => ({ count: prev.count + 1 })), + })) + + function Comp() { + const [count, { inc }] = _useStore(store, (state) => state.count) + + return ( +
+

Count: {count}

+ +
+ ) + } + + const { getByText } = render() + expect(getByText('Count: 0')).toBeInTheDocument() + + await user.click(getByText('Inc')) + + await waitFor(() => expect(getByText('Count: 1')).toBeInTheDocument()) + }) + + it('returns selected state and setState for plain stores', async () => { + const store = createStore(0) + + function Comp() { + const [value, setState] = _useStore(store, (state) => state) + + return ( +
+

Value: {value}

+ +
+ ) + } + + const { getByText } = render() + expect(getByText('Value: 0')).toBeInTheDocument() + + await user.click(getByText('Inc')) + + await waitFor(() => expect(getByText('Value: 1')).toBeInTheDocument()) + }) +}) + describe('shallow', () => { test('should return true for shallowly equal objects', () => { const objA = { a: 1, b: 'hello' } diff --git a/packages/preact-store/tests/test.test-d.ts b/packages/preact-store/tests/test.test-d.ts index 3ce5bf4d..95b1c310 100644 --- a/packages/preact-store/tests/test.test-d.ts +++ b/packages/preact-store/tests/test.test-d.ts @@ -1,15 +1,179 @@ import { expectTypeOf, test } from 'vitest' -import { createStore } from '@tanstack/store' -import { useStore } from '../src' +import { createAtom, createStore } from '@tanstack/store' +import { + _useStore, + createStoreContext, + useAtom, + useCreateAtom, + useCreateStore, + useSelector, + useStore, + useValue, +} from '../src' +// eslint-disable-next-line no-duplicate-imports +import type { Atom, ReadonlyStore, Store } from '@tanstack/store' -test('useStore works with derived state', () => { - const store = createStore(12) - const derived = createStore(() => store.state * 2) +test('useCreateAtom returns a writable atom for initial values', () => { + const atom = useCreateAtom(12) - const val = useStore(derived, (state) => { - expectTypeOf(state).toEqualTypeOf() - return state + expectTypeOf(atom.get()).toExtend() + expectTypeOf(atom.set).toBeFunction() +}) + +test('useCreateAtom returns a readonly atom for derived values', () => { + const atom = useCreateAtom(() => 12, { + compare: (prev, next) => prev === next, + }) + + expectTypeOf(atom.get()).toExtend() + expectTypeOf(atom).not.toHaveProperty('set') +}) + +test('useValue infers value from mutable and readonly atoms', () => { + const writableAtom = createAtom(12) + const readonlyAtom = createAtom(() => 24) + const writableStore = createStore(12) + const readonlyStore = createStore(() => 24) + + expectTypeOf(useValue(writableAtom)).toExtend() + expectTypeOf(useValue(readonlyAtom)).toExtend() + expectTypeOf(useValue(writableStore)).toExtend() + expectTypeOf(useValue(readonlyStore)).toExtend() + expectTypeOf( + useValue(writableAtom, { + compare: (prev, next) => prev === next, + }), + ).toExtend() +}) + +test('useAtom only accepts writable atoms', () => { + const writableAtom = createAtom(12) + const readonlyAtom = createAtom(() => 24) + + const [value, setValue] = useAtom(writableAtom) + const [valueWithOptions] = useAtom(writableAtom, { + compare: (prev, next) => prev === next, }) - expectTypeOf(val).toEqualTypeOf() + expectTypeOf(value).toExtend() + expectTypeOf(valueWithOptions).toExtend() + expectTypeOf(setValue).toBeFunction() + // @ts-expect-error readonly atoms cannot be used with useAtom + useAtom(readonlyAtom) +}) + +test('useCreateStore returns writable and readonly store types', () => { + const writableStore = useCreateStore(12) + const writableStoreWithActions = useCreateStore( + { count: 0 }, + ({ setState }) => ({ + inc: () => setState((prev) => ({ count: prev.count + 1 })), + }), + ) + const readonlyStore = useCreateStore(() => 24) + + expectTypeOf(writableStore.state).toExtend() + expectTypeOf(writableStore.setState).toBeFunction() + expectTypeOf(writableStoreWithActions.state).toMatchObjectType<{ + count: number + }>() + expectTypeOf(writableStoreWithActions.actions.inc).toBeFunction() + expectTypeOf(readonlyStore.state).toExtend() + expectTypeOf(readonlyStore).not.toHaveProperty('setState') + + useCreateStore({ count: 0 }, () => ({ + // @ts-expect-error actions must be functions + asdf: 123, + inc: () => {}, + })) +}) + +test('useSelector infers state and selected types for stores', () => { + const baseStore = createStore(12) + const derivedStore = createStore(() => { + return { val: baseStore.state * 2 } + }) + + const val = useSelector(derivedStore, (state) => { + expectTypeOf(state).toMatchObjectType<{ val: number }>() + return state.val + }) + const valWithOptions = useSelector(derivedStore, (state) => state.val, { + compare: (prev, next) => prev === next, + }) + + expectTypeOf(val).toExtend() + expectTypeOf(valWithOptions).toExtend() +}) + +test('useSelector infers state and selected types for atoms', () => { + const atom = createAtom({ val: 12 }) + + const val = useSelector(atom, (state) => { + expectTypeOf(state).toMatchObjectType<{ val: number }>() + return state.val + }) + + expectTypeOf(val).toExtend() +}) + +test('useStore matches useSelector types for compatibility', () => { + const baseStore = createStore(12) + const derivedStore = createStore(() => { + return { val: baseStore.state * 2 } + }) + + const selectorValue = useSelector(derivedStore, (state) => state.val) + const compatValue = useStore( + derivedStore, + (state) => state.val, + (prev, next) => prev === next, + ) + + expectTypeOf(selectorValue).toExtend() + expectTypeOf(compatValue).toExtend() +}) + +test('createStoreContext preserves keyed atom and store types', () => { + const countAtom = createAtom(12) + const readonlySource = createStore(() => ({ value: 24 })) + const storeFactory = createStoreContext<{ + countAtom: typeof countAtom + readonlyStore: typeof readonlySource + }>() + const contextValue = storeFactory.useStoreContext() + + expectTypeOf(contextValue.countAtom).toExtend>() + expectTypeOf(contextValue.countAtom.set).toBeFunction() + + const [value, setValue] = useAtom(contextValue.countAtom) + expectTypeOf(value).toExtend() + expectTypeOf(setValue).toBeFunction() + + const readonlyStore = contextValue.readonlyStore + expectTypeOf(readonlyStore).toExtend>() + expectTypeOf(readonlyStore).not.toHaveProperty('setState') + + const selected = useSelector(readonlyStore, (state) => state.value) + expectTypeOf(selected).toExtend() +}) + +test('_useStore returns actions for stores with actions', () => { + const store = createStore({ count: 0 }, ({ setState }) => ({ + inc: () => setState((prev) => ({ count: prev.count + 1 })), + })) + + const [selected, actions] = _useStore(store, (state) => state.count) + + expectTypeOf(selected).toExtend() + expectTypeOf(actions.inc).toBeFunction() +}) + +test('_useStore returns setState for plain stores', () => { + const store = createStore(0) + + const [selected, setState] = _useStore(store, (state) => state) + + expectTypeOf(selected).toExtend() + expectTypeOf(setState).toEqualTypeOf['setState']>() }) diff --git a/packages/react-store/src/_useStore.ts b/packages/react-store/src/_useStore.ts new file mode 100644 index 00000000..30fdd927 --- /dev/null +++ b/packages/react-store/src/_useStore.ts @@ -0,0 +1,43 @@ +import { useMemo } from 'react' +import { useSelector } from './useSelector' +import type { Store, StoreActionMap } from '@tanstack/store' +import type { UseSelectorOptions } from './useSelector' + +/** + * Experimental combined read+write hook for stores, mirroring useAtom's tuple + * pattern. + * + * Returns `[selected, actions]` when the store has an actions factory, or + * `[selected, setState]` for plain stores. + * + * @example + * ```tsx + * // Store with actions + * const [cats, { addCat }] = _useStore(petStore, (s) => s.cats) + * + * // Store without actions + * const [count, setState] = _useStore(plainStore, (s) => s) + * setState((prev) => prev + 1) + * ``` + */ +/* eslint-disable react-hooks/rules-of-hooks, @eslint-react/rules-of-hooks -- experimental API with underscore prefix */ +export function _useStore< + TState, + TActions extends StoreActionMap, + TSelected = NoInfer, +>( + store: Store, + selector: (state: NoInfer) => TSelected, + options?: UseSelectorOptions, +): [ + TSelected, + [TActions] extends [never] ? Store['setState'] : TActions, +] { + const selected = useSelector(store, selector, options) + const actionsOrSetState = useMemo( + () => (store.actions as StoreActionMap | undefined) ?? store.setState, + [store], + ) + + return [selected, actionsOrSetState] as any +} diff --git a/packages/react-store/src/index.ts b/packages/react-store/src/index.ts index 0716abab..4bcfb415 100644 --- a/packages/react-store/src/index.ts +++ b/packages/react-store/src/index.ts @@ -4,14 +4,9 @@ export * from './createStoreContext' export * from './useCreateAtom' export * from './useCreateStore' -export * from './useSetValue' -export * from './useStoreActions' - export * from './useValue' export * from './useSelector' export * from './useAtom' export * from './useStore' // @deprecated in favor of useSelector - -// comparators -export * from './shallow' +export * from './_useStore' diff --git a/packages/react-store/src/useAtom.ts b/packages/react-store/src/useAtom.ts index 2bb41d4b..862be2dc 100644 --- a/packages/react-store/src/useAtom.ts +++ b/packages/react-store/src/useAtom.ts @@ -1,4 +1,3 @@ -import { useSetValue } from './useSetValue' import { useValue } from './useValue' import type { Atom } from '@tanstack/store' import type { UseSelectorOptions } from './useSelector' @@ -19,7 +18,6 @@ export function useAtom( options?: UseSelectorOptions, ): [TValue, Atom['set']] { const value = useValue(atom, options) - const setValue = useSetValue(atom) - return [value, setValue] + return [value, atom.set] } diff --git a/packages/react-store/src/useSetValue.ts b/packages/react-store/src/useSetValue.ts deleted file mode 100644 index 87d99893..00000000 --- a/packages/react-store/src/useSetValue.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { useCallback } from 'react' -import type { Atom, Store, StoreActionMap } from '@tanstack/store' - -/** - * Returns a stable setter for a writable atom or store. - * - * Writable atoms preserve their native `set` contract, supporting both direct - * values and updater functions. Writable stores preserve their native - * `setState` contract, supporting updater functions. - * - * @example - * ```tsx - * const setCount = useSetValue(countAtom) - * setCount((prev) => prev + 1) - * ``` - * - * @example - * ```tsx - * const setState = useSetValue(appStore) - * setState((prev) => ({ ...prev, count: prev.count + 1 })) - * ``` - */ -export function useSetValue(source: Atom): Atom['set'] -export function useSetValue( - source: Store, -): Store['setState'] -export function useSetValue( - source: Atom | Store, -) { - return useCallback['set'] | Store['setState']>( - (valueOrUpdater: TValue | ((prevVal: TValue) => TValue)) => { - if ('setState' in source) { - source.setState(valueOrUpdater as (prevVal: TValue) => TValue) - } else { - if (typeof valueOrUpdater === 'function') { - source.set(valueOrUpdater as (prevVal: TValue) => TValue) - } else { - source.set(valueOrUpdater) - } - } - }, - [source], - ) -} diff --git a/packages/react-store/src/useStore.ts b/packages/react-store/src/useStore.ts index 7257eac1..b08021a1 100644 --- a/packages/react-store/src/useStore.ts +++ b/packages/react-store/src/useStore.ts @@ -3,6 +3,11 @@ import { useSelector } from './useSelector' /** * Deprecated alias for {@link useSelector}. * + * @example + * ```tsx + * const count = useStore(counterStore, (state) => state.count) + * ``` + * * @deprecated Use `useSelector` instead. */ export const useStore = ( diff --git a/packages/react-store/src/useStoreActions.ts b/packages/react-store/src/useStoreActions.ts deleted file mode 100644 index 16de6c94..00000000 --- a/packages/react-store/src/useStoreActions.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { useMemo } from 'react' -import type { Store, StoreActionMap } from '@tanstack/store' - -/** - * Returns the stable actions bag from a writable store created with actions. - * - * Use this when a component only needs to call store actions and should not - * subscribe to state changes. - * - * @example - * ```tsx - * const actions = useStoreActions(counterStore) - * actions.inc() - * ``` - */ -export function useStoreActions( - store: Store, -): TActions { - return useMemo(() => store.actions, [store]) -} diff --git a/packages/react-store/tests/index.test.tsx b/packages/react-store/tests/index.test.tsx index 9193e3c0..2beb066d 100644 --- a/packages/react-store/tests/index.test.tsx +++ b/packages/react-store/tests/index.test.tsx @@ -3,15 +3,14 @@ import { userEvent } from '@testing-library/user-event' import { describe, expect, it, test, vi } from 'vitest' import { createAtom, createStore } from '@tanstack/store' import { + _useStore, createStoreContext, shallow, useAtom, useCreateAtom, useCreateStore, useSelector, - useSetValue, useStore, - useStoreActions, useValue, } from '../src/index' @@ -143,25 +142,6 @@ describe('atom hooks', () => { expect(getByText('Renders: 2')).toBeInTheDocument() }) - it('useSetValue updates atoms by value and updater and stays stable', () => { - const atom = createAtom(0) - const { result, rerender } = renderHook(() => useSetValue(atom)) - const setAtom = result.current - - act(() => { - result.current(1) - }) - expect(atom.get()).toBe(1) - - rerender() - expect(result.current).toBe(setAtom) - - act(() => { - result.current((prev) => prev + 1) - }) - expect(atom.get()).toBe(2) - }) - it('useAtom returns the current value and setter', () => { const atom = createAtom(0) const { result } = renderHook(() => useAtom(atom)) @@ -174,25 +154,6 @@ describe('atom hooks', () => { expect(result.current[0]).toBe(5) }) - - it('useSetValue updates stores by updater and stays stable', () => { - const store = createStore(0) - const { result, rerender } = renderHook(() => useSetValue(store)) - const setStore = result.current - - act(() => { - result.current((prev) => prev + 1) - }) - expect(store.state).toBe(1) - - rerender() - expect(result.current).toBe(setStore) - - act(() => { - result.current((prev) => prev + 1) - }) - expect(store.state).toBe(2) - }) }) describe('store contexts', () => { @@ -208,21 +169,22 @@ describe('store contexts', () => { const { countAtom: currentAtom, totalStore: currentStore } = useStoreContext() const value = useValue(currentAtom) - const setValue = useSetValue(currentAtom) const total = useSelector(currentStore, (state) => state.count) - const setTotal = useSetValue(currentStore) return (

Value: {value}

Total: {total}

- +
+ ) + } + + const { getByText } = render() + expect(getByText('Count: 0')).toBeInTheDocument() + + await user.click(getByText('Inc')) + + await waitFor(() => expect(getByText('Count: 1')).toBeInTheDocument()) + }) + + it('returns selected state and setState for plain stores', async () => { + const store = createStore(0) + + function Comp() { + const [value, setState] = _useStore(store, (state) => state) + + return ( +
+

Value: {value}

+ +
+ ) + } + + const { getByText } = render() + expect(getByText('Value: 0')).toBeInTheDocument() + + await user.click(getByText('Inc')) + + await waitFor(() => expect(getByText('Value: 1')).toBeInTheDocument()) + }) +}) + describe('shallow', () => { test('should return true for shallowly equal objects', () => { const objA = { a: 1, b: 'hello' } diff --git a/packages/react-store/tests/test.test-d.ts b/packages/react-store/tests/test.test-d.ts index 2aa4ab20..55b0d1b6 100644 --- a/packages/react-store/tests/test.test-d.ts +++ b/packages/react-store/tests/test.test-d.ts @@ -1,17 +1,16 @@ import { expectTypeOf, test } from 'vitest' import { createAtom, createStore } from '@tanstack/store' import { + _useStore, createStoreContext, useAtom, useCreateAtom, useCreateStore, useSelector, - useSetValue, useStore, - useStoreActions, useValue, } from '../src' -import type { Atom, ReadonlyStore } from '@tanstack/store' +import type { Atom, ReadonlyStore, Store } from '@tanstack/store' test('useCreateAtom returns a writable atom for initial values', () => { const atom = useCreateAtom(12) @@ -46,22 +45,6 @@ test('useValue infers value from mutable and readonly atoms', () => { ).toExtend() }) -test('useSetValue preserves native atom and store setter contracts', () => { - const writableAtom = createAtom(12) - const readonlyAtom = createAtom(() => 24) - const writableStore = createStore(12) - const readonlyStore = createStore(() => 24) - - expectTypeOf(useSetValue(writableAtom)).toEqualTypeOf['set']>() - expectTypeOf(useSetValue(writableStore)).toEqualTypeOf< - typeof writableStore.setState - >() - // @ts-expect-error readonly atoms cannot be set - useSetValue(readonlyAtom) - // @ts-expect-error readonly stores cannot be set - useSetValue(readonlyStore) -}) - test('useAtom only accepts writable atoms', () => { const writableAtom = createAtom(12) const readonlyAtom = createAtom(() => 24) @@ -80,9 +63,12 @@ test('useAtom only accepts writable atoms', () => { test('useCreateStore returns writable and readonly store types', () => { const writableStore = useCreateStore(12) - const writableStoreWithActions = useCreateStore({ count: 0 }, ({ set }) => ({ - inc: () => set((prev) => ({ count: prev.count + 1 })), - })) + const writableStoreWithActions = useCreateStore( + { count: 0 }, + ({ setState }) => ({ + inc: () => setState((prev) => ({ count: prev.count + 1 })), + }), + ) const readonlyStore = useCreateStore(() => 24) expectTypeOf(writableStore.state).toExtend() @@ -147,25 +133,6 @@ test('useStore matches useSelector types for compatibility', () => { expectTypeOf(compatValue).toExtend() }) -test('useStoreActions infers the action bag from writable stores', () => { - const store = createStore({ count: 0 }, ({ get, set }) => ({ - inc: () => set((prev) => ({ count: prev.count + 1 })), - current: () => get().count, - })) - - const actions = useStoreActions(store) - - expectTypeOf(actions.inc).toBeFunction() - expectTypeOf(actions.current()).toExtend() - - const plainStore = createStore(12) - expectTypeOf(useStoreActions(plainStore)).toEqualTypeOf() - - const readonlyStore = createStore(() => 24) - // @ts-expect-error readonly stores do not expose actions - useStoreActions(readonlyStore) -}) - test('createStoreContext preserves keyed atom and store types', () => { const countAtom = createAtom(12) const readonlySource = createStore(() => ({ value: 24 })) @@ -177,7 +144,6 @@ test('createStoreContext preserves keyed atom and store types', () => { expectTypeOf(contextValue.countAtom).toExtend>() expectTypeOf(contextValue.countAtom.set).toBeFunction() - expectTypeOf(useSetValue(contextValue.countAtom)).toBeFunction() const [value, setValue] = useAtom(contextValue.countAtom) expectTypeOf(value).toExtend() @@ -190,3 +156,23 @@ test('createStoreContext preserves keyed atom and store types', () => { const selected = useSelector(readonlyStore, (state) => state.value) expectTypeOf(selected).toExtend() }) + +test('_useStore returns actions for stores with actions', () => { + const store = createStore({ count: 0 }, ({ setState }) => ({ + inc: () => setState((prev) => ({ count: prev.count + 1 })), + })) + + const [selected, actions] = _useStore(store, (state) => state.count) + + expectTypeOf(selected).toExtend() + expectTypeOf(actions.inc).toBeFunction() +}) + +test('_useStore returns setState for plain stores', () => { + const store = createStore(0) + + const [selected, setState] = _useStore(store, (state) => state) + + expectTypeOf(selected).toExtend() + expectTypeOf(setState).toEqualTypeOf['setState']>() +}) diff --git a/packages/solid-store/src/_useStore.ts b/packages/solid-store/src/_useStore.ts new file mode 100644 index 00000000..4b2ba8b4 --- /dev/null +++ b/packages/solid-store/src/_useStore.ts @@ -0,0 +1,40 @@ +import { useSelector } from './useSelector' +import type { Accessor } from 'solid-js' +import type { Store, StoreActionMap } from '@tanstack/store' +import type { UseSelectorOptions } from './useSelector' + +/** + * Experimental combined read+write hook for stores, mirroring useAtom's tuple + * pattern. + * + * Returns `[selected, actions]` when the store has an actions factory, or + * `[selected, setState]` for plain stores. + * + * @example + * ```tsx + * // Store with actions + * const [cats, { addCat }] = _useStore(petStore, (s) => s.cats) + * + * // Store without actions + * const [count, setState] = _useStore(plainStore, (s) => s) + * setState((prev) => prev + 1) + * ``` + */ +export function _useStore< + TState, + TActions extends StoreActionMap, + TSelected = NoInfer, +>( + store: Store, + selector: (state: NoInfer) => TSelected, + options?: UseSelectorOptions, +): [ + Accessor, + [TActions] extends [never] ? Store['setState'] : TActions, +] { + const selected = useSelector(store, selector, options) + const actionsOrSetState = + (store.actions as StoreActionMap | undefined) ?? store.setState + + return [selected, actionsOrSetState] as any +} diff --git a/packages/solid-store/src/index.tsx b/packages/solid-store/src/index.tsx index 6f8cee03..ea10ac37 100644 --- a/packages/solid-store/src/index.tsx +++ b/packages/solid-store/src/index.tsx @@ -1,84 +1,8 @@ -import { createSignal, onCleanup } from 'solid-js' -import type { Accessor } from 'solid-js' -import type { Atom, ReadonlyAtom } from '@tanstack/store' - export * from '@tanstack/store' -type EqualityFn = (objA: T, objB: T) => boolean -interface UseStoreOptions { - equal?: EqualityFn -} - -export function useStore>( - store: Atom | ReadonlyAtom, - selector: (state: NoInfer) => TSelected = (d) => d as any, - options: UseStoreOptions = {}, -): Accessor { - const [signal, setSignal] = createSignal(selector(store.get())) - const equal = options.equal ?? shallow - - const unsub = store.subscribe((s) => { - const data = selector(s) - if (equal(signal(), data)) { - return - } - setSignal(() => data) - }).unsubscribe - - onCleanup(() => { - unsub() - }) - - return signal -} - -export function shallow(objA: T, objB: T) { - if (Object.is(objA, objB)) { - return true - } - - if ( - typeof objA !== 'object' || - objA === null || - typeof objB !== 'object' || - objB === null - ) { - return false - } - - if (objA instanceof Map && objB instanceof Map) { - if (objA.size !== objB.size) return false - for (const [k, v] of objA) { - if (!objB.has(k) || !Object.is(v, objB.get(k))) return false - } - return true - } - - if (objA instanceof Set && objB instanceof Set) { - if (objA.size !== objB.size) return false - for (const v of objA) { - if (!objB.has(v)) return false - } - return true - } - - if (objA instanceof Date && objB instanceof Date) { - if (objA.getTime() !== objB.getTime()) return false - return true - } - - const keysA = Object.keys(objA) - if (keysA.length !== Object.keys(objB).length) { - return false - } +export * from './useValue' +export * from './useSelector' - for (let i = 0; i < keysA.length; i++) { - if ( - !Object.prototype.hasOwnProperty.call(objB, keysA[i] as string) || - !Object.is(objA[keysA[i] as keyof T], objB[keysA[i] as keyof T]) - ) { - return false - } - } - return true -} +export * from './useAtom' +export * from './useStore' // @deprecated in favor of useSelector +export * from './_useStore' diff --git a/packages/solid-store/src/useAtom.ts b/packages/solid-store/src/useAtom.ts new file mode 100644 index 00000000..7703a16c --- /dev/null +++ b/packages/solid-store/src/useAtom.ts @@ -0,0 +1,30 @@ +import { useValue } from './useValue' +import type { Accessor } from 'solid-js' +import type { Atom } from '@tanstack/store' +import type { UseSelectorOptions } from './useSelector' + +/** + * Returns the current atom accessor together with a setter. + * + * Use this when a component needs to both read and update the same writable + * atom. + * + * @example + * ```tsx + * const [count, setCount] = useAtom(countAtom) + * + * return ( + * + * ) + * ``` + */ +export function useAtom( + atom: Atom, + options?: UseSelectorOptions, +): [Accessor, Atom['set']] { + const value = useValue(atom, options) + + return [value, atom.set] +} diff --git a/packages/solid-store/src/useSelector.ts b/packages/solid-store/src/useSelector.ts new file mode 100644 index 00000000..720c5579 --- /dev/null +++ b/packages/solid-store/src/useSelector.ts @@ -0,0 +1,57 @@ +import { createSignal, onCleanup } from 'solid-js' +import type { Accessor } from 'solid-js' + +export interface UseSelectorOptions { + compare?: (a: TSelected, b: TSelected) => boolean +} + +type SelectionSource = { + get: () => T + subscribe: (listener: (value: T) => void) => { + unsubscribe: () => void + } +} + +function defaultCompare(a: T, b: T) { + return a === b +} + +/** + * Selects a slice of state from an atom or store and subscribes the component + * to that selection. + * + * This is the primary Solid read hook for TanStack Store. It returns a Solid + * accessor so consumers can read the selected value reactively. + * + * @example + * ```tsx + * const count = useSelector(counterStore, (state) => state.count) + * + * return

{count()}

+ * ``` + * + * @example + * ```tsx + * const doubled = useSelector(countAtom, (value) => value * 2) + * ``` + */ +export function useSelector( + source: SelectionSource, + selector: (snapshot: TSource) => TSelected, + options?: UseSelectorOptions, +): Accessor { + const compare = options?.compare ?? defaultCompare + const [signal, setSignal] = createSignal(selector(source.get()), { + equals: compare, + }) + + const unsubscribe = source.subscribe((snapshot) => { + setSignal(() => selector(snapshot)) + }).unsubscribe + + onCleanup(() => { + unsubscribe() + }) + + return signal +} diff --git a/packages/solid-store/src/useStore.ts b/packages/solid-store/src/useStore.ts new file mode 100644 index 00000000..05149a97 --- /dev/null +++ b/packages/solid-store/src/useStore.ts @@ -0,0 +1,24 @@ +import { useSelector } from './useSelector' +import type { Accessor } from 'solid-js' + +/** + * Deprecated alias for {@link useSelector}. + * + * @example + * ```tsx + * const count = useStore(counterStore, (state) => state.count) + * ``` + * + * @deprecated Use `useSelector` instead. + */ +export const useStore = ( + source: { + get: () => TSource + subscribe: (listener: (value: TSource) => void) => { + unsubscribe: () => void + } + }, + selector: (snapshot: TSource) => TSelected = (value) => + value as unknown as TSelected, + compare?: (a: TSelected, b: TSelected) => boolean, +): Accessor => useSelector(source, selector, { compare }) diff --git a/packages/solid-store/src/useValue.ts b/packages/solid-store/src/useValue.ts new file mode 100644 index 00000000..a6ebbd63 --- /dev/null +++ b/packages/solid-store/src/useValue.ts @@ -0,0 +1,33 @@ +import { useSelector } from './useSelector' +import type { Accessor } from 'solid-js' +import type { Atom, ReadonlyAtom, ReadonlyStore, Store } from '@tanstack/store' +import type { UseSelectorOptions } from './useSelector' + +/** + * Subscribes to an atom or store and returns its current value accessor. + * + * This is the whole-value counterpart to {@link useSelector}. Use it when the + * component needs the entire current value from a source. + * + * @example + * ```tsx + * const count = useValue(countAtom) + * + * return

{count()}

+ * ``` + * + * @example + * ```tsx + * const state = useValue(counterStore) + * ``` + */ +export function useValue( + source: + | Atom + | ReadonlyAtom + | Store + | ReadonlyStore, + options?: UseSelectorOptions, +): Accessor { + return useSelector(source, (value) => value, options) +} diff --git a/packages/solid-store/tests/index.test.tsx b/packages/solid-store/tests/index.test.tsx index 082521c0..55c11a58 100644 --- a/packages/solid-store/tests/index.test.tsx +++ b/packages/solid-store/tests/index.test.tsx @@ -1,55 +1,309 @@ -import { describe, expect, it } from 'vitest' -import { render, renderHook } from '@solidjs/testing-library' -import { createStore } from '@tanstack/store' -import { useStore } from '../src/index' +import { describe, expect, it, test, vi } from 'vitest' +import { renderHook } from '@solidjs/testing-library' +import { createAtom, createStore } from '@tanstack/store' +import { + _useStore, + shallow, + useAtom, + useSelector, + useStore, + useValue, +} from '../src/index' -describe('useStore', () => { - it.todo('allows us to select state using a selector', () => { +describe('atom hooks', () => { + it('useValue reads mutable atom state and updates when changed', () => { + const atom = createAtom(0) + const { result } = renderHook(() => useValue(atom)) + + expect(result()).toBe(0) + + atom.set((prev) => prev + 1) + + expect(result()).toBe(1) + }) + + it('useAtom returns the current accessor and setter', () => { + const atom = createAtom(0) + const { result } = renderHook(() => useAtom(atom)) + + expect(result[0]()).toBe(0) + + result[1]((prev) => prev + 5) + + expect(result[0]()).toBe(5) + }) +}) + +describe('store hooks', () => { + it('useSelector allows us to select state using a selector', () => { const store = createStore({ select: 0, ignored: 1, }) - function Comp() { - const storeVal = useStore(store, (state) => state.select) + const { result } = renderHook(() => + useSelector(store, (state) => state.select), + ) + + expect(result()).toBe(0) + }) + + it('useValue reads writable and readonly store state', () => { + const baseStore = createStore(1) + const readonlyStore = createStore(() => ({ value: baseStore.state * 2 })) + const { result: writableValue } = renderHook(() => useValue(baseStore)) + const { result: readonlyValue } = renderHook(() => useValue(readonlyStore)) + + expect(writableValue()).toBe(1) + expect(readonlyValue().value).toBe(2) - return

Store: {storeVal()}

- } + baseStore.setState((prev) => prev + 1) - const { getByText } = render(() => ) - expect(getByText('Store: 0')).toBeInTheDocument() + expect(writableValue()).toBe(2) + expect(readonlyValue().value).toBe(4) }) - it('allows us to select state using a selector', () => { + it('useSelector only updates the accessor when selected state changes', () => { const store = createStore({ select: 0, ignored: 1, }) + const renderSpy = vi.fn() + + const { result } = renderHook(() => { + const value = useSelector(store, (state) => state.select) + renderSpy() + return value + }) + + expect(result()).toBe(0) + expect(renderSpy).toHaveBeenCalledTimes(1) + + store.setState((prev) => ({ + ...prev, + ignored: prev.ignored + 1, + })) + expect(renderSpy).toHaveBeenCalledTimes(1) + + store.setState((prev) => ({ + ...prev, + select: prev.select + 1, + })) + expect(result()).toBe(1) + expect(renderSpy).toHaveBeenCalledTimes(1) + }) + + it('useSelector respects custom compare', () => { + const store = createStore({ + array: [ + { select: 0, ignore: 1 }, + { select: 0, ignore: 1 }, + ], + }) + const renderSpy = vi.fn() + + const { result } = renderHook(() => { + const value = useSelector( + store, + (state) => state.array.map(({ ignore, ...rest }) => rest), + { + compare: (prev, next) => + JSON.stringify(prev) === JSON.stringify(next), + }, + ) + renderSpy() + return value + }) + + expect(result().map((item) => item.select)).toEqual([0, 0]) + expect(renderSpy).toHaveBeenCalledTimes(1) + + store.setState((prev) => ({ + array: prev.array.map((item) => ({ + ...item, + ignore: item.ignore + 1, + })), + })) + expect(renderSpy).toHaveBeenCalledTimes(1) + store.setState((prev) => ({ + array: prev.array.map((item) => ({ + ...item, + select: item.select + 1, + })), + })) + expect(result().map((item) => item.select)).toEqual([1, 1]) + expect(renderSpy).toHaveBeenCalledTimes(1) + }) + + it('useSelector works with mounted derived stores', () => { + const store = createStore(0) + const derived = createStore(() => ({ val: store.state * 2 })) const { result } = renderHook(() => - useStore(store, (state) => state.select), + useSelector(derived, (state) => state.val), ) expect(result()).toBe(0) + + store.setState((prev) => prev + 1) + + expect(result()).toBe(2) }) +}) - it('updates accessor value when state is updated', () => { +describe('useStore', () => { + it('is a compatibility alias for useSelector', () => { const store = createStore(0) + const { result } = renderHook(() => useStore(store, (state) => state)) - const { result } = renderHook(() => useStore(store)) + expect(result()).toBe(0) store.setState((prev) => prev + 1) expect(result()).toBe(1) }) - it('updates when date changes', () => { - const store = createStore(new Date('2025-03-29T21:06:30.401Z')) + it('supports atom sources through the deprecated alias', () => { + const atom = createAtom(0) + const { result } = renderHook(() => useStore(atom, (state) => state)) + + expect(result()).toBe(0) + + atom.set((prev) => prev + 1) + + expect(result()).toBe(1) + }) +}) + +describe('_useStore', () => { + it('returns selected state and actions for stores with actions', () => { + const store = createStore({ count: 0 }, ({ setState }) => ({ + inc: () => setState((prev) => ({ count: prev.count + 1 })), + })) + + const { result } = renderHook(() => + _useStore(store, (state) => state.count), + ) + + expect(result[0]()).toBe(0) + + result[1].inc() + + expect(result[0]()).toBe(1) + }) + + it('returns selected state and setState for plain stores', () => { + const store = createStore(0) + + const { result } = renderHook(() => _useStore(store, (state) => state)) + + expect(result[0]()).toBe(0) + + result[1]((prev) => prev + 1) + + expect(result[0]()).toBe(1) + }) +}) + +describe('shallow', () => { + test('should return true for shallowly equal objects', () => { + const objA = { a: 1, b: 'hello' } + const objB = { a: 1, b: 'hello' } + expect(shallow(objA, objB)).toBe(true) + }) + + test('should return false for objects with different values', () => { + const objA = { a: 1, b: 'hello' } + const objB = { a: 2, b: 'world' } + expect(shallow(objA, objB)).toBe(false) + }) + + test('should return false for objects with different keys', () => { + const objA = { a: 1, b: 'hello' } + const objB = { a: 1, c: 'world' } + // @ts-expect-error + expect(shallow(objA, objB)).toBe(false) + }) + + test('should return false for objects with different structures', () => { + const objA = { a: 1, b: 'hello' } + const objB = [1, 'hello'] + // @ts-expect-error + expect(shallow(objA, objB)).toBe(false) + }) + + test('should return false for one object being null', () => { + const objA = { a: 1, b: 'hello' } + const objB = null + expect(shallow(objA, objB)).toBe(false) + }) + + test('should return false for one object being undefined', () => { + const objA = { a: 1, b: 'hello' } + const objB = undefined + expect(shallow(objA, objB)).toBe(false) + }) + + test('should return true for two null objects', () => { + const objA = null + const objB = null + expect(shallow(objA, objB)).toBe(true) + }) + + test('should return false for objects with different types', () => { + const objA = { a: 1, b: 'hello' } + const objB = { a: '1', b: 'hello' } + // @ts-expect-error + expect(shallow(objA, objB)).toBe(false) + }) + + test('should return true for shallow equal objects with symbol keys', () => { + const sym = Symbol.for('key') + const objA = { [sym]: 1 } + const objB = { [sym]: 1 } + expect(shallow(objA, objB)).toBe(true) + }) + + test('should return false for shallow different values for symbol keys', () => { + const sym = Symbol.for('key') + const objA = { [sym]: 1 } + const objB = { [sym]: 2 } + expect(shallow(objA, objB)).toBe(false) + }) + + test('should return true for shallowly equal maps', () => { + const objA = new Map([['1', 'hello']]) + const objB = new Map([['1', 'hello']]) + expect(shallow(objA, objB)).toBe(true) + }) + + test('should return false for maps with different values', () => { + const objA = new Map([['1', 'hello']]) + const objB = new Map([['1', 'world']]) + expect(shallow(objA, objB)).toBe(false) + }) + + test('should return true for shallowly equal sets', () => { + const objA = new Set([1]) + const objB = new Set([1]) + expect(shallow(objA, objB)).toBe(true) + }) - const { result } = renderHook(() => useStore(store)) + test('should return false for sets with different values', () => { + const objA = new Set([1]) + const objB = new Set([2]) + expect(shallow(objA, objB)).toBe(false) + }) - store.setState(() => new Date('2025-03-29T21:06:40.401Z')) + test('should return false for dates with different values', () => { + const objA = new Date('2025-04-10T14:48:00') + const objB = new Date('2025-04-10T14:58:00') + expect(shallow(objA, objB)).toBe(false) + }) - expect(result()).toStrictEqual(new Date('2025-03-29T21:06:40.401Z')) + test('should return true for equal dates', () => { + const objA = new Date('2025-02-10') + const objB = new Date('2025-02-10') + expect(shallow(objA, objB)).toBe(true) }) }) diff --git a/packages/solid-store/tests/test.test-d.ts b/packages/solid-store/tests/test.test-d.ts index e3da48b4..1e49ea64 100644 --- a/packages/solid-store/tests/test.test-d.ts +++ b/packages/solid-store/tests/test.test-d.ts @@ -1,16 +1,70 @@ import { expectTypeOf, test } from 'vitest' -import { createStore } from '@tanstack/store' -import { useStore } from '../src' +import { createAtom, createStore } from '@tanstack/store' +import { _useStore, useAtom, useSelector, useStore, useValue } from '../src' import type { Accessor } from 'solid-js' +import type { Store } from '@tanstack/store' -test('useStore works with derived state', () => { +test('useSelector works with derived state', () => { const store = createStore(12) const derived = createStore(() => store.state * 2) - const val = useStore(derived, (state) => { + const val = useSelector(derived, (state) => { expectTypeOf(state).toEqualTypeOf() return state }) expectTypeOf(val).toEqualTypeOf>() }) + +test('useValue infers value from mutable and readonly sources', () => { + const writableAtom = createAtom(12) + const readonlyAtom = createAtom(() => 24) + const writableStore = createStore(12) + const readonlyStore = createStore(() => 24) + + expectTypeOf(useValue(writableAtom)).toEqualTypeOf>() + expectTypeOf(useValue(readonlyAtom)).toEqualTypeOf>() + expectTypeOf(useValue(writableStore)).toEqualTypeOf>() + expectTypeOf(useValue(readonlyStore)).toEqualTypeOf>() +}) + +test('useAtom only accepts writable atoms', () => { + const writableAtom = createAtom(12) + const readonlyAtom = createAtom(() => 24) + + const [value, setValue] = useAtom(writableAtom) + + expectTypeOf(value).toEqualTypeOf>() + expectTypeOf(setValue).toBeFunction() + // @ts-expect-error readonly atoms cannot be used with useAtom + useAtom(readonlyAtom) +}) + +test('useStore matches useSelector types for compatibility', () => { + const store = createStore(12) + const selectorValue = useSelector(store, (state) => state) + const compatValue = useStore(store, (state) => state) + + expectTypeOf(selectorValue).toEqualTypeOf>() + expectTypeOf(compatValue).toEqualTypeOf>() +}) + +test('_useStore returns actions for stores with actions', () => { + const store = createStore({ count: 0 }, ({ setState }) => ({ + inc: () => setState((prev) => ({ count: prev.count + 1 })), + })) + + const [selected, actions] = _useStore(store, (state) => state.count) + + expectTypeOf(selected).toEqualTypeOf>() + expectTypeOf(actions.inc).toBeFunction() +}) + +test('_useStore returns setState for plain stores', () => { + const store = createStore(0) + + const [selected, setState] = _useStore(store, (state) => state) + + expectTypeOf(selected).toEqualTypeOf>() + expectTypeOf(setState).toEqualTypeOf['setState']>() +}) diff --git a/packages/store/src/index.ts b/packages/store/src/index.ts index ff7c5459..71215a5e 100644 --- a/packages/store/src/index.ts +++ b/packages/store/src/index.ts @@ -1,3 +1,4 @@ export * from './types' export * from './atom' export * from './store' +export * from './shallow' diff --git a/packages/react-store/src/shallow.ts b/packages/store/src/shallow.ts similarity index 100% rename from packages/react-store/src/shallow.ts rename to packages/store/src/shallow.ts diff --git a/packages/store/src/store.ts b/packages/store/src/store.ts index b1772123..4e83b9e6 100644 --- a/packages/store/src/store.ts +++ b/packages/store/src/store.ts @@ -1,18 +1,14 @@ import { createAtom, toObserver } from './atom' import type { Atom, Observer, Subscription } from './types' -export interface StoreActionsApi { - set: (updater: (prev: T) => T) => void - get: () => T -} - export type StoreAction = (...args: Array) => any export type StoreActionMap = Record -export type StoreActionsFactory = ( - api: StoreActionsApi, -) => TActions +export type StoreActionsFactory = (store: { + setState: Store['setState'] + get: Store['get'] +}) => TActions type NonFunction = T extends (...args: Array) => any ? never : T @@ -35,11 +31,11 @@ export class Store { valueOrFn as T | ((prev?: NoInfer) => T), ) as Atom + this.setState = this.setState.bind(this) + this.get = this.get.bind(this) + if (actionsFactory) { - this.actions = actionsFactory({ - set: (updater) => this.setState(updater), - get: () => this.get(), - }) + this.actions = actionsFactory(this) } } public setState(updater: (prev: T) => T) { diff --git a/packages/store/tests/store-type-safety.test.ts b/packages/store/tests/store-type-safety.test.ts index c8568b94..e0585d8e 100644 --- a/packages/store/tests/store-type-safety.test.ts +++ b/packages/store/tests/store-type-safety.test.ts @@ -64,16 +64,18 @@ describe('Store.setState Type Safety Improvements', () => { }) test('should infer action types safely', () => { - const store = createStore({ count: 0 }, ({ get, set }) => ({ - inc: () => set((prev) => ({ count: prev.count + 1 })), + const store = createStore({ count: 0 }, ({ get, setState }) => ({ + inc: () => setState((prev) => ({ count: prev.count + 1 })), current: () => get().count, })) store.actions.inc() expect(store.actions.current()).toBe(1) - // @ts-expect-error set only accepts updater functions - createStore({ count: 0 }, ({ set }) => ({ bad: () => set({ count: 1 }) })) + createStore({ count: 0 }, ({ setState }) => ({ + // @ts-expect-error setState only accepts updater functions + bad: () => setState({ count: 1 }), + })) if (typecheckOnly) { createStore({ count: 0 }, () => ({ @@ -106,8 +108,8 @@ describe('Store.setState Type Safety Improvements', () => { createStore( // @ts-expect-error function first arg with actions is not supported () => ({ count: 0 }), - ({ set }) => ({ - inc: () => set((prev) => ({ count: prev.count + 1 })), + ({ setState }) => ({ + inc: () => setState((prev) => ({ count: prev.count + 1 })), }), ) } diff --git a/packages/store/tests/store.test.ts b/packages/store/tests/store.test.ts index c08187b0..7c088144 100644 --- a/packages/store/tests/store.test.ts +++ b/packages/store/tests/store.test.ts @@ -39,8 +39,8 @@ describe('store', () => { }) test('supports actions on writable stores', () => { - const store = createStore({ count: 0 }, ({ set }) => ({ - inc: () => set((prev) => ({ count: prev.count + 1 })), + const store = createStore({ count: 0 }, ({ setState }) => ({ + inc: () => setState((prev) => ({ count: prev.count + 1 })), })) expect(store.state).toEqual({ count: 0 }) @@ -51,10 +51,10 @@ describe('store', () => { }) test('actions can read current state', () => { - const store = createStore({ count: 1 }, ({ get, set }) => ({ + const store = createStore({ count: 1 }, ({ get, setState }) => ({ addIfOdd: () => { if (get().count % 2 === 1) { - set((prev) => ({ count: prev.count + 1 })) + setState((prev) => ({ count: prev.count + 1 })) } }, })) @@ -67,8 +67,8 @@ describe('store', () => { }) test('actions bag is stable across updates', () => { - const store = createStore({ count: 0 }, ({ set }) => ({ - inc: () => set((prev) => ({ count: prev.count + 1 })), + const store = createStore({ count: 0 }, ({ setState }) => ({ + inc: () => setState((prev) => ({ count: prev.count + 1 })), })) const actions = store.actions diff --git a/packages/svelte-store/src/_useStore.ts b/packages/svelte-store/src/_useStore.ts new file mode 100644 index 00000000..da9caba6 --- /dev/null +++ b/packages/svelte-store/src/_useStore.ts @@ -0,0 +1,38 @@ +import { useSelector } from './useSelector.svelte.js' +import type { Store, StoreActionMap } from '@tanstack/store' +import type { UseSelectorOptions } from './useSelector.svelte.js' + +/** + * Experimental combined read+write hook for stores, mirroring useAtom's tuple + * pattern. + * + * Returns `[selected, actions]` when the store has an actions factory, or + * `[selected, setState]` for plain stores. + * + * @example + * ```ts + * // Store with actions + * const [cats, { addCat }] = _useStore(petStore, (s) => s.cats) + * + * // Store without actions + * const [count, setState] = _useStore(plainStore, (s) => s) + * ``` + */ +export function _useStore< + TState, + TActions extends StoreActionMap, + TSelected = NoInfer, +>( + store: Store, + selector: (state: NoInfer) => TSelected, + options?: UseSelectorOptions, +): [ + { readonly current: TSelected }, + [TActions] extends [never] ? Store['setState'] : TActions, +] { + const selected = useSelector(store, selector, options) + const actionsOrSetState = + (store.actions as StoreActionMap | undefined) ?? store.setState + + return [selected, actionsOrSetState] as any +} diff --git a/packages/svelte-store/src/index.svelte.ts b/packages/svelte-store/src/index.svelte.ts index ea4df8ce..9ae7245d 100644 --- a/packages/svelte-store/src/index.svelte.ts +++ b/packages/svelte-store/src/index.svelte.ts @@ -1,86 +1,8 @@ -import type { Atom, ReadonlyAtom } from '@tanstack/store' - export * from '@tanstack/store' -type EqualityFn = (objA: T, objB: T) => boolean -interface UseStoreOptions { - equal?: EqualityFn -} - -export function useStore>( - store: Atom | ReadonlyAtom, - selector: (state: NoInfer) => TSelected = (d) => d as any, - options: UseStoreOptions = {}, -): { readonly current: TSelected } { - const equal = options.equal ?? shallow - let slice = $state(selector(store.get())) - - $effect(() => { - const unsub = store.subscribe((s) => { - const data = selector(s) - if (equal(slice, data)) { - return - } - slice = data - }).unsubscribe - - return unsub - }) - - return { - get current() { - return slice - }, - } -} - -export function shallow(objA: T, objB: T) { - if (Object.is(objA, objB)) { - return true - } - - if ( - typeof objA !== 'object' || - objA === null || - typeof objB !== 'object' || - objB === null - ) { - return false - } - - if (objA instanceof Map && objB instanceof Map) { - if (objA.size !== objB.size) return false - for (const [k, v] of objA) { - if (!objB.has(k) || !Object.is(v, objB.get(k))) return false - } - return true - } - - if (objA instanceof Set && objB instanceof Set) { - if (objA.size !== objB.size) return false - for (const v of objA) { - if (!objB.has(v)) return false - } - return true - } - - if (objA instanceof Date && objB instanceof Date) { - if (objA.getTime() !== objB.getTime()) return false - return true - } - - const keysA = Object.keys(objA) - if (keysA.length !== Object.keys(objB).length) { - return false - } +export * from './useSelector.svelte.js' +export * from './useValue' - for (let i = 0; i < keysA.length; i++) { - if ( - !Object.prototype.hasOwnProperty.call(objB, keysA[i] as string) || - !Object.is(objA[keysA[i] as keyof T], objB[keysA[i] as keyof T]) - ) { - return false - } - } - return true -} +export * from './useAtom' +export * from './useStore' // @deprecated in favor of useSelector +export * from './_useStore' diff --git a/packages/svelte-store/src/useAtom.ts b/packages/svelte-store/src/useAtom.ts new file mode 100644 index 00000000..5fcbbeaa --- /dev/null +++ b/packages/svelte-store/src/useAtom.ts @@ -0,0 +1,25 @@ +import { useValue } from './useValue' +import type { Atom } from '@tanstack/store' +import type { UseSelectorOptions } from './useSelector.svelte.js' + +/** + * Returns the current atom holder together with a setter. + * + * Use this when a component needs to both read and update the same writable + * atom. + * + * @example + * ```ts + * const [count, setCount] = useAtom(countAtom) + * setCount((prev) => prev + 1) + * console.log(count.current) + * ``` + */ +export function useAtom( + atom: Atom, + options?: UseSelectorOptions, +): [{ readonly current: TValue }, Atom['set']] { + const value = useValue(atom, options) + + return [value, atom.set] +} diff --git a/packages/svelte-store/src/useSelector.svelte.ts b/packages/svelte-store/src/useSelector.svelte.ts new file mode 100644 index 00000000..82a72fb1 --- /dev/null +++ b/packages/svelte-store/src/useSelector.svelte.ts @@ -0,0 +1,57 @@ +import type { Atom, ReadonlyAtom, ReadonlyStore, Store } from '@tanstack/store' + +export interface UseSelectorOptions { + compare?: (a: TSelected, b: TSelected) => boolean +} + +function defaultCompare(a: T, b: T) { + return a === b +} + +/** + * Selects a slice of state from an atom or store and exposes it through a + * rune-friendly holder object. + * + * Read the selected value from `.current`. + * + * @example + * ```ts + * const count = useSelector(counterStore, (state) => state.count) + * console.log(count.current) + * ``` + * + * @example + * ```ts + * const doubled = useSelector(countAtom, (value) => value * 2) + * ``` + */ +export function useSelector>( + source: + | Atom + | ReadonlyAtom + | Store + | ReadonlyStore, + selector: (state: NoInfer) => TSelected = (d) => d as any, + options: UseSelectorOptions = {}, +): { readonly current: TSelected } { + const compare = options.compare ?? defaultCompare + let slice = $state(selector(source.get())) + + $effect(() => { + const unsub = source.subscribe((s) => { + const data = selector(s) + if (compare(slice, data)) { + return + } + slice = data + }).unsubscribe + + return unsub + }) + + return { + get current() { + return slice + }, + } +} diff --git a/packages/svelte-store/src/useStore.ts b/packages/svelte-store/src/useStore.ts new file mode 100644 index 00000000..b3a5642b --- /dev/null +++ b/packages/svelte-store/src/useStore.ts @@ -0,0 +1,23 @@ +import { useSelector } from './useSelector.svelte.js' +import type { Atom, ReadonlyAtom, ReadonlyStore, Store } from '@tanstack/store' + +/** + * Deprecated alias for {@link useSelector}. + * + * @example + * ```ts + * const count = useStore(counterStore, (state) => state.count) + * console.log(count.current) + * ``` + * + * @deprecated Use `useSelector` instead. + */ +export const useStore = >( + source: + | Atom + | ReadonlyAtom + | Store + | ReadonlyStore, + selector: (state: NoInfer) => TSelected = (d) => d as any, + compare?: (a: TSelected, b: TSelected) => boolean, +) => useSelector(source, selector, { compare }) diff --git a/packages/svelte-store/src/useValue.ts b/packages/svelte-store/src/useValue.ts new file mode 100644 index 00000000..95765c4a --- /dev/null +++ b/packages/svelte-store/src/useValue.ts @@ -0,0 +1,29 @@ +import { useSelector } from './useSelector.svelte.js' +import type { Atom, ReadonlyAtom, ReadonlyStore, Store } from '@tanstack/store' +import type { UseSelectorOptions } from './useSelector.svelte.js' + +/** + * Subscribes to an atom or store and returns its whole current value through a + * rune-friendly holder object. + * + * @example + * ```ts + * const count = useValue(countAtom) + * console.log(count.current) + * ``` + * + * @example + * ```ts + * const state = useValue(counterStore) + * ``` + */ +export function useValue( + source: + | Atom + | ReadonlyAtom + | Store + | ReadonlyStore, + options?: UseSelectorOptions, +): { readonly current: TValue } { + return useSelector(source, (value) => value, options) +} diff --git a/packages/svelte-store/tests/BaseStore.test.svelte b/packages/svelte-store/tests/BaseStore.test.svelte index 529e410f..aef07158 100644 --- a/packages/svelte-store/tests/BaseStore.test.svelte +++ b/packages/svelte-store/tests/BaseStore.test.svelte @@ -1,13 +1,13 @@

Store: {storeVal.current}

diff --git a/packages/svelte-store/tests/Render.test.svelte b/packages/svelte-store/tests/Render.test.svelte index 7700c555..baf6381a 100644 --- a/packages/svelte-store/tests/Render.test.svelte +++ b/packages/svelte-store/tests/Render.test.svelte @@ -1,14 +1,14 @@ + +
+

Value: {value.current}

+

Readonly: {readonlyValue.current.value}

+ +
diff --git a/packages/svelte-store/tests/index.test.ts b/packages/svelte-store/tests/index.test.ts index 24f7a3dd..158fc0ef 100644 --- a/packages/svelte-store/tests/index.test.ts +++ b/packages/svelte-store/tests/index.test.ts @@ -4,10 +4,11 @@ import { userEvent } from '@testing-library/user-event' import { shallow } from '../src/index.svelte.js' import TestBaseStore from './BaseStore.test.svelte' import TestRerender from './Render.test.svelte' +import TestValue from './Value.test.svelte' const user = userEvent.setup() -describe('useStore', () => { +describe('useSelector', () => { it('allows us to select state using a selector', () => { const { getByText } = render(TestBaseStore) expect(getByText('Store: 0')).toBeInTheDocument() @@ -26,6 +27,17 @@ describe('useStore', () => { await user.click(getByText('Update ignored')) expect(getByText('Number rendered: 2')).toBeInTheDocument() }) + + it('useValue reads writable and readonly store state', async () => { + const { getByText } = render(TestValue) + expect(getByText('Value: 1')).toBeInTheDocument() + expect(getByText('Readonly: 2')).toBeInTheDocument() + + await user.click(getByText('Update')) + + await waitFor(() => expect(getByText('Value: 2')).toBeInTheDocument()) + await waitFor(() => expect(getByText('Readonly: 4')).toBeInTheDocument()) + }) }) describe('shallow', () => { diff --git a/packages/svelte-store/tests/test.test-d.ts b/packages/svelte-store/tests/test.test-d.ts new file mode 100644 index 00000000..d26acb20 --- /dev/null +++ b/packages/svelte-store/tests/test.test-d.ts @@ -0,0 +1,84 @@ +import { expectTypeOf, test } from 'vitest' +import { createAtom, createStore } from '@tanstack/store' +import { + _useStore, + useAtom, + useSelector, + useStore, + useValue, +} from '../src/index.svelte.js' +import type { Store } from '@tanstack/store' + +test('useSelector works with derived state', () => { + const store = createStore(12) + const derived = createStore(() => store.state * 2) + + const val = useSelector(derived, (state) => { + expectTypeOf(state).toEqualTypeOf() + return state + }) + + expectTypeOf(val).toEqualTypeOf<{ readonly current: number }>() +}) + +test('useValue infers value from mutable and readonly sources', () => { + const writableAtom = createAtom(12) + const readonlyAtom = createAtom(() => 24) + const writableStore = createStore(12) + const readonlyStore = createStore(() => 24) + + expectTypeOf(useValue(writableAtom)).toEqualTypeOf<{ + readonly current: number + }>() + expectTypeOf(useValue(readonlyAtom)).toEqualTypeOf<{ + readonly current: number + }>() + expectTypeOf(useValue(writableStore)).toEqualTypeOf<{ + readonly current: number + }>() + expectTypeOf(useValue(readonlyStore)).toEqualTypeOf<{ + readonly current: number + }>() +}) + +test('useAtom only accepts writable atoms', () => { + const writableAtom = createAtom(12) + const readonlyAtom = createAtom(() => 24) + + const [value, setValue] = useAtom(writableAtom) + + expectTypeOf(value).toEqualTypeOf<{ readonly current: number }>() + expectTypeOf(setValue).toBeFunction() + // @ts-expect-error readonly atoms cannot be used with useAtom + useAtom(readonlyAtom) +}) + +test('useStore matches useSelector types for compatibility', () => { + const store = createStore(12) + const selectorValue = useSelector(store, (state) => state) + const compatValue = useStore(store, (state) => state) + + expectTypeOf(selectorValue).toEqualTypeOf<{ readonly current: number }>() + expectTypeOf(compatValue).toEqualTypeOf<{ readonly current: number }>() +}) + +test('_useStore returns selected state and second tuple element for stores with actions', () => { + const store = createStore({ count: 0 }, ({ setState }) => ({ + inc: () => setState((prev) => ({ count: prev.count + 1 })), + })) + + const result = _useStore(store, (state) => state.count) + + expectTypeOf(result[0]).toEqualTypeOf<{ readonly current: number }>() + // The second element should be the actions bag + expectTypeOf(result).toBeArray() +}) + +test('_useStore returns setState for plain stores', () => { + const store = createStore(0) + + const [selected, setState] = _useStore(store, (state) => state) + + expectTypeOf(selected).toEqualTypeOf<{ readonly current: number }>() + expectTypeOf(setState).toEqualTypeOf['setState']>() +}) diff --git a/packages/vue-store/src/_useStore.ts b/packages/vue-store/src/_useStore.ts new file mode 100644 index 00000000..f7401fb8 --- /dev/null +++ b/packages/vue-store/src/_useStore.ts @@ -0,0 +1,41 @@ +import { useSelector } from './useSelector' +import type { Ref } from 'vue-demi' +import type { Store, StoreActionMap } from '@tanstack/store' +import type { UseSelectorOptions } from './useSelector' + +/** + * Experimental combined read+write hook for stores, mirroring useAtom's tuple + * pattern. + * + * Returns `[selected, actions]` when the store has an actions factory, or + * `[selected, setState]` for plain stores. + * + * @example + * ```ts + * // Store with actions + * const [cats, { addCat }] = _useStore(petStore, (s) => s.cats) + * console.log(cats.value) + * + * // Store without actions + * const [count, setState] = _useStore(plainStore, (s) => s) + * setState((prev) => prev + 1) + * ``` + */ +export function _useStore< + TState, + TActions extends StoreActionMap, + TSelected = NoInfer, +>( + store: Store, + selector: (state: NoInfer) => TSelected, + options?: UseSelectorOptions, +): [ + Readonly>, + [TActions] extends [never] ? Store['setState'] : TActions, +] { + const selected = useSelector(store, selector, options) + const actionsOrSetState = + (store.actions as StoreActionMap | undefined) ?? store.setState + + return [selected, actionsOrSetState] as any +} diff --git a/packages/vue-store/src/index.ts b/packages/vue-store/src/index.ts index e8c1107c..ea10ac37 100644 --- a/packages/vue-store/src/index.ts +++ b/packages/vue-store/src/index.ts @@ -1,90 +1,8 @@ -import { readonly, ref, toRaw, watch } from 'vue-demi' -import type { Atom, ReadonlyAtom } from '@tanstack/store' -import type { Ref } from 'vue-demi' - export * from '@tanstack/store' -type EqualityFn = (objA: T, objB: T) => boolean -interface UseStoreOptions { - equal?: EqualityFn -} - -export function useStore>( - store: Atom | ReadonlyAtom, - selector: (state: NoInfer) => TSelected = (d) => d as any, - options: UseStoreOptions = {}, -): Readonly> { - const slice = ref(selector(store.get())) as Ref - const equal = options.equal ?? shallow - - watch( - () => store, - (value, _oldValue, onCleanup) => { - const unsub = value.subscribe((s) => { - const data = selector(s) - if (equal(toRaw(slice.value), data)) { - return - } - slice.value = data - }).unsubscribe - - onCleanup(() => { - unsub() - }) - }, - { immediate: true }, - ) - - return readonly(slice) as never -} - -export function shallow(objA: T, objB: T) { - if (Object.is(objA, objB)) { - return true - } - - if ( - typeof objA !== 'object' || - objA === null || - typeof objB !== 'object' || - objB === null - ) { - return false - } - - if (objA instanceof Map && objB instanceof Map) { - if (objA.size !== objB.size) return false - for (const [k, v] of objA) { - if (!objB.has(k) || !Object.is(v, objB.get(k))) return false - } - return true - } - - if (objA instanceof Set && objB instanceof Set) { - if (objA.size !== objB.size) return false - for (const v of objA) { - if (!objB.has(v)) return false - } - return true - } - - if (objA instanceof Date && objB instanceof Date) { - if (objA.getTime() !== objB.getTime()) return false - return true - } - - const keysA = Object.keys(objA) - if (keysA.length !== Object.keys(objB).length) { - return false - } +export * from './useValue' +export * from './useSelector' - for (let i = 0; i < keysA.length; i++) { - if ( - !Object.prototype.hasOwnProperty.call(objB, keysA[i] as string) || - !Object.is(objA[keysA[i] as keyof T], objB[keysA[i] as keyof T]) - ) { - return false - } - } - return true -} +export * from './useAtom' +export * from './useStore' // @deprecated in favor of useSelector +export * from './_useStore' diff --git a/packages/vue-store/src/useAtom.ts b/packages/vue-store/src/useAtom.ts new file mode 100644 index 00000000..673441a3 --- /dev/null +++ b/packages/vue-store/src/useAtom.ts @@ -0,0 +1,27 @@ +import { useValue } from './useValue' +import type { Ref } from 'vue-demi' +import type { Atom } from '@tanstack/store' +import type { UseSelectorOptions } from './useSelector' + +/** + * Returns the current atom ref together with a setter. + * + * Use this when a component needs to both read and update the same writable + * atom. + * + * @example + * ```ts + * const [count, setCount] = useAtom(countAtom) + * + * setCount((prev) => prev + 1) + * console.log(count.value) + * ``` + */ +export function useAtom( + atom: Atom, + options?: UseSelectorOptions, +): [Readonly>, Atom['set']] { + const value = useValue(atom, options) + + return [value, atom.set] +} diff --git a/packages/vue-store/src/useSelector.ts b/packages/vue-store/src/useSelector.ts new file mode 100644 index 00000000..a557a960 --- /dev/null +++ b/packages/vue-store/src/useSelector.ts @@ -0,0 +1,57 @@ +import { onScopeDispose, readonly, shallowRef, toRaw } from 'vue-demi' +import type { Ref } from 'vue-demi' + +export interface UseSelectorOptions { + compare?: (a: TSelected, b: TSelected) => boolean +} + +type SelectionSource = { + get: () => T + subscribe: (listener: (value: T) => void) => { + unsubscribe: () => void + } +} + +function defaultCompare(a: T, b: T) { + return a === b +} + +/** + * Selects a slice of state from an atom or store and subscribes the component + * to that selection. + * + * This is the primary Vue read hook for TanStack Store. It returns a readonly + * ref containing the selected value. + * + * @example + * ```ts + * const count = useSelector(counterStore, (state) => state.count) + * console.log(count.value) + * ``` + * + * @example + * ```ts + * const doubled = useSelector(countAtom, (value) => value * 2) + * ``` + */ +export function useSelector( + source: SelectionSource, + selector: (snapshot: TSource) => TSelected, + options?: UseSelectorOptions, +): Readonly> { + const compare = options?.compare ?? defaultCompare + const slice = shallowRef(selector(source.get())) as Ref + const unsubscribe = source.subscribe((snapshot) => { + const selected = selector(snapshot) + if (compare(toRaw(slice.value), selected)) { + return + } + slice.value = selected + }).unsubscribe + + onScopeDispose(() => { + unsubscribe() + }) + + return readonly(slice) as Readonly> +} diff --git a/packages/vue-store/src/useStore.ts b/packages/vue-store/src/useStore.ts new file mode 100644 index 00000000..2883074c --- /dev/null +++ b/packages/vue-store/src/useStore.ts @@ -0,0 +1,23 @@ +import { useSelector } from './useSelector' +import type { Ref } from 'vue-demi' + +/** + * Deprecated alias for {@link useSelector}. + * + * @example + * ```ts + * const count = useStore(counterStore, (state) => state.count) + * ``` + * + * @deprecated Use `useSelector` instead. + */ +export const useStore = ( + source: { + get: () => TSource + subscribe: (listener: (value: TSource) => void) => { + unsubscribe: () => void + } + }, + selector: (snapshot: TSource) => TSelected, + compare?: (a: TSelected, b: TSelected) => boolean, +): Readonly> => useSelector(source, selector, { compare }) diff --git a/packages/vue-store/src/useValue.ts b/packages/vue-store/src/useValue.ts new file mode 100644 index 00000000..ef24720b --- /dev/null +++ b/packages/vue-store/src/useValue.ts @@ -0,0 +1,32 @@ +import { useSelector } from './useSelector' +import type { Ref } from 'vue-demi' +import type { Atom, ReadonlyAtom, ReadonlyStore, Store } from '@tanstack/store' +import type { UseSelectorOptions } from './useSelector' + +/** + * Subscribes to an atom or store and returns its current value ref. + * + * This is the whole-value counterpart to {@link useSelector}. Use it when the + * component needs the entire current value from a source. + * + * @example + * ```ts + * const count = useValue(countAtom) + * console.log(count.value) + * ``` + * + * @example + * ```ts + * const state = useValue(counterStore) + * ``` + */ +export function useValue( + source: + | Atom + | ReadonlyAtom + | Store + | ReadonlyStore, + options?: UseSelectorOptions, +): Readonly> { + return useSelector(source, (value) => value, options) +} diff --git a/packages/vue-store/tests/index.test.tsx b/packages/vue-store/tests/index.test.tsx index a81df786..c5d36a97 100644 --- a/packages/vue-store/tests/index.test.tsx +++ b/packages/vue-store/tests/index.test.tsx @@ -1,13 +1,76 @@ import { describe, expect, it, test, vi } from 'vitest' import { defineComponent, h } from 'vue-demi' import { render, waitFor } from '@testing-library/vue' -import { createStore } from '@tanstack/store' +import { createAtom, createStore } from '@tanstack/store' import { userEvent } from '@testing-library/user-event' -import { shallow, useStore } from '../src/index' +import { + _useStore, + shallow, + useAtom, + useSelector, + useStore, + useValue, +} from '../src/index' const user = userEvent.setup() -describe('useStore', () => { +describe('atom hooks', () => { + it('useValue reads mutable atom state and rerenders when updated', async () => { + const atom = createAtom(0) + + const Comp = defineComponent(() => { + const value = useValue(atom) + + return () => + h('div', [ + h('p', `Value: ${value.value}`), + h( + 'button', + { + onClick: () => atom.set((prev) => prev + 1), + }, + 'Update', + ), + ]) + }) + + const { getByText } = render(Comp) + expect(getByText('Value: 0')).toBeInTheDocument() + + await user.click(getByText('Update')) + + await waitFor(() => expect(getByText('Value: 1')).toBeInTheDocument()) + }) + + it('useAtom returns the current ref and setter', async () => { + const atom = createAtom(0) + + const Comp = defineComponent(() => { + const [value, setValue] = useAtom(atom) + + return () => + h('div', [ + h('p', `Value: ${value.value}`), + h( + 'button', + { + onClick: () => setValue((prev) => prev + 5), + }, + 'Add 5', + ), + ]) + }) + + const { getByText } = render(Comp) + expect(getByText('Value: 0')).toBeInTheDocument() + + await user.click(getByText('Add 5')) + + await waitFor(() => expect(getByText('Value: 5')).toBeInTheDocument()) + }) +}) + +describe('store hooks', () => { it('allows us to select state using a selector', () => { const store = createStore({ select: 0, @@ -15,7 +78,7 @@ describe('useStore', () => { }) const Comp = defineComponent(() => { - const storeVal = useStore(store, (state) => state.select) + const storeVal = useSelector(store, (state) => state.select) return () => h('p', `Store: ${storeVal.value}`) }) @@ -24,6 +87,38 @@ describe('useStore', () => { expect(getByText('Store: 0')).toBeInTheDocument() }) + it('useValue reads writable and readonly store state', async () => { + const baseStore = createStore(1) + const readonlyStore = createStore(() => ({ value: baseStore.state * 2 })) + + const Comp = defineComponent(() => { + const value = useValue(baseStore) + const readonlyValue = useValue(readonlyStore) + + return () => + h('div', [ + h('p', `Value: ${value.value}`), + h('p', `Readonly: ${readonlyValue.value.value}`), + h( + 'button', + { + onClick: () => baseStore.setState((prev) => prev + 1), + }, + 'Update', + ), + ]) + }) + + const { getByText } = render(Comp) + expect(getByText('Value: 1')).toBeInTheDocument() + expect(getByText('Readonly: 2')).toBeInTheDocument() + + await user.click(getByText('Update')) + + await waitFor(() => expect(getByText('Value: 2')).toBeInTheDocument()) + await waitFor(() => expect(getByText('Readonly: 4')).toBeInTheDocument()) + }) + it('only triggers a re-render when selector state is updated', async () => { const store = createStore({ select: 0, @@ -31,8 +126,7 @@ describe('useStore', () => { }) const Comp = defineComponent(() => { - const storeVal = useStore(store, (state) => state.select) - + const storeVal = useSelector(store, (state) => state.select) const fn = vi.fn() return () => { @@ -78,6 +172,207 @@ describe('useStore', () => { await user.click(getByText('Update ignored')) expect(getByText('Number rendered: 2')).toBeInTheDocument() }) + + it('useSelector allows specifying a custom equality function', async () => { + const store = createStore({ + array: [ + { select: 0, ignore: 1 }, + { select: 0, ignore: 1 }, + ], + }) + + const Comp = defineComponent(() => { + const storeVal = useSelector( + store, + (state) => state.array.map(({ ignore, ...rest }) => rest), + { + compare: (prev, next) => + JSON.stringify(prev) === JSON.stringify(next), + }, + ) + const fn = vi.fn() + + return () => { + fn() + const value = storeVal.value + .map((item) => item.select) + .reduce((total, num) => total + num, 0) + + return h('div', [ + h('p', `Number rendered: ${fn.mock.calls.length}`), + h('p', `Store: ${value}`), + h( + 'button', + { + onClick: () => + store.setState((v) => ({ + array: v.array.map((item) => ({ + ...item, + select: item.select + 5, + })), + })), + }, + 'Update select', + ), + h( + 'button', + { + onClick: () => + store.setState((v) => ({ + array: v.array.map((item) => ({ + ...item, + ignore: item.ignore + 2, + })), + })), + }, + 'Update ignored', + ), + ]) + } + }) + + const { getByText } = render(Comp) + expect(getByText('Store: 0')).toBeInTheDocument() + expect(getByText('Number rendered: 1')).toBeInTheDocument() + + await user.click(getByText('Update select')) + + await waitFor(() => expect(getByText('Store: 10')).toBeInTheDocument()) + expect(getByText('Number rendered: 2')).toBeInTheDocument() + + await user.click(getByText('Update ignored')) + expect(getByText('Number rendered: 2')).toBeInTheDocument() + }) + + it('useSelector works with mounted derived stores', async () => { + const store = createStore(0) + const derived = createStore(() => ({ val: store.state * 2 })) + + const Comp = defineComponent(() => { + const derivedVal = useSelector(derived, (state) => state.val) + + return () => + h('div', [ + h('p', `Derived: ${derivedVal.value}`), + h( + 'button', + { + onClick: () => store.setState((prev) => prev + 1), + }, + 'Update', + ), + ]) + }) + + const { getByText } = render(Comp) + expect(getByText('Derived: 0')).toBeInTheDocument() + + await user.click(getByText('Update')) + + await waitFor(() => expect(getByText('Derived: 2')).toBeInTheDocument()) + }) +}) + +describe('useStore', () => { + it('is a compatibility alias for useSelector', async () => { + const store = createStore(0) + + const Comp = defineComponent(() => { + const value = useStore(store, (state) => state) + + return () => + h('div', [ + h('p', `Value: ${value.value}`), + h( + 'button', + { + onClick: () => store.setState((prev) => prev + 1), + }, + 'Update', + ), + ]) + }) + + const { getByText } = render(Comp) + expect(getByText('Value: 0')).toBeInTheDocument() + + await user.click(getByText('Update')) + + await waitFor(() => expect(getByText('Value: 1')).toBeInTheDocument()) + }) + + it('supports atom sources through the deprecated alias', async () => { + const atom = createAtom(0) + + const Comp = defineComponent(() => { + const value = useStore(atom, (state) => state) + + return () => + h('div', [ + h('p', `Value: ${value.value}`), + h( + 'button', + { + onClick: () => atom.set((prev) => prev + 1), + }, + 'Update', + ), + ]) + }) + + const { getByText } = render(Comp) + expect(getByText('Value: 0')).toBeInTheDocument() + + await user.click(getByText('Update')) + + await waitFor(() => expect(getByText('Value: 1')).toBeInTheDocument()) + }) +}) + +describe('_useStore', () => { + it('returns selected state and actions for stores with actions', async () => { + const store = createStore({ count: 0 }, ({ setState }) => ({ + inc: () => setState((prev) => ({ count: prev.count + 1 })), + })) + + const Comp = defineComponent(() => { + const [count, { inc }] = _useStore(store, (state) => state.count) + + return () => + h('div', [ + h('p', `Count: ${count.value}`), + h('button', { onClick: () => inc() }, 'Inc'), + ]) + }) + + const { getByText } = render(Comp) + expect(getByText('Count: 0')).toBeInTheDocument() + + await user.click(getByText('Inc')) + + await waitFor(() => expect(getByText('Count: 1')).toBeInTheDocument()) + }) + + it('returns selected state and setState for plain stores', async () => { + const store = createStore(0) + + const Comp = defineComponent(() => { + const [value, setState] = _useStore(store, (state) => state) + + return () => + h('div', [ + h('p', `Value: ${value.value}`), + h('button', { onClick: () => setState((prev) => prev + 1) }, 'Inc'), + ]) + }) + + const { getByText } = render(Comp) + expect(getByText('Value: 0')).toBeInTheDocument() + + await user.click(getByText('Inc')) + + await waitFor(() => expect(getByText('Value: 1')).toBeInTheDocument()) + }) }) describe('shallow', () => { @@ -132,6 +427,20 @@ describe('shallow', () => { expect(shallow(objA, objB)).toBe(false) }) + test('should return true for shallow equal objects with symbol keys', () => { + const sym = Symbol.for('key') + const objA = { [sym]: 1 } + const objB = { [sym]: 1 } + expect(shallow(objA, objB)).toBe(true) + }) + + test('should return false for shallow different values for symbol keys', () => { + const sym = Symbol.for('key') + const objA = { [sym]: 1 } + const objB = { [sym]: 2 } + expect(shallow(objA, objB)).toBe(false) + }) + test('should return true for shallowly equal maps', () => { const objA = new Map([['1', 'hello']]) const objB = new Map([['1', 'hello']]) diff --git a/packages/vue-store/tests/test.test-d.ts b/packages/vue-store/tests/test.test-d.ts index 5b0108b3..f224628f 100644 --- a/packages/vue-store/tests/test.test-d.ts +++ b/packages/vue-store/tests/test.test-d.ts @@ -1,16 +1,70 @@ import { expectTypeOf, test } from 'vitest' -import { createStore } from '@tanstack/store' -import { useStore } from '../src' +import { createAtom, createStore } from '@tanstack/store' +import { _useStore, useAtom, useSelector, useStore, useValue } from '../src' import type { Ref } from 'vue-demi' +import type { Store } from '@tanstack/store' -test('useStore works with derived state', () => { +test('useSelector works with derived state', () => { const store = createStore(12) const derived = createStore(() => store.state * 2) - const val = useStore(derived, (state) => { + const val = useSelector(derived, (state) => { expectTypeOf(state).toEqualTypeOf() return state }) expectTypeOf(val).toEqualTypeOf>>() }) + +test('useValue infers value from mutable and readonly sources', () => { + const writableAtom = createAtom(12) + const readonlyAtom = createAtom(() => 24) + const writableStore = createStore(12) + const readonlyStore = createStore(() => 24) + + expectTypeOf(useValue(writableAtom)).toEqualTypeOf>>() + expectTypeOf(useValue(readonlyAtom)).toEqualTypeOf>>() + expectTypeOf(useValue(writableStore)).toEqualTypeOf>>() + expectTypeOf(useValue(readonlyStore)).toEqualTypeOf>>() +}) + +test('useAtom only accepts writable atoms', () => { + const writableAtom = createAtom(12) + const readonlyAtom = createAtom(() => 24) + + const [value, setValue] = useAtom(writableAtom) + + expectTypeOf(value).toEqualTypeOf>>() + expectTypeOf(setValue).toBeFunction() + // @ts-expect-error readonly atoms cannot be used with useAtom + useAtom(readonlyAtom) +}) + +test('useStore matches useSelector types for compatibility', () => { + const store = createStore(12) + const selectorValue = useSelector(store, (state) => state) + const compatValue = useStore(store, (state) => state) + + expectTypeOf(selectorValue).toEqualTypeOf>>() + expectTypeOf(compatValue).toEqualTypeOf>>() +}) + +test('_useStore returns actions for stores with actions', () => { + const store = createStore({ count: 0 }, ({ setState }) => ({ + inc: () => setState((prev) => ({ count: prev.count + 1 })), + })) + + const [selected, actions] = _useStore(store, (state) => state.count) + + expectTypeOf(selected).toEqualTypeOf>>() + expectTypeOf(actions.inc).toBeFunction() +}) + +test('_useStore returns setState for plain stores', () => { + const store = createStore(0) + + const [selected, setState] = _useStore(store, (state) => state) + + expectTypeOf(selected).toEqualTypeOf>>() + expectTypeOf(setState).toEqualTypeOf['setState']>() +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7e1ba3de..f8c59b6c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -95,6 +95,55 @@ importers: specifier: ^4.1.4 version: 4.1.4(@types/node@25.6.0)(@vitest/coverage-istanbul@4.1.4)(jsdom@29.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + examples/angular/atoms: + dependencies: + '@angular/animations': + specifier: ^21.2.8 + version: 21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)) + '@angular/common': + specifier: ^21.2.8 + version: 21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) + '@angular/compiler': + specifier: ^21.2.8 + version: 21.2.8 + '@angular/core': + specifier: ^21.2.8 + version: 21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/platform-browser': + specifier: ^21.2.8 + version: 21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)) + '@angular/platform-browser-dynamic': + specifier: ^21.2.8 + version: 21.2.8(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/compiler@21.2.8)(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))) + '@angular/router': + specifier: ^21.2.8 + version: 21.2.8(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2) + '@tanstack/angular-store': + specifier: ^0.10.0 + version: link:../../../packages/angular-store + rxjs: + specifier: ^7.8.2 + version: 7.8.2 + tslib: + specifier: ^2.8.1 + version: 2.8.1 + zone.js: + specifier: ^0.16.1 + version: 0.16.1 + devDependencies: + '@angular-devkit/build-angular': + specifier: ^21.2.7 + version: 21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(@angular/compiler@21.2.8)(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@25.6.0)(chokidar@5.0.0)(jiti@2.6.1)(lightningcss@1.32.0)(ng-packagr@21.2.2(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(tslib@2.8.1)(typescript@6.0.2))(tsx@4.21.0)(typescript@6.0.2)(vitest@4.1.4)(yaml@2.8.3) + '@angular/cli': + specifier: ^21.2.7 + version: 21.2.7(@types/node@25.6.0)(chokidar@5.0.0) + '@angular/compiler-cli': + specifier: ^21.2.8 + version: 21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2) + typescript: + specifier: ^6.0.2 + version: 6.0.2 + examples/angular/simple: dependencies: '@angular/animations': @@ -144,6 +193,181 @@ importers: specifier: ^6.0.2 version: 6.0.2 + examples/angular/store-actions: + dependencies: + '@angular/animations': + specifier: ^21.2.8 + version: 21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)) + '@angular/common': + specifier: ^21.2.8 + version: 21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) + '@angular/compiler': + specifier: ^21.2.8 + version: 21.2.8 + '@angular/core': + specifier: ^21.2.8 + version: 21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/platform-browser': + specifier: ^21.2.8 + version: 21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)) + '@angular/platform-browser-dynamic': + specifier: ^21.2.8 + version: 21.2.8(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/compiler@21.2.8)(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))) + '@angular/router': + specifier: ^21.2.8 + version: 21.2.8(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2) + '@tanstack/angular-store': + specifier: ^0.10.0 + version: link:../../../packages/angular-store + rxjs: + specifier: ^7.8.2 + version: 7.8.2 + tslib: + specifier: ^2.8.1 + version: 2.8.1 + zone.js: + specifier: ^0.16.1 + version: 0.16.1 + devDependencies: + '@angular-devkit/build-angular': + specifier: ^21.2.7 + version: 21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(@angular/compiler@21.2.8)(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@25.6.0)(chokidar@5.0.0)(jiti@2.6.1)(lightningcss@1.32.0)(ng-packagr@21.2.2(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(tslib@2.8.1)(typescript@6.0.2))(tsx@4.21.0)(typescript@6.0.2)(vitest@4.1.4)(yaml@2.8.3) + '@angular/cli': + specifier: ^21.2.7 + version: 21.2.7(@types/node@25.6.0)(chokidar@5.0.0) + '@angular/compiler-cli': + specifier: ^21.2.8 + version: 21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2) + typescript: + specifier: ^6.0.2 + version: 6.0.2 + + examples/angular/store-context: + dependencies: + '@angular/animations': + specifier: ^21.2.8 + version: 21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)) + '@angular/common': + specifier: ^21.2.8 + version: 21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) + '@angular/compiler': + specifier: ^21.2.8 + version: 21.2.8 + '@angular/core': + specifier: ^21.2.8 + version: 21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/platform-browser': + specifier: ^21.2.8 + version: 21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)) + '@angular/platform-browser-dynamic': + specifier: ^21.2.8 + version: 21.2.8(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/compiler@21.2.8)(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))) + '@angular/router': + specifier: ^21.2.8 + version: 21.2.8(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2) + '@tanstack/angular-store': + specifier: ^0.10.0 + version: link:../../../packages/angular-store + rxjs: + specifier: ^7.8.2 + version: 7.8.2 + tslib: + specifier: ^2.8.1 + version: 2.8.1 + zone.js: + specifier: ^0.16.1 + version: 0.16.1 + devDependencies: + '@angular-devkit/build-angular': + specifier: ^21.2.7 + version: 21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(@angular/compiler@21.2.8)(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@25.6.0)(chokidar@5.0.0)(jiti@2.6.1)(lightningcss@1.32.0)(ng-packagr@21.2.2(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(tslib@2.8.1)(typescript@6.0.2))(tsx@4.21.0)(typescript@6.0.2)(vitest@4.1.4)(yaml@2.8.3) + '@angular/cli': + specifier: ^21.2.7 + version: 21.2.7(@types/node@25.6.0)(chokidar@5.0.0) + '@angular/compiler-cli': + specifier: ^21.2.8 + version: 21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2) + typescript: + specifier: ^6.0.2 + version: 6.0.2 + + examples/angular/stores: + dependencies: + '@angular/animations': + specifier: ^21.2.8 + version: 21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)) + '@angular/common': + specifier: ^21.2.8 + version: 21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) + '@angular/compiler': + specifier: ^21.2.8 + version: 21.2.8 + '@angular/core': + specifier: ^21.2.8 + version: 21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/platform-browser': + specifier: ^21.2.8 + version: 21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)) + '@angular/platform-browser-dynamic': + specifier: ^21.2.8 + version: 21.2.8(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/compiler@21.2.8)(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))) + '@angular/router': + specifier: ^21.2.8 + version: 21.2.8(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2) + '@tanstack/angular-store': + specifier: ^0.10.0 + version: link:../../../packages/angular-store + rxjs: + specifier: ^7.8.2 + version: 7.8.2 + tslib: + specifier: ^2.8.1 + version: 2.8.1 + zone.js: + specifier: ^0.16.1 + version: 0.16.1 + devDependencies: + '@angular-devkit/build-angular': + specifier: ^21.2.7 + version: 21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(@angular/compiler@21.2.8)(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@25.6.0)(chokidar@5.0.0)(jiti@2.6.1)(lightningcss@1.32.0)(ng-packagr@21.2.2(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(tslib@2.8.1)(typescript@6.0.2))(tsx@4.21.0)(typescript@6.0.2)(vitest@4.1.4)(yaml@2.8.3) + '@angular/cli': + specifier: ^21.2.7 + version: 21.2.7(@types/node@25.6.0)(chokidar@5.0.0) + '@angular/compiler-cli': + specifier: ^21.2.8 + version: 21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2) + typescript: + specifier: ^6.0.2 + version: 6.0.2 + + examples/preact/atoms: + dependencies: + '@tanstack/preact-store': + specifier: ^0.12.0 + version: link:../../../packages/preact-store + preact: + specifier: ^10.29.1 + version: 10.29.1 + devDependencies: + '@preact/preset-vite': + specifier: ^2.10.5 + version: 2.10.5(@babel/core@7.29.0)(preact@10.29.1)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@types/node': + specifier: ^25.6.0 + version: 25.6.0 + eslint: + specifier: ^10.2.0 + version: 10.2.0(jiti@2.6.1) + eslint-config-preact: + specifier: ^2.0.0 + version: 2.0.0(eslint@10.2.0(jiti@2.6.1)) + typescript: + specifier: 6.0.2 + version: 6.0.2 + vite: + specifier: ^8.0.8 + version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + examples/preact/simple: dependencies: '@tanstack/preact-store': @@ -172,6 +396,90 @@ importers: specifier: ^8.0.8 version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + examples/preact/store-actions: + dependencies: + '@tanstack/preact-store': + specifier: ^0.12.0 + version: link:../../../packages/preact-store + preact: + specifier: ^10.29.1 + version: 10.29.1 + devDependencies: + '@preact/preset-vite': + specifier: ^2.10.5 + version: 2.10.5(@babel/core@7.29.0)(preact@10.29.1)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@types/node': + specifier: ^25.6.0 + version: 25.6.0 + eslint: + specifier: ^10.2.0 + version: 10.2.0(jiti@2.6.1) + eslint-config-preact: + specifier: ^2.0.0 + version: 2.0.0(eslint@10.2.0(jiti@2.6.1)) + typescript: + specifier: 6.0.2 + version: 6.0.2 + vite: + specifier: ^8.0.8 + version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + + examples/preact/store-context: + dependencies: + '@tanstack/preact-store': + specifier: ^0.12.0 + version: link:../../../packages/preact-store + preact: + specifier: ^10.29.1 + version: 10.29.1 + devDependencies: + '@preact/preset-vite': + specifier: ^2.10.5 + version: 2.10.5(@babel/core@7.29.0)(preact@10.29.1)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@types/node': + specifier: ^25.6.0 + version: 25.6.0 + eslint: + specifier: ^10.2.0 + version: 10.2.0(jiti@2.6.1) + eslint-config-preact: + specifier: ^2.0.0 + version: 2.0.0(eslint@10.2.0(jiti@2.6.1)) + typescript: + specifier: 6.0.2 + version: 6.0.2 + vite: + specifier: ^8.0.8 + version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + + examples/preact/stores: + dependencies: + '@tanstack/preact-store': + specifier: ^0.12.0 + version: link:../../../packages/preact-store + preact: + specifier: ^10.29.1 + version: 10.29.1 + devDependencies: + '@preact/preset-vite': + specifier: ^2.10.5 + version: 2.10.5(@babel/core@7.29.0)(preact@10.29.1)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@types/node': + specifier: ^25.6.0 + version: 25.6.0 + eslint: + specifier: ^10.2.0 + version: 10.2.0(jiti@2.6.1) + eslint-config-preact: + specifier: ^2.0.0 + version: 2.0.0(eslint@10.2.0(jiti@2.6.1)) + typescript: + specifier: 6.0.2 + version: 6.0.2 + vite: + specifier: ^8.0.8 + version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + examples/react/atoms: dependencies: '@tanstack/react-store': @@ -247,76 +555,264 @@ importers: specifier: ^8.0.8 version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) - examples/react/store-context: + examples/react/store-context: + dependencies: + '@tanstack/react-store': + specifier: ^0.10.0 + version: link:../../../packages/react-store + react: + specifier: ^19.2.5 + version: 19.2.5 + react-dom: + specifier: ^19.2.5 + version: 19.2.5(react@19.2.5) + devDependencies: + '@types/react': + specifier: ^19.2.14 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.14) + '@vitejs/plugin-react': + specifier: ^6.0.1 + version: 6.0.1(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + vite: + specifier: ^8.0.8 + version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + + examples/react/stores: + dependencies: + '@tanstack/react-store': + specifier: ^0.10.0 + version: link:../../../packages/react-store + react: + specifier: ^19.2.5 + version: 19.2.5 + react-dom: + specifier: ^19.2.5 + version: 19.2.5(react@19.2.5) + devDependencies: + '@types/react': + specifier: ^19.2.14 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.14) + '@vitejs/plugin-react': + specifier: ^6.0.1 + version: 6.0.1(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + vite: + specifier: ^8.0.8 + version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + + examples/solid/atoms: + dependencies: + '@tanstack/solid-store': + specifier: ^0.10.0 + version: link:../../../packages/solid-store + solid-js: + specifier: ^1.9.12 + version: 1.9.12 + devDependencies: + typescript: + specifier: 6.0.2 + version: 6.0.2 + vite: + specifier: ^8.0.8 + version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite-plugin-solid: + specifier: ^2.11.12 + version: 2.11.12(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + + examples/solid/simple: + dependencies: + '@tanstack/solid-store': + specifier: ^0.10.0 + version: link:../../../packages/solid-store + solid-js: + specifier: ^1.9.12 + version: 1.9.12 + devDependencies: + typescript: + specifier: 6.0.2 + version: 6.0.2 + vite: + specifier: ^8.0.8 + version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite-plugin-solid: + specifier: ^2.11.12 + version: 2.11.12(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + + examples/solid/store-actions: + dependencies: + '@tanstack/solid-store': + specifier: ^0.10.0 + version: link:../../../packages/solid-store + solid-js: + specifier: ^1.9.12 + version: 1.9.12 + devDependencies: + typescript: + specifier: 6.0.2 + version: 6.0.2 + vite: + specifier: ^8.0.8 + version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite-plugin-solid: + specifier: ^2.11.12 + version: 2.11.12(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + + examples/solid/store-context: + dependencies: + '@tanstack/solid-store': + specifier: ^0.10.0 + version: link:../../../packages/solid-store + solid-js: + specifier: ^1.9.12 + version: 1.9.12 + devDependencies: + typescript: + specifier: 6.0.2 + version: 6.0.2 + vite: + specifier: ^8.0.8 + version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite-plugin-solid: + specifier: ^2.11.12 + version: 2.11.12(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + + examples/solid/stores: + dependencies: + '@tanstack/solid-store': + specifier: ^0.10.0 + version: link:../../../packages/solid-store + solid-js: + specifier: ^1.9.12 + version: 1.9.12 + devDependencies: + typescript: + specifier: 6.0.2 + version: 6.0.2 + vite: + specifier: ^8.0.8 + version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vite-plugin-solid: + specifier: ^2.11.12 + version: 2.11.12(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + + examples/svelte/atoms: + dependencies: + '@tanstack/svelte-store': + specifier: ^0.11.0 + version: link:../../../packages/svelte-store + devDependencies: + '@sveltejs/vite-plugin-svelte': + specifier: ^7.0.0 + version: 7.0.0(svelte@5.55.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@tsconfig/svelte': + specifier: ^5.0.8 + version: 5.0.8 + svelte: + specifier: ^5.55.3 + version: 5.55.3 + svelte-check: + specifier: ^4.4.6 + version: 4.4.6(picomatch@4.0.4)(svelte@5.55.3)(typescript@6.0.2) + tslib: + specifier: ^2.8.1 + version: 2.8.1 + typescript: + specifier: 6.0.2 + version: 6.0.2 + vite: + specifier: ^8.0.8 + version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + + examples/svelte/simple: dependencies: - '@tanstack/react-store': - specifier: ^0.10.0 - version: link:../../../packages/react-store - react: - specifier: ^19.2.5 - version: 19.2.5 - react-dom: - specifier: ^19.2.5 - version: 19.2.5(react@19.2.5) + '@tanstack/svelte-store': + specifier: ^0.11.0 + version: link:../../../packages/svelte-store devDependencies: - '@types/react': - specifier: ^19.2.14 - version: 19.2.14 - '@types/react-dom': - specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.14) - '@vitejs/plugin-react': - specifier: ^6.0.1 - version: 6.0.1(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@sveltejs/vite-plugin-svelte': + specifier: ^7.0.0 + version: 7.0.0(svelte@5.55.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@tsconfig/svelte': + specifier: ^5.0.8 + version: 5.0.8 + svelte: + specifier: ^5.55.3 + version: 5.55.3 + svelte-check: + specifier: ^4.4.6 + version: 4.4.6(picomatch@4.0.4)(svelte@5.55.3)(typescript@6.0.2) + tslib: + specifier: ^2.8.1 + version: 2.8.1 + typescript: + specifier: 6.0.2 + version: 6.0.2 vite: specifier: ^8.0.8 version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) - examples/react/stores: + examples/svelte/store-actions: dependencies: - '@tanstack/react-store': - specifier: ^0.10.0 - version: link:../../../packages/react-store - react: - specifier: ^19.2.5 - version: 19.2.5 - react-dom: - specifier: ^19.2.5 - version: 19.2.5(react@19.2.5) + '@tanstack/svelte-store': + specifier: ^0.11.0 + version: link:../../../packages/svelte-store devDependencies: - '@types/react': - specifier: ^19.2.14 - version: 19.2.14 - '@types/react-dom': - specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.14) - '@vitejs/plugin-react': - specifier: ^6.0.1 - version: 6.0.1(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@sveltejs/vite-plugin-svelte': + specifier: ^7.0.0 + version: 7.0.0(svelte@5.55.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@tsconfig/svelte': + specifier: ^5.0.8 + version: 5.0.8 + svelte: + specifier: ^5.55.3 + version: 5.55.3 + svelte-check: + specifier: ^4.4.6 + version: 4.4.6(picomatch@4.0.4)(svelte@5.55.3)(typescript@6.0.2) + tslib: + specifier: ^2.8.1 + version: 2.8.1 + typescript: + specifier: 6.0.2 + version: 6.0.2 vite: specifier: ^8.0.8 version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) - examples/solid/simple: + examples/svelte/store-context: dependencies: - '@tanstack/solid-store': - specifier: ^0.10.0 - version: link:../../../packages/solid-store - solid-js: - specifier: ^1.9.12 - version: 1.9.12 + '@tanstack/svelte-store': + specifier: ^0.11.0 + version: link:../../../packages/svelte-store devDependencies: + '@sveltejs/vite-plugin-svelte': + specifier: ^7.0.0 + version: 7.0.0(svelte@5.55.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + '@tsconfig/svelte': + specifier: ^5.0.8 + version: 5.0.8 + svelte: + specifier: ^5.55.3 + version: 5.55.3 + svelte-check: + specifier: ^4.4.6 + version: 4.4.6(picomatch@4.0.4)(svelte@5.55.3)(typescript@6.0.2) + tslib: + specifier: ^2.8.1 + version: 2.8.1 typescript: specifier: 6.0.2 version: 6.0.2 vite: specifier: ^8.0.8 version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) - vite-plugin-solid: - specifier: ^2.11.12 - version: 2.11.12(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) - examples/svelte/simple: + examples/svelte/stores: dependencies: '@tanstack/svelte-store': specifier: ^0.11.0 @@ -344,6 +840,28 @@ importers: specifier: ^8.0.8 version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + examples/vue/atoms: + dependencies: + '@tanstack/vue-store': + specifier: ^0.10.0 + version: link:../../../packages/vue-store + vue: + specifier: ^3.5.32 + version: 3.5.32(typescript@6.0.2) + devDependencies: + '@vitejs/plugin-vue': + specifier: ^6.0.5 + version: 6.0.5(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@6.0.2)) + typescript: + specifier: 6.0.2 + version: 6.0.2 + vite: + specifier: ^8.0.8 + version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vue-tsc: + specifier: ^3.2.6 + version: 3.2.6(typescript@6.0.2) + examples/vue/simple: dependencies: '@tanstack/vue-store': @@ -366,6 +884,72 @@ importers: specifier: ^3.2.6 version: 3.2.6(typescript@6.0.2) + examples/vue/store-actions: + dependencies: + '@tanstack/vue-store': + specifier: ^0.10.0 + version: link:../../../packages/vue-store + vue: + specifier: ^3.5.32 + version: 3.5.32(typescript@6.0.2) + devDependencies: + '@vitejs/plugin-vue': + specifier: ^6.0.5 + version: 6.0.5(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@6.0.2)) + typescript: + specifier: 6.0.2 + version: 6.0.2 + vite: + specifier: ^8.0.8 + version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vue-tsc: + specifier: ^3.2.6 + version: 3.2.6(typescript@6.0.2) + + examples/vue/store-context: + dependencies: + '@tanstack/vue-store': + specifier: ^0.10.0 + version: link:../../../packages/vue-store + vue: + specifier: ^3.5.32 + version: 3.5.32(typescript@6.0.2) + devDependencies: + '@vitejs/plugin-vue': + specifier: ^6.0.5 + version: 6.0.5(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@6.0.2)) + typescript: + specifier: 6.0.2 + version: 6.0.2 + vite: + specifier: ^8.0.8 + version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vue-tsc: + specifier: ^3.2.6 + version: 3.2.6(typescript@6.0.2) + + examples/vue/stores: + dependencies: + '@tanstack/vue-store': + specifier: ^0.10.0 + version: link:../../../packages/vue-store + vue: + specifier: ^3.5.32 + version: 3.5.32(typescript@6.0.2) + devDependencies: + '@vitejs/plugin-vue': + specifier: ^6.0.5 + version: 6.0.5(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@6.0.2)) + typescript: + specifier: 6.0.2 + version: 6.0.2 + vite: + specifier: ^8.0.8 + version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) + vue-tsc: + specifier: ^3.2.6 + version: 3.2.6(typescript@6.0.2) + packages/angular-store: dependencies: '@tanstack/store': @@ -8773,7 +9357,7 @@ snapshots: tree-kill: 1.2.2 tslib: 2.8.1 typescript: 6.0.2 - webpack: 5.105.2(esbuild@0.27.4) + webpack: 5.105.2(esbuild@0.27.3) webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.105.2(esbuild@0.27.3)) webpack-dev-server: 5.2.3(tslib@2.8.1)(webpack@5.105.2(esbuild@0.27.3)) webpack-merge: 6.0.1 @@ -8812,7 +9396,7 @@ snapshots: dependencies: '@angular-devkit/architect': 0.2102.7(chokidar@5.0.0) rxjs: 7.8.2 - webpack: 5.105.2(esbuild@0.27.4) + webpack: 5.105.2(esbuild@0.27.3) webpack-dev-server: 5.2.3(tslib@2.8.1)(webpack@5.105.2(esbuild@0.27.3)) transitivePeerDependencies: - chokidar @@ -8883,7 +9467,7 @@ snapshots: lmdb: 3.5.1 ng-packagr: 21.2.2(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(tslib@2.8.1)(typescript@6.0.2) postcss: 8.5.6 - vitest: 4.1.4(@types/node@25.6.0)(@vitest/coverage-istanbul@4.1.4)(jsdom@29.0.2)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.4(@types/node@25.6.0)(@vitest/coverage-istanbul@4.1.4)(jsdom@29.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -10779,7 +11363,7 @@ snapshots: dependencies: '@angular/compiler-cli': 21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2) typescript: 6.0.2 - webpack: 5.105.2(esbuild@0.27.4) + webpack: 5.105.2(esbuild@0.27.3) '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': dependencies: @@ -12122,7 +12706,7 @@ snapshots: magicast: 0.5.2 obug: 2.1.1 tinyrainbow: 3.1.0 - vitest: 4.1.4(@types/node@25.6.0)(@vitest/coverage-istanbul@4.1.4)(jsdom@29.0.2)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.4(@types/node@25.6.0)(@vitest/coverage-istanbul@4.1.4)(jsdom@29.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - supports-color @@ -12135,14 +12719,6 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.4(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': - dependencies: - '@vitest/spy': 4.1.4 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) - '@vitest/mocker@4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.1.4 @@ -12606,7 +13182,7 @@ snapshots: dependencies: '@babel/core': 7.29.0 find-up: 5.0.0 - webpack: 5.105.2(esbuild@0.27.4) + webpack: 5.105.2(esbuild@0.27.3) babel-plugin-jsx-dom-expressions@0.40.6(@babel/core@7.29.0): dependencies: @@ -13004,7 +13580,7 @@ snapshots: schema-utils: 4.3.3 serialize-javascript: 7.0.5 tinyglobby: 0.2.16 - webpack: 5.105.2(esbuild@0.27.4) + webpack: 5.105.2(esbuild@0.27.3) core-js-compat@3.49.0: dependencies: @@ -13043,7 +13619,7 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.7.4 optionalDependencies: - webpack: 5.105.2(esbuild@0.27.4) + webpack: 5.105.2(esbuild@0.27.3) css-select@5.2.2: dependencies: @@ -14763,7 +15339,7 @@ snapshots: dependencies: less: 4.4.2 optionalDependencies: - webpack: 5.105.2(esbuild@0.27.4) + webpack: 5.105.2(esbuild@0.27.3) less@4.4.2: dependencies: @@ -14802,7 +15378,7 @@ snapshots: dependencies: webpack-sources: 3.3.4 optionalDependencies: - webpack: 5.105.2(esbuild@0.27.4) + webpack: 5.105.2(esbuild@0.27.3) lightningcss-android-arm64@1.32.0: optional: true @@ -15073,7 +15649,7 @@ snapshots: dependencies: schema-utils: 4.3.3 tapable: 2.3.2 - webpack: 5.105.2(esbuild@0.27.4) + webpack: 5.105.2(esbuild@0.27.3) minimalistic-assert@1.0.1: {} @@ -15726,7 +16302,7 @@ snapshots: postcss: 8.5.6 semver: 7.7.4 optionalDependencies: - webpack: 5.105.2(esbuild@0.27.4) + webpack: 5.105.2(esbuild@0.27.3) transitivePeerDependencies: - typescript @@ -16215,7 +16791,7 @@ snapshots: neo-async: 2.6.2 optionalDependencies: sass: 1.97.3 - webpack: 5.105.2(esbuild@0.27.4) + webpack: 5.105.2(esbuild@0.27.3) sass@1.97.3: dependencies: @@ -16514,7 +17090,7 @@ snapshots: dependencies: iconv-lite: 0.6.3 source-map-js: 1.2.1 - webpack: 5.105.2(esbuild@0.27.4) + webpack: 5.105.2(esbuild@0.27.3) source-map-support@0.5.21: dependencies: @@ -16760,15 +17336,15 @@ snapshots: term-size@2.2.1: {} - terser-webpack-plugin@5.4.0(esbuild@0.27.4)(webpack@5.105.2(esbuild@0.27.3)): + terser-webpack-plugin@5.4.0(esbuild@0.27.3)(webpack@5.105.2(esbuild@0.27.3)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 terser: 5.46.0 - webpack: 5.105.2(esbuild@0.27.4) + webpack: 5.105.2(esbuild@0.27.3) optionalDependencies: - esbuild: 0.27.4 + esbuild: 0.27.3 terser@5.46.0: dependencies: @@ -17170,35 +17746,6 @@ snapshots: optionalDependencies: vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) - vitest@4.1.4(@types/node@25.6.0)(@vitest/coverage-istanbul@4.1.4)(jsdom@29.0.2)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): - dependencies: - '@vitest/expect': 4.1.4 - '@vitest/mocker': 4.1.4(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)) - '@vitest/pretty-format': 4.1.4 - '@vitest/runner': 4.1.4 - '@vitest/snapshot': 4.1.4 - '@vitest/spy': 4.1.4 - '@vitest/utils': 4.1.4 - es-module-lexer: 2.0.0 - expect-type: 1.3.0 - magic-string: 0.30.21 - obug: 2.1.1 - pathe: 2.0.3 - picomatch: 4.0.4 - std-env: 4.0.0 - tinybench: 2.9.0 - tinyexec: 1.0.4 - tinyglobby: 0.2.16 - tinyrainbow: 3.1.0 - vite: 7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 25.6.0 - '@vitest/coverage-istanbul': 4.1.4(vitest@4.1.4) - jsdom: 29.0.2 - transitivePeerDependencies: - - msw - vitest@4.1.4(@types/node@25.6.0)(@vitest/coverage-istanbul@4.1.4)(jsdom@29.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.6.4)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.4 @@ -17308,7 +17855,7 @@ snapshots: range-parser: 1.2.1 schema-utils: 4.3.3 optionalDependencies: - webpack: 5.105.2(esbuild@0.27.4) + webpack: 5.105.2(esbuild@0.27.3) transitivePeerDependencies: - tslib @@ -17343,7 +17890,7 @@ snapshots: webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.105.2(esbuild@0.27.3)) ws: 8.20.0 optionalDependencies: - webpack: 5.105.2(esbuild@0.27.4) + webpack: 5.105.2(esbuild@0.27.3) transitivePeerDependencies: - bufferutil - debug @@ -17362,9 +17909,9 @@ snapshots: webpack-subresource-integrity@5.1.0(webpack@5.105.2(esbuild@0.27.3)): dependencies: typed-assert: 1.0.9 - webpack: 5.105.2(esbuild@0.27.4) + webpack: 5.105.2(esbuild@0.27.3) - webpack@5.105.2(esbuild@0.27.4): + webpack@5.105.2(esbuild@0.27.3): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -17388,7 +17935,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.2 - terser-webpack-plugin: 5.4.0(esbuild@0.27.4)(webpack@5.105.2(esbuild@0.27.3)) + terser-webpack-plugin: 5.4.0(esbuild@0.27.3)(webpack@5.105.2(esbuild@0.27.3)) watchpack: 2.5.1 webpack-sources: 3.3.4 transitivePeerDependencies: