Skip to content

Commit bd0dc12

Browse files
committed
alternative _injectStore for angular
1 parent 6da0820 commit bd0dc12

5 files changed

Lines changed: 76 additions & 46 deletions

File tree

docs/framework/angular/reference/functions/injectStore.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,16 @@ title: _injectStore
99
function _injectStore<TState, TActions, TSelected>(
1010
store,
1111
selector,
12-
options?): [Signal<TSelected>, [TActions] extends [never] ? (updater) => void : TActions];
12+
options?): WritableStoreSliceSignal<TState, TSelected, TActions>;
1313
```
1414

15-
Defined in: [packages/angular-store/src/\_injectStore.ts:24](https://github.com/TanStack/store/blob/main/packages/angular-store/src/_injectStore.ts#L24)
15+
Defined in: [packages/angular-store/src/\_injectStore.ts:34](https://github.com/TanStack/store/blob/main/packages/angular-store/src/_injectStore.ts#L34)
1616

1717
Experimental combined read+write injection function for stores, mirroring
1818
injectAtom's pattern.
1919

20-
Returns `[signal, actions]` when the store has an actions factory, or
21-
`[signal, setState]` for plain stores.
20+
Returns a callable slice with methods when the store has an actions factory, or
21+
with only the setState method for plain stores.
2222

2323
## Type Parameters
2424

@@ -38,7 +38,7 @@ Returns `[signal, actions]` when the store has an actions factory, or
3838

3939
### store
4040

41-
`Store`\<`TState`, `TActions`\>
41+
`Store`\<`TState`, `TActions`\> | () => `Store`\<`TState`, `TActions`\>
4242

4343
### selector
4444

@@ -50,16 +50,16 @@ Returns `[signal, actions]` when the store has an actions factory, or
5050

5151
## Returns
5252

53-
\[`Signal`\<`TSelected`\>, \[`TActions`\] *extends* \[`never`\] ? (`updater`) => `void` : `TActions`\]
53+
`WritableStoreSliceSignal`\<`TState`, `TSelected`, `TActions`\>
5454

5555
## Example
5656

5757
```ts
5858
// Store with actions
59-
readonly result = _injectStore(petStore, (s) => s.cats)
60-
// result[0] is Signal<number>, result[1] is actions
59+
readonly dogs = _injectStore(petStore, (s) => s.dogs)
60+
// dogs() and dogs.addDog()
6161

