Skip to content

Commit a8526fc

Browse files
committed
feat: add action helper to consistently $onAction
- See #1400
1 parent 6962e11 commit a8526fc

File tree

3 files changed

+77
-28
lines changed

3 files changed

+77
-28
lines changed

packages/pinia/__tests__/onAction.spec.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,29 @@ describe('Subscriptions', () => {
144144
expect(func2).toHaveBeenCalledTimes(1)
145145
})
146146

147+
it('can listen to setup actions within other actions thanks to `action`', () => {
148+
const store = defineStore('id', ({ action }) => {
149+
const a1 = action(() => 1)
150+
const a2 = action(() => a1() * 2)
151+
return { a1, a2 }
152+
})()
153+
const spy = vi.fn()
154+
store.$onAction(spy)
155+
store.a1()
156+
expect(spy).toHaveBeenCalledTimes(1)
157+
158+
store.a2()
159+
expect(spy).toHaveBeenCalledTimes(3)
160+
expect(spy).toHaveBeenNthCalledWith(
161+
2,
162+
expect.objectContaining({ name: 'a2' })
163+
)
164+
expect(spy).toHaveBeenNthCalledWith(
165+
3,
166+
expect.objectContaining({ name: 'a1' })
167+
)
168+
})
169+
147170
describe('multiple store instances', () => {
148171
const useStore = defineStore({
149172
id: 'main',

packages/pinia/src/store.ts

Lines changed: 49 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,26 @@ const fallbackRunWithContext = (fn: () => unknown) => fn()
5656

5757
type _ArrayType<AT> = AT extends Array<infer T> ? T : never
5858

59+
/**
60+
* Marks a function as an action for `$onAction`
61+
* @internal
62+
*/
63+
const ACTION_MARKER = Symbol()
64+
/**
65+
* Action name symbol. Allows to add a name to an action after defining it
66+
* @internal
67+
*/
68+
const ACTION_NAME = Symbol()
69+
/**
70+
* Function type extended with action markers
71+
* @internal
72+
*/
73+
interface MarkedAction<Fn extends _Method = _Method> {
74+
(...args: Parameters<Fn>): ReturnType<Fn>
75+
[ACTION_MARKER]: boolean
76+
[ACTION_NAME]: string
77+
}
78+
5979
function mergeReactiveObjects<
6080
T extends Record<any, unknown> | Map<unknown, unknown> | Set<unknown>,
6181
>(target: T, patchToApply: _DeepPartial<T>): T {
@@ -211,7 +231,7 @@ function createSetupStore<
211231
A extends _ActionsTree,
212232
>(
213233
$id: Id,
214-
setup: () => SS,
234+
setup: (helpers: SetupStoreHelpers) => SS,
215235
options:
216236
| DefineSetupStoreOptions<Id, S, G, A>
217237
| DefineStoreOptions<Id, S, G, A> = {},
@@ -350,14 +370,18 @@ function createSetupStore<
350370
}
351371

352372
/**
353-
* Wraps an action to handle subscriptions.
354-
*
373+
* Helper that wraps function so it can be tracked with $onAction
374+
* @param fn - action to wrap
355375
* @param name - name of the action
356-
* @param action - action to wrap
357-
* @returns a wrapped action to handle subscriptions
358376
*/
359-
function wrapAction(name: string, action: _Method) {
360-
return function (this: any) {
377+
const action = <Fn extends _Method>(fn: Fn, name: string = ''): Fn => {
378+
if (ACTION_MARKER in fn) {
379+
// we ensure the name is set from the returned function
380+
;(fn as unknown as MarkedAction<Fn>)[ACTION_NAME] = name
381+
return fn
382+
}
383+
384+
const wrappedAction = function (this: any) {
361385
setActivePinia(pinia)
362386
const args = Array.from(arguments)
363387

@@ -373,15 +397,15 @@ function createSetupStore<
373397
// @ts-expect-error
374398
triggerSubscriptions(actionSubscriptions, {
375399
args,
376-
name,
400+
name: wrappedAction[ACTION_NAME],
377401
store,
378402
after,
379403
onError,
380404
})
381405

382406
let ret: unknown
383407
try {
384-
ret = action.apply(this && this.$id === $id ? this : store, args)
408+
ret = fn.apply(this && this.$id === $id ? this : store, args)
385409
// handle sync errors
386410
} catch (error) {
387411
triggerSubscriptions(onErrorCallbackList, error)
@@ -403,7 +427,14 @@ function createSetupStore<
403427
// trigger after callbacks
404428
triggerSubscriptions(afterCallbackList, ret)
405429
return ret
406-
}
430+
} as MarkedAction<Fn>
431+
432+
wrappedAction[ACTION_MARKER] = true
433+
wrappedAction[ACTION_NAME] = name // will be set later
434+
435+
// @ts-expect-error: we are intentionally limiting the returned type to just Fn
436+
// because all the added properties are internals that are exposed through `$onAction()` only
437+
return wrappedAction
407438
}
408439

409440
const _hmrPayload = /*#__PURE__*/ markRaw({
@@ -480,7 +511,7 @@ function createSetupStore<
480511

481512
// TODO: idea create skipSerialize that marks properties as non serializable and they are skipped
482513
const setupStore = runWithContext(() =>
483-
pinia._e.run(() => (scope = effectScope()).run(setup)!)
514+
pinia._e.run(() => (scope = effectScope()).run(() => setup({ action }))!)
484515
)!
485516

486517
// overwrite existing actions to support $onAction
@@ -519,8 +550,7 @@ function createSetupStore<
519550
}
520551
// action
521552
} else if (typeof prop === 'function') {
522-
// @ts-expect-error: we are overriding the function we avoid wrapping if
523-
const actionValue = __DEV__ && hot ? prop : wrapAction(key, prop)
553+
const actionValue = __DEV__ && hot ? prop : action(prop as _Method, key)
524554
// this a hot module replacement store because the hotUpdate method needs
525555
// to do it with the right context
526556
/* istanbul ignore if */
@@ -629,9 +659,9 @@ function createSetupStore<
629659
})
630660

631661
for (const actionName in newStore._hmrPayload.actions) {
632-
const action: _Method = newStore[actionName]
662+
const actionFn: _Method = newStore[actionName]
633663

634-
set(store, actionName, wrapAction(actionName, action))
664+
set(store, actionName, action(actionFn, actionName))
635665
}
636666

637667
// TODO: does this work in both setup and option store?
@@ -784,13 +814,9 @@ export type StoreState<SS> =
784814
? UnwrapRef<S>
785815
: _ExtractStateFromSetupStore<SS>
786816

787-
// type a1 = _ExtractStateFromSetupStore<{ a: Ref<number>; action: () => void }>
788-
// type a2 = _ExtractActionsFromSetupStore<{ a: Ref<number>; action: () => void }>
789-
// type a3 = _ExtractGettersFromSetupStore<{
790-
// a: Ref<number>
791-
// b: ComputedRef<string>
792-
// action: () => void
793-
// }>
817+
export interface SetupStoreHelpers {
818+
action: <Fn extends _Method>(fn: Fn) => Fn
819+
}
794820

795821
/**
796822
* Creates a `useStore` function that retrieves the store instance
@@ -831,7 +857,7 @@ export function defineStore<
831857
*/
832858
export function defineStore<Id extends string, SS>(
833859
id: Id,
834-
storeSetup: () => SS,
860+
storeSetup: (helpers: SetupStoreHelpers) => SS,
835861
options?: DefineSetupStoreOptions<
836862
Id,
837863
_ExtractStateFromSetupStore<SS>,

packages/playground/src/stores/nasa.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ref } from 'vue'
33
import { acceptHMRUpdate, defineStore } from 'pinia'
44
import { getNASAPOD } from '../api/nasa'
55

6-
export const useNasaStore = defineStore('nasa-pod-swrv', () => {
6+
export const useNasaStore = defineStore('nasa-pod-swrv', ({ action }) => {
77
// can't go past today
88
const today = new Date().toISOString().slice(0, 10)
99

@@ -30,21 +30,21 @@ export const useNasaStore = defineStore('nasa-pod-swrv', () => {
3030
}
3131
)
3232

33-
function incrementDay(date: string) {
33+
const incrementDay = action((date: string) => {
3434
const from = new Date(date).getTime()
3535

3636
currentDate.value = new Date(from + 1000 * 60 * 60 * 24)
3737
.toISOString()
3838
.slice(0, 10)
39-
}
39+
})
4040

41-
function decrementDay(date: string) {
41+
const decrementDay = action((date: string) => {
4242
const from = new Date(date).getTime()
4343

4444
currentDate.value = new Date(from - 1000 * 60 * 60 * 24)
4545
.toISOString()
4646
.slice(0, 10)
47-
}
47+
})
4848

4949
return { image, currentDate, incrementDay, decrementDay, error, isValidating }
5050
})

0 commit comments

Comments
 (0)