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/store.ts b/lib/classic-store.ts similarity index 50% rename from lib/store.ts rename to lib/classic-store.ts index 5a910ae..d4360fc 100644 --- a/lib/store.ts +++ b/lib/classic-store.ts @@ -3,10 +3,22 @@ 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: T + defaultState: DefaultState useStore: () => [T, Updater] useSelector: (selector: (state: T) => S) => S + subject$: BehaviorSubject + update: Updater + getValue: () => T } const createStore = (initialState: T) => { @@ -20,12 +32,35 @@ const createStore = (initialState: T) => { }; }; -export const buildStore = (defaultState: T): StoreBuilder => { - const getStoreInstance = createStore(defaultState); +const isDefaultState = ( + defaultState: DefaultState +): defaultState is T => { + if ( + typeof defaultState === 'object' && + defaultState !== null && + 'hydrator' in defaultState + ) { + return false; + } + return true; +}; - const useStore = () => { - const store = getStoreInstance(); +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, @@ -36,10 +71,11 @@ export const buildStore = (defaultState: T): StoreBuilder => { subscription.unsubscribe(); }; }; - const update: Updater = (updater) => { - store.next(updater(store.getValue())); - }; - const state: T = useSyncExternalStore(subscribe, () => store.getValue()); + const state: T = useSyncExternalStore( + subscribe, + () => store.getValue(), + () => initialState + ); return [state, update] satisfies [T, Updater]; }; @@ -51,9 +87,13 @@ export const buildStore = (defaultState: T): StoreBuilder => { return useMemo(() => selectedValue, [selectedValue]); }; + return { defaultState, useStore, - useSelector + 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/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;