6262
// Store without actions
63-
readonly result = _injectStore(plainStore, (s) => s)
64-
// result[0] is Signal<number>, result[1] is setState
63+
readonly value = _injectStore(plainStore, (s) => s)
64+
// value() and value.setState(...)
6565
```

examples/angular/store-actions/src/app/app.component.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ const petStore = createStore(
4646
</div>
4747
<div>
4848
<p>Dogs: {{ dogs() }}</p>
49-
<button type="button" (click)="dogActions.addDog()">
49+
<button type="button" (click)="dogs.addDog()">
5050
Vote for dogs
5151
</button>
5252
</div>
@@ -59,10 +59,8 @@ export class AppComponent {
5959
cats = injectSelector(petStore, (state) => state.cats)
6060
addCat = petStore.actions.addCat
6161

62-
// _injectStore gives both the selected signal and actions in a single tuple
63-
private dogResult = _injectStore(petStore, (state) => state.dogs)
64-
dogs = this.dogResult[0]
65-
dogActions = this.dogResult[1]
62+
// _injectStore: callable slice for reads; action methods for writes
63+
dogs = _injectStore(petStore, (state) => state.dogs)
6664

6765
total = injectSelector(petStore, (state) => state.cats + state.dogs)
6866

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,66 @@
1+
import { untracked } from '@angular/core'
12
import { injectSelector } from './injectSelector'
23
import type { Signal } from '@angular/core'
34
import type { Store, StoreActionMap } from '@tanstack/store'
45
import type { InjectSelectorOptions } from './injectSelector'
56

7+
type WritableStoreSliceSignal<
8+
TState,
9+
TSelected,
10+
TActions extends StoreActionMap,
11+
> = Signal<TSelected> &
12+
([TActions] extends [never]
13+
? Pick<Store<TState>, 'setState'>
14+
: TActions)
15+
616
/**
717
* Experimental combined read+write injection function for stores, mirroring
818
* injectAtom's pattern.
9-
*
10-
* Returns `[signal, actions]` when the store has an actions factory, or
11-
* `[signal, setState]` for plain stores.
19+
*
20+
* Returns a callable slice with methods when the store has an actions factory, or
21+
* with only the setState method for plain stores.
1222
*
1323
* @example
1424
* ```ts
1525
* // Store with actions
16-
* readonly result = _injectStore(petStore, (s) => s.cats)
17-
* // result[0] is Signal<number>, result[1] is actions
26+
* readonly dogs = _injectStore(petStore, (s) => s.dogs)
27+
* // dogs() and dogs.addDog()
1828
*
1929
* // Store without actions
20-
* readonly result = _injectStore(plainStore, (s) => s)
21-
* // result[0] is Signal<number>, result[1] is setState
30+
* readonly value = _injectStore(plainStore, (s) => s)
31+
* // value() and value.setState(...)
2232
* ```
2333
*/
2434
export function _injectStore<
2535
TState,
2636
TActions extends StoreActionMap,
2737
TSelected = NoInfer<TState>,
2838
>(
29-
store: Store<TState, TActions>,
39+
store: Store<TState, TActions> | (() => Store<TState, TActions>),
3040
selector: (state: NoInfer<TState>) => TSelected,
3141
options?: InjectSelectorOptions<TSelected>,
32-
): [
33-
Signal<TSelected>,
34-
[TActions] extends [never] ? Store<TState>['setState'] : TActions,
35-
] {
42+
): WritableStoreSliceSignal<TState, TSelected, TActions> {
3643
const selected = injectSelector(store, selector, options)
37-
const actionsOrSetState =
38-
(store.actions as StoreActionMap | undefined) ?? store.setState
3944

40-
return [selected, actionsOrSetState] as any
45+
return new Proxy(selected, {
46+
apply: () => selected(),
47+
get(_target, prop, receiver) {
48+
const inst = untracked(() =>
49+
typeof store === 'function' ? store() : store,
50+
)
51+
52+
const actions = inst.actions as StoreActionMap | undefined
53+
54+
if (actions != null && typeof actions === 'object') {
55+
const method = Reflect.get(actions, prop, actions)
56+
if (Object.hasOwn(actions, prop) && typeof method === 'function') {
57+
return method
58+
}
59+
} else if (prop === 'setState' && typeof inst.setState === 'function') {
60+
return inst.setState
61+
}
62+
63+
return Reflect.get(selected, prop, receiver)
64+
},
65+
}) as WritableStoreSliceSignal<TState, TSelected, TActions>
4166
}

packages/angular-store/tests/index.test.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
effect,
66
input,
77
inputBinding,
8+
isSignal,
89
signal,
910
untracked,
1011
} from '@angular/core'
@@ -496,6 +497,14 @@ describe('dataType', () => {
496497
})
497498

498499
describe('_injectStore', () => {
500+
test('return value passes isSignal (proxies the selector signal)', () => {
501+
TestBed.runInInjectionContext(() => {
502+
const store = createStore(0)
503+
const slice = _injectStore(store, (s) => s)
504+
expect(isSignal(slice)).toBe(true)
505+
})
506+
})
507+
499508
test('returns selected state and actions for stores with actions', () => {
500509
const store = createStore({ count: 0 }, ({ setState }) => ({
501510
inc: () => setState((prev) => ({ count: prev.count + 1 })),
@@ -511,12 +520,10 @@ describe('_injectStore', () => {
511520
standalone: true,
512521
})
513522
class MyCmp {
514-
private result = _injectStore(store, (state) => state.count)
515-
count = this.result[0]
516-
actions = this.result[1]
523+
protected count = _injectStore(store, (state) => state.count)
517524

518525
inc() {
519-
this.actions.inc()
526+
this.count.inc()
520527
}
521528
}
522529

@@ -550,12 +557,10 @@ describe('_injectStore', () => {
550557
standalone: true,
551558
})
552559
class MyCmp {
553-
private result = _injectStore(store, (state) => state)
554-
value = this.result[0]
555-
setState = this.result[1]
560+
private value = _injectStore(store, (state) => state)
556561

557562
inc() {
558-
this.setState((prev) => prev + 1)
563+
this.value.setState((prev: number) => prev + 1)
559564
}
560565
}
561566

packages/angular-store/tests/test.test-d.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,22 +73,24 @@ test('createStoreContext preserves typed context shape', () => {
7373
expectTypeOf(ctx.petStore).toEqualTypeOf<Store<{ cats: number }>>()
7474
})
7575

76-
test('_injectStore returns actions for stores with actions', () => {
76+
test('_injectStore returns callable slice with actions for stores with actions', () => {
7777
const store = createStore({ count: 0 }, ({ setState }) => ({
7878
inc: () => setState((prev) => ({ count: prev.count + 1 })),
7979
}))
8080

81-
const [selected, actions] = _injectStore(store, (state) => state.count)
81+
const slice = _injectStore(store, (state) => state.count)
8282

83-
expectTypeOf(selected).toEqualTypeOf<Signal<number>>()
84-
expectTypeOf(actions.inc).toBeFunction()
83+
expectTypeOf(slice).toEqualTypeOf<
84+
Signal<number> & { inc: () => void }
85+
>()
8586
})
8687

87-
test('_injectStore returns setState for plain stores', () => {
88+
test('_injectStore returns callable slice with setState for plain stores', () => {
8889
const store = createStore(0)
8990

90-
const [selected, setState] = _injectStore(store, (state) => state)
91+
const slice = _injectStore(store, (state) => state)
9192

92-
expectTypeOf(selected).toEqualTypeOf<Signal<number>>()
93-
expectTypeOf(setState).toEqualTypeOf<Store<number>['setState']>()
93+
expectTypeOf(slice).toEqualTypeOf<
94+
Signal<number> & { setState: Store<number>['setState'] }
95+
>()
9496
})

0 commit comments

Comments
 (0)