diff --git a/src/babel-plugin-factories.json b/src/babel-plugin-factories.json index 8fc1e510..5588f10f 100644 --- a/src/babel-plugin-factories.json +++ b/src/babel-plugin-factories.json @@ -17,6 +17,7 @@ "patronum/once", "patronum/or", "patronum/pending", + "patronum/previous", "patronum/reset", "patronum/reshape", "patronum/snapshot", @@ -44,6 +45,7 @@ "once": "once", "or": "or", "pending": "pending", + "previous": "previous", "reset": "reset", "reshape": "reshape", "snapshot": "snapshot", @@ -54,4 +56,4 @@ "throttle": "throttle", "time": "time" } -} \ No newline at end of file +} diff --git a/src/index.md b/src/index.md index 632a8312..a6b5a900 100644 --- a/src/index.md +++ b/src/index.md @@ -42,6 +42,7 @@ All methods split into categories. - [snapshot](./snapshot/readme.md) — Create store value snapshot. - [splitMap](./split-map/readme.md) — Split event to different events and map data. - [spread](./spread/readme.md) — Send fields from object to same targets. +- [previous](./previous/readme.md) - Get previous value of store. ### Debug diff --git a/src/previous/index.ts b/src/previous/index.ts new file mode 100644 index 00000000..ff71db06 --- /dev/null +++ b/src/previous/index.ts @@ -0,0 +1,34 @@ +import { Node, Store, createStore, launch, step, is } from 'effector'; + +export function previous(store: Store): Store; +export function previous( + store: Store, + initialValue: Init, +): Store; +export function previous( + ...args: [store: Store, defaultValue?: Init] +) { + const [store] = args; + const initialValue = (args.length < 2 ? null : args[1]) as Init | null; + if (!is.store(store)) { + throw Error('previous first argument should be a store'); + } + const $prevValue = createStore(initialValue, { + serialize: 'ignore', + skipVoid: false, + }); + const storeNode: Node = (store as any).graphite; + storeNode.seq.push( + step.compute({ + fn(upd, _, stack) { + launch({ + target: $prevValue, + params: stack.a, + defer: true, + }); + return upd; + }, + }), + ); + return $prevValue; +} diff --git a/src/previous/previous.fork.test.ts b/src/previous/previous.fork.test.ts new file mode 100644 index 00000000..36bab800 --- /dev/null +++ b/src/previous/previous.fork.test.ts @@ -0,0 +1,87 @@ +import { + allSettled, + createEvent, + createStore, + fork, + restore, + sample, +} from 'effector'; + +import { previous } from './index'; + +it('has null when store is not changed', () => { + const scope = fork(); + const $initalStore = createStore(10); + const $prevValue = previous($initalStore); + + expect(scope.getState($prevValue)).toBe(null); +}); + +it('has initial value when defined', () => { + const scope = fork(); + const $initalStore = createStore(10); + const $prevValue = previous($initalStore, 0); + + expect(scope.getState($prevValue)).toBe(0); +}); + +it('has first value on update', async () => { + const scope = fork(); + const changeInitialStore = createEvent(); + const $initalStore = restore(changeInitialStore, 10); + + const $prevValue = previous($initalStore); + + await allSettled(changeInitialStore, { scope, params: 20 }); + + expect(scope.getState($prevValue)).toBe(10); +}); + +it('has previous value on multiple updates', async () => { + const scope = fork(); + const changeInitialStore = createEvent(); + const $initalStore = restore(changeInitialStore, 10); + + const $prevValue = previous($initalStore); + + await allSettled(changeInitialStore, { scope, params: 20 }); + await allSettled(changeInitialStore, { scope, params: 30 }); + await allSettled(changeInitialStore, { scope, params: 40 }); + + expect(scope.getState($prevValue)).toBe(30); +}); + +it('has first scope value after first update', async () => { + const inc = createEvent(); + const $initalStore = createStore(0); + const $prevValue = previous($initalStore, -1); + $initalStore.on(inc, (x) => x + 1); + const scope = fork({ values: [[$initalStore, 10]] }); + await allSettled(inc, { scope }); + expect(scope.getState($prevValue)).toBe(10); +}); + +test('undefined support', async () => { + const changeInitialStore = createEvent(); + const $initialStore = createStore('a', { skipVoid: false }); + const $prevValue = previous($initialStore); + + sample({ clock: changeInitialStore, target: $initialStore }); + + const scope = fork({ values: [[$initialStore, 'b']] }); + await allSettled(changeInitialStore, { scope, params: undefined }); + expect(scope.getState($prevValue)).toBe('b'); + await allSettled(changeInitialStore, { scope, params: 'c' }); + expect(scope.getState($prevValue)).toBe(undefined); +}); + +test('undefined as defaultValue support', () => { + const changeInitialStore = createEvent(); + const $initialStore = createStore('a', { skipVoid: false }); + const $prevValue = previous($initialStore, undefined); + + sample({ clock: changeInitialStore, target: $initialStore }); + + const scope = fork({ values: [[$initialStore, 'b']] }); + expect(scope.getState($prevValue)).toBe(undefined); +}); diff --git a/src/previous/previous.test.ts b/src/previous/previous.test.ts new file mode 100644 index 00000000..a6a67a7e --- /dev/null +++ b/src/previous/previous.test.ts @@ -0,0 +1,73 @@ +import { createEvent, createStore, restore, sample } from 'effector'; + +import { previous } from './index'; + +it('has null when store is not changed', () => { + const $initalStore = createStore(10); + const $prevValue = previous($initalStore); + + expect($prevValue.getState()).toBe(null); +}); + +it('has initial value when defined', () => { + const $initalStore = createStore(10); + const $prevValue = previous($initalStore, 0); + + expect($prevValue.getState()).toBe(0); +}); + +it('has first value on update', () => { + const changeInitialStore = createEvent(); + const $initalStore = restore(changeInitialStore, 10); + + const $prevValue = previous($initalStore); + + changeInitialStore(20); + + expect($prevValue.getState()).toBe(10); +}); + +it('has previous value on multiple updates', () => { + const changeInitialStore = createEvent(); + const $initalStore = restore(changeInitialStore, 10); + + const $prevValue = previous($initalStore); + + changeInitialStore(20); + changeInitialStore(30); + changeInitialStore(40); + + expect($prevValue.getState()).toBe(30); +}); + +test('undefined support', () => { + const changeInitialStore = createEvent(); + const $initialStore = createStore('a', { skipVoid: false }); + const $prevValue = previous($initialStore); + + sample({ clock: changeInitialStore, target: $initialStore }); + + changeInitialStore(); + expect($prevValue.getState()).toBe('a'); + changeInitialStore('b'); + expect($prevValue.getState()).toBe(undefined); +}); + +test('undefined as defaultValue support', () => { + const changeInitialStore = createEvent(); + const $initialStore = createStore('a', { skipVoid: false }); + const $prevValue = previous($initialStore, undefined); + + sample({ clock: changeInitialStore, target: $initialStore }); + + expect($prevValue.getState()).toBe(undefined); +}); + +test('store validation', () => { + expect(() => { + // @ts-expect-error + previous(null); + }).toThrowErrorMatchingInlineSnapshot( + `"previous first argument should be a store"`, + ); +}); diff --git a/src/previous/readme.md b/src/previous/readme.md new file mode 100644 index 00000000..c878d403 --- /dev/null +++ b/src/previous/readme.md @@ -0,0 +1,65 @@ +# previous + +:::note since +patronum 2.1.0 +::: + +```ts +import { previous } from 'patronum'; +// or +import { previous } from 'patronum/previous-value'; +``` + +### Motivation + +The method allows to get previous value of given store. Usually need for analytics + +### Formulae + +```ts +$target = previous($source); +$target = previous($source, 'initial value'); +``` + +### Arguments + +1. `$source` ([_`Store`_]) - source store +2. `defaultValue` (_optional_) - default value for `$target` store, if not passed, `null` will be used + +### Returns + +- `$target` ([_`Store`_]) - new store that contain previous value of `$source` after first update and null or default value (if passed) before that + +### Example + +Push analytics with route transition: + +```ts +import { createStore, createEvent, createEffect, sample } from 'effector'; +import { previous } from 'patronum'; + +const openNewRoute = createEvent(); +const $currentRoute = createStore('main_page'); +const $previousRoute = previous($currentRoute); + +const sendRouteTransitionFx = createEffect(async ({ prevRoute, nextRoute }) => { + console.log(prevRoute, '->', newRoute) + await fetch(...) +}); + +sample({clock: openNewRoute, target: $currentRoute}); + +sample({ + clock: openNewRoute, + source: { + prevRoute: $previousRoute, + nextRoute: $currentRoute, + }, + target: sendRouteTransitionFx, +}); + +openNewRoute('messages'); +// main_page -> messages +openNewRoute('chats'); +// messages -> chats +``` diff --git a/test-typings/previous.ts b/test-typings/previous.ts new file mode 100644 index 00000000..003d062d --- /dev/null +++ b/test-typings/previous.ts @@ -0,0 +1,34 @@ +import { expectType } from 'tsd'; +import { Store, createStore, createEvent } from 'effector'; +import { previous } from '../dist/previous'; + +{ + const $foo = createStore('a'); + const $fooPrev = previous($foo); + + expectType>($fooPrev); +} +{ + const $foo = createStore('a'); + const $fooPrev = previous($foo, 'b'); + + expectType>($fooPrev); +} +{ + const $foo = createStore('a'); + const $fooPrev = previous($foo, 0); + + expectType>($fooPrev); +} +{ + const $foo = createStore('a'); + const $fooPrev = previous($foo, undefined); + + expectType>($fooPrev); +} +{ + const foo = createEvent(); + + // @ts-expect-error + previous(foo, 0); +}