From 3c201e8df55f6c4bac992d625b04b654598eeaf9 Mon Sep 17 00:00:00 2001 From: Julius Koronci Date: Sun, 3 Mar 2024 19:46:29 +0100 Subject: [PATCH 1/4] clean up, rename some methods and expose more utilities --- e2e/counter.spec.ts | 14 ++++ lib/__tests__/index.test.ts | 2 +- lib/classic-store.ts | 99 ++++++++++++++++++++++++++ lib/index.ts | 2 +- lib/store.ts | 59 --------------- stories/counter/DefaultPromise.tsx | 16 +++++ stories/counter/Root.tsx | 2 + stories/counter/counterStore.ts | 4 +- stories/counter/counterStore2.ts | 6 +- stories/counter/counterStorePromise.ts | 15 ++++ 10 files changed, 154 insertions(+), 65 deletions(-) create mode 100644 lib/classic-store.ts delete mode 100644 lib/store.ts create mode 100644 stories/counter/DefaultPromise.tsx create mode 100644 stories/counter/counterStorePromise.ts diff --git a/e2e/counter.spec.ts b/e2e/counter.spec.ts index b4eac36..ee8ae37 100644 --- a/e2e/counter.spec.ts +++ b/e2e/counter.spec.ts @@ -21,10 +21,14 @@ test('test counter with classic store', async ({ page }) => { const counter3Text = getStorybookLocator(page).locator( 'text=We are counting third: 0', ); + const counterPromiseText = getStorybookLocator(page).locator( + 'text=We are counting hydrating default: 88', + ); await expect(counter1Text).toBeVisible(); await expect(counter2Text).toBeVisible(); await expect(counter3Text).toBeVisible(); + await expect(counterPromiseText).toBeVisible(); const button3 = getStorybookLocator(page).locator( 'button:has-text("Let\'s go, third counter, reusing second store")', @@ -68,4 +72,14 @@ test('test counter with classic store', async ({ page }) => { await expect( getStorybookLocator(page).locator('text=We are counting second: 2'), ).toBeVisible(); + + const buttonPromise = getStorybookLocator(page).locator( + 'button:has-text("Let\'s go, promise counter")', + ); + await buttonPromise.click(); + await expect( + getStorybookLocator(page).locator( + 'text=We are counting hydrating default: 89', + ), + ).toBeVisible(); }); diff --git a/lib/__tests__/index.test.ts b/lib/__tests__/index.test.ts index b62d183..a273f1d 100644 --- a/lib/__tests__/index.test.ts +++ b/lib/__tests__/index.test.ts @@ -3,6 +3,6 @@ import * as publicApi from '../index'; describe('Public api', () => { it('Should have a test export', () => { - expect(publicApi.buildStore).toBeDefined(); + expect(publicApi.buildClassicStore).toBeDefined(); }); }); diff --git a/lib/classic-store.ts b/lib/classic-store.ts new file mode 100644 index 0000000..6b85435 --- /dev/null +++ b/lib/classic-store.ts @@ -0,0 +1,99 @@ +import { BehaviorSubject } from 'rxjs'; +import { useMemo, useSyncExternalStore } from 'react'; + +type UpdateFunction = (state: T) => T; +type Updater = (cb: UpdateFunction) => void; + +type DefaultState = + | T + | { + hydrator: () => Promise; + beforeLoadState: T; + persist?: (state: T) => Promise; + }; + +interface StoreBuilder { + defaultState: DefaultState; + useStore: () => [T, Updater]; + useSelector: (selector: (state: T) => S) => S; + $subject: BehaviorSubject; + update: Updater; + getValue: () => T; +} + +const createStore = (initialState: T) => { + let storeInstance: BehaviorSubject | null = null; + + return () => { + if (storeInstance === null) { + storeInstance = new BehaviorSubject(initialState); + } + return storeInstance; + }; +}; + +const isDefaultState = ( + defaultState: DefaultState, +): defaultState is T => { + if ( + typeof defaultState === 'object' && + defaultState !== null && + 'hydrator' in defaultState + ) { + return false; + } + return true; +}; + +export const buildClassicStore = async ( + defaultState: DefaultState, +): Promise> => { + const initialState = isDefaultState(defaultState) + ? defaultState + : await defaultState.hydrator(); + + const getStoreInstance = createStore(initialState); + + const store = getStoreInstance(); + + const update: Updater = (updater) => { + store.next(updater(store.getValue())); + }; + + const useStore = () => { + const subscribe = (onStoreChange: () => void) => { + const subscription = store.subscribe({ + next: onStoreChange, + error: console.error, + }); + + return () => { + subscription.unsubscribe(); + }; + }; + const state: T = useSyncExternalStore( + subscribe, + () => store.getValue(), + () => initialState, + ); + + return [state, update] satisfies [T, Updater]; + }; + + const useSelector = (selector: (state: T) => S) => { + const [value] = useStore(); + + const selectedValue = selector(value); + + return useMemo(() => selectedValue, [selectedValue]); + }; + + return { + defaultState, + useStore, + useSelector, + $subject: store, + update, + getValue: store.getValue, + }; +}; diff --git a/lib/index.ts b/lib/index.ts index d406816..b5903bf 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1 +1 @@ -export * from './store'; +export * from './classic-store'; diff --git a/lib/store.ts b/lib/store.ts deleted file mode 100644 index 5a910ae..0000000 --- a/lib/store.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { BehaviorSubject } from 'rxjs'; -import { useMemo, useSyncExternalStore } from 'react'; - -type UpdateFunction = (state: T) => T; -type Updater = (cb: UpdateFunction) => void; -interface StoreBuilder { - defaultState: T - useStore: () => [T, Updater] - useSelector: (selector: (state: T) => S) => S -} - -const createStore = (initialState: T) => { - let storeInstance: BehaviorSubject | null = null; - - return () => { - if (storeInstance === null) { - storeInstance = new BehaviorSubject(initialState); - } - return storeInstance; - }; -}; - -export const buildStore = (defaultState: T): StoreBuilder => { - const getStoreInstance = createStore(defaultState); - - const useStore = () => { - const store = getStoreInstance(); - - const subscribe = (onStoreChange: () => void) => { - const subscription = store.subscribe({ - next: onStoreChange, - error: console.error - }); - - return () => { - subscription.unsubscribe(); - }; - }; - const update: Updater = (updater) => { - store.next(updater(store.getValue())); - }; - const state: T = useSyncExternalStore(subscribe, () => store.getValue()); - - return [state, update] satisfies [T, Updater]; - }; - - const useSelector = (selector: (state: T) => S) => { - const [value] = useStore(); - - const selectedValue = selector(value); - - return useMemo(() => selectedValue, [selectedValue]); - }; - return { - defaultState, - useStore, - useSelector - }; -}; diff --git a/stories/counter/DefaultPromise.tsx b/stories/counter/DefaultPromise.tsx new file mode 100644 index 0000000..af0342f --- /dev/null +++ b/stories/counter/DefaultPromise.tsx @@ -0,0 +1,16 @@ +import { Button, Flex, Text } from '@radix-ui/themes'; +import React from 'react'; +import useStore, { useCounterPromiseSelector } from './counterStorePromise'; + +export const DefaultPromise = () => { + const [, updateCount] = useStore(); + const count = useCounterPromiseSelector((state) => state.count); + const increment = () => + updateCount((prevState) => ({ ...prevState, count: prevState.count + 1 })); + return ( + + We are counting hydrating default: {count} + + + ); +}; diff --git a/stories/counter/Root.tsx b/stories/counter/Root.tsx index 6c31231..a8095b9 100644 --- a/stories/counter/Root.tsx +++ b/stories/counter/Root.tsx @@ -3,6 +3,7 @@ import { Counter } from './Counter'; import React from 'react'; import { Counter2 } from './Counter2'; import { Counter3 } from './Counter3'; +import { DefaultPromise } from './DefaultPromise'; export const Root = () => { return ( @@ -10,6 +11,7 @@ export const Root = () => { + ); }; diff --git a/stories/counter/counterStore.ts b/stories/counter/counterStore.ts index f189294..2d85681 100644 --- a/stories/counter/counterStore.ts +++ b/stories/counter/counterStore.ts @@ -1,10 +1,10 @@ -import { buildStore } from '../../lib'; +import { buildClassicStore } from '../../lib'; export interface CounterState { count: number; } -const counterStoreBuilder = buildStore({ count: 0 }); +const counterStoreBuilder = await buildClassicStore({ count: 0 }); const { useStore, useSelector: useCounterSelector } = counterStoreBuilder; export { useCounterSelector }; diff --git a/stories/counter/counterStore2.ts b/stories/counter/counterStore2.ts index 9ee6987..f05bc5a 100644 --- a/stories/counter/counterStore2.ts +++ b/stories/counter/counterStore2.ts @@ -1,10 +1,12 @@ -import { buildStore } from '../../lib'; +import { buildClassicStore } from '../../lib'; export interface Counter2State { count: number; } -const counter2StoreBuilder = buildStore({ count: 0 }); +const counter2StoreBuilder = await buildClassicStore({ + count: 0, +}); const { useStore, useSelector: useCounter2Selector } = counter2StoreBuilder; export { useCounter2Selector }; diff --git a/stories/counter/counterStorePromise.ts b/stories/counter/counterStorePromise.ts new file mode 100644 index 0000000..7aa7d00 --- /dev/null +++ b/stories/counter/counterStorePromise.ts @@ -0,0 +1,15 @@ +import { buildClassicStore } from '../../lib'; + +export interface CounterState { + count: number; +} + +const counterStore3Builder = await buildClassicStore({ + hydrator: () => Promise.resolve({ count: 88 }), + beforeLoadState: { count: 0 }, +}); +const { useStore, useSelector: useCounterPromiseSelector } = + counterStore3Builder; + +export { useCounterPromiseSelector }; +export default useStore; From b5c05f56b247d7c6610dcece757d3e3e54d9ec98 Mon Sep 17 00:00:00 2001 From: Julius Koronci Date: Sun, 3 Mar 2024 19:48:05 +0100 Subject: [PATCH 2/4] linter --- lib/classic-store.ts | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/classic-store.ts b/lib/classic-store.ts index 6b85435..d0259a3 100644 --- a/lib/classic-store.ts +++ b/lib/classic-store.ts @@ -7,18 +7,18 @@ type Updater = (cb: UpdateFunction) => void; type DefaultState = | T | { - hydrator: () => Promise; - beforeLoadState: T; - persist?: (state: T) => Promise; - }; + hydrator: () => Promise + beforeLoadState: T + persist?: (state: T) => Promise + }; interface StoreBuilder { - defaultState: DefaultState; - useStore: () => [T, Updater]; - useSelector: (selector: (state: T) => S) => S; - $subject: BehaviorSubject; - update: Updater; - getValue: () => T; + defaultState: DefaultState + useStore: () => [T, Updater] + useSelector: (selector: (state: T) => S) => S + $subject: BehaviorSubject + update: Updater + getValue: () => T } const createStore = (initialState: T) => { @@ -33,7 +33,7 @@ const createStore = (initialState: T) => { }; const isDefaultState = ( - defaultState: DefaultState, + defaultState: DefaultState ): defaultState is T => { if ( typeof defaultState === 'object' && @@ -46,7 +46,7 @@ const isDefaultState = ( }; export const buildClassicStore = async ( - defaultState: DefaultState, + defaultState: DefaultState ): Promise> => { const initialState = isDefaultState(defaultState) ? defaultState @@ -64,7 +64,7 @@ export const buildClassicStore = async ( const subscribe = (onStoreChange: () => void) => { const subscription = store.subscribe({ next: onStoreChange, - error: console.error, + error: console.error }); return () => { @@ -74,7 +74,7 @@ export const buildClassicStore = async ( const state: T = useSyncExternalStore( subscribe, () => store.getValue(), - () => initialState, + () => initialState ); return [state, update] satisfies [T, Updater]; @@ -94,6 +94,6 @@ export const buildClassicStore = async ( useSelector, $subject: store, update, - getValue: store.getValue, + getValue: () => store.getValue() }; }; From 7477a0354729f5ddd4f95c0366e6e0696a8d9685 Mon Sep 17 00:00:00 2001 From: Julius Koronci Date: Sun, 3 Mar 2024 19:51:42 +0100 Subject: [PATCH 3/4] rx naming --- lib/classic-store.ts | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/classic-store.ts b/lib/classic-store.ts index d0259a3..d779aab 100644 --- a/lib/classic-store.ts +++ b/lib/classic-store.ts @@ -7,18 +7,18 @@ type Updater = (cb: UpdateFunction) => void; type DefaultState = | T | { - hydrator: () => Promise - beforeLoadState: T - persist?: (state: T) => Promise - }; + hydrator: () => Promise; + beforeLoadState: T; + persist?: (state: T) => Promise; + }; interface StoreBuilder { - defaultState: DefaultState - useStore: () => [T, Updater] - useSelector: (selector: (state: T) => S) => S - $subject: BehaviorSubject - update: Updater - getValue: () => T + defaultState: DefaultState; + useStore: () => [T, Updater]; + useSelector: (selector: (state: T) => S) => S; + subject$: BehaviorSubject; + update: Updater; + getValue: () => T; } const createStore = (initialState: T) => { @@ -33,7 +33,7 @@ const createStore = (initialState: T) => { }; const isDefaultState = ( - defaultState: DefaultState + defaultState: DefaultState, ): defaultState is T => { if ( typeof defaultState === 'object' && @@ -46,7 +46,7 @@ const isDefaultState = ( }; export const buildClassicStore = async ( - defaultState: DefaultState + defaultState: DefaultState, ): Promise> => { const initialState = isDefaultState(defaultState) ? defaultState @@ -64,7 +64,7 @@ export const buildClassicStore = async ( const subscribe = (onStoreChange: () => void) => { const subscription = store.subscribe({ next: onStoreChange, - error: console.error + error: console.error, }); return () => { @@ -74,7 +74,7 @@ export const buildClassicStore = async ( const state: T = useSyncExternalStore( subscribe, () => store.getValue(), - () => initialState + () => initialState, ); return [state, update] satisfies [T, Updater]; @@ -92,8 +92,8 @@ export const buildClassicStore = async ( defaultState, useStore, useSelector, - $subject: store, + subject$: store, update, - getValue: () => store.getValue() + getValue: () => store.getValue(), }; }; From cbf94d2cf4bcf6c385443dd82ebbfc2589392ad6 Mon Sep 17 00:00:00 2001 From: Julius Koronci Date: Sun, 3 Mar 2024 20:05:33 +0100 Subject: [PATCH 4/4] linter --- lib/classic-store.ts | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/classic-store.ts b/lib/classic-store.ts index d779aab..d4360fc 100644 --- a/lib/classic-store.ts +++ b/lib/classic-store.ts @@ -7,18 +7,18 @@ type Updater = (cb: UpdateFunction) => void; type DefaultState = | T | { - hydrator: () => Promise; - beforeLoadState: T; - persist?: (state: T) => Promise; - }; + hydrator: () => Promise + beforeLoadState: T + persist?: (state: T) => Promise + }; interface StoreBuilder { - defaultState: DefaultState; - useStore: () => [T, Updater]; - useSelector: (selector: (state: T) => S) => S; - subject$: BehaviorSubject; - update: Updater; - getValue: () => T; + defaultState: DefaultState + useStore: () => [T, Updater] + useSelector: (selector: (state: T) => S) => S + subject$: BehaviorSubject + update: Updater + getValue: () => T } const createStore = (initialState: T) => { @@ -33,7 +33,7 @@ const createStore = (initialState: T) => { }; const isDefaultState = ( - defaultState: DefaultState, + defaultState: DefaultState ): defaultState is T => { if ( typeof defaultState === 'object' && @@ -46,7 +46,7 @@ const isDefaultState = ( }; export const buildClassicStore = async ( - defaultState: DefaultState, + defaultState: DefaultState ): Promise> => { const initialState = isDefaultState(defaultState) ? defaultState @@ -64,7 +64,7 @@ export const buildClassicStore = async ( const subscribe = (onStoreChange: () => void) => { const subscription = store.subscribe({ next: onStoreChange, - error: console.error, + error: console.error }); return () => { @@ -74,7 +74,7 @@ export const buildClassicStore = async ( const state: T = useSyncExternalStore( subscribe, () => store.getValue(), - () => initialState, + () => initialState ); return [state, update] satisfies [T, Updater]; @@ -94,6 +94,6 @@ export const buildClassicStore = async ( useSelector, subject$: store, update, - getValue: () => store.getValue(), + getValue: () => store.getValue() }; };