dantian 0.0.2-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +1 -0
- package/.eslintrc.cjs +28 -0
- package/.github/workflows/ci.yml +74 -0
- package/.prettierrc +6 -0
- package/.storybook/main.ts +18 -0
- package/.storybook/preview.tsx +24 -0
- package/.storybook/theme.tsx +7 -0
- package/.versionrc.json +9 -0
- package/.yarn/install-state.gz +0 -0
- package/.yarnrc.yml +1 -0
- package/CHANGELOG.md +5 -0
- package/LICENSE +21 -0
- package/README.md +55 -0
- package/commitlint.config.js +1 -0
- package/e2e/counter.spec.ts +85 -0
- package/e2e/getLocator.ts +4 -0
- package/lib/__tests__/index.test.ts +8 -0
- package/lib/classic-store.ts +86 -0
- package/lib/dao.ts +3 -0
- package/lib/event-store.ts +169 -0
- package/lib/index.ts +3 -0
- package/lib/types.ts +28 -0
- package/package.json +73 -0
- package/playwright.config.ts +77 -0
- package/stories/counter/Counter.tsx +22 -0
- package/stories/counter/Counter2.tsx +23 -0
- package/stories/counter/Counter3.tsx +18 -0
- package/stories/counter/DefaultPromise.tsx +16 -0
- package/stories/counter/Root.tsx +17 -0
- package/stories/counter/counterStore.ts +11 -0
- package/stories/counter/counterStore2.ts +13 -0
- package/stories/counter/counterStorePromise.ts +15 -0
- package/stories/counter.stories.ts +23 -0
- package/stories/event-store/Root.tsx +19 -0
- package/stories/event-store/state.ts +9 -0
- package/stories/event-store/store.ts +28 -0
- package/stories/event-store.stories.ts +23 -0
- package/tsconfig.json +24 -0
- package/vite.config.js +21 -0
- package/vitest.config.ts +9 -0
package/.eslintignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.eslintrc.cjs
|
package/.eslintrc.cjs
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
env: {
|
|
3
|
+
browser: true,
|
|
4
|
+
es2021: true,
|
|
5
|
+
},
|
|
6
|
+
extends: [
|
|
7
|
+
'standard-with-typescript',
|
|
8
|
+
'plugin:react/recommended',
|
|
9
|
+
'plugin:storybook/recommended',
|
|
10
|
+
'prettier',
|
|
11
|
+
],
|
|
12
|
+
plugins: ['prettier'],
|
|
13
|
+
overrides: [],
|
|
14
|
+
parserOptions: {
|
|
15
|
+
ecmaVersion: 'latest',
|
|
16
|
+
sourceType: 'module',
|
|
17
|
+
},
|
|
18
|
+
plugins: ['react'],
|
|
19
|
+
rules: {
|
|
20
|
+
'@typescript-eslint/semi': 'off',
|
|
21
|
+
'@typescript-eslint/explicit-function-return-type': 'off',
|
|
22
|
+
},
|
|
23
|
+
settings: {
|
|
24
|
+
react: {
|
|
25
|
+
version: 'detect',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, master]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main, master]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
ci-checks-and-publish:
|
|
11
|
+
timeout-minutes: 60
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v3
|
|
16
|
+
- name: Configure Git
|
|
17
|
+
run: |
|
|
18
|
+
git config --global user.name "JuliusKoronciCH"
|
|
19
|
+
git config --global user.email "julius.koronci@gmail.com"
|
|
20
|
+
- uses: actions/checkout@v3
|
|
21
|
+
- name: Set up Node.js
|
|
22
|
+
uses: actions/setup-node@v3
|
|
23
|
+
with:
|
|
24
|
+
node-version: 20
|
|
25
|
+
registry-url: https://registry.npmjs.org/
|
|
26
|
+
|
|
27
|
+
- name: Install Dependencies
|
|
28
|
+
run: corepack enable
|
|
29
|
+
if: ${{ !steps.cache.outputs.cache-hit }}
|
|
30
|
+
|
|
31
|
+
- name: Cache Dependencies (Yarn Berry)
|
|
32
|
+
uses: actions/cache@v3
|
|
33
|
+
with:
|
|
34
|
+
path: ~/.yarn/cache # Cache location for Yarn Berry
|
|
35
|
+
key: ${{ runner.os }}-yarn-berry-${{ hashFiles('**/yarn.lock') }}
|
|
36
|
+
restore-keys: |
|
|
37
|
+
${{ runner.os }}-yarn-berry-
|
|
38
|
+
|
|
39
|
+
- name: Install Dependencies
|
|
40
|
+
run: yarn install --immutable
|
|
41
|
+
|
|
42
|
+
- name: Install Playwright Browsers
|
|
43
|
+
run: yarn playwright install --with-deps
|
|
44
|
+
|
|
45
|
+
- name: Run Linters
|
|
46
|
+
run: yarn lint
|
|
47
|
+
|
|
48
|
+
- name: Run Prettier
|
|
49
|
+
run: yarn format
|
|
50
|
+
|
|
51
|
+
- name: Run Tests
|
|
52
|
+
run: yarn test
|
|
53
|
+
|
|
54
|
+
- name: Start Storybook
|
|
55
|
+
run: yarn storybook &
|
|
56
|
+
|
|
57
|
+
- name: Run Playwright tests
|
|
58
|
+
run: yarn playwright test
|
|
59
|
+
|
|
60
|
+
- uses: actions/upload-artifact@v3
|
|
61
|
+
if: always()
|
|
62
|
+
with:
|
|
63
|
+
name: playwright-report
|
|
64
|
+
path: playwright-report/
|
|
65
|
+
retention-days: 30
|
|
66
|
+
|
|
67
|
+
- name: Publish to npm (on merge to main)
|
|
68
|
+
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
|
69
|
+
run: |
|
|
70
|
+
yarn standard-version
|
|
71
|
+
npm publish --tag beta
|
|
72
|
+
env:
|
|
73
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
74
|
+
GITHUB_TOKEN: ${{ secrets.CHANGELOG_TOKEN }}
|
package/.prettierrc
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { StorybookConfig } from '@storybook/react-vite';
|
|
2
|
+
|
|
3
|
+
const config: StorybookConfig = {
|
|
4
|
+
stories: ['../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
|
5
|
+
addons: [
|
|
6
|
+
'@storybook/addon-links',
|
|
7
|
+
'@storybook/addon-essentials',
|
|
8
|
+
'@storybook/addon-interactions',
|
|
9
|
+
],
|
|
10
|
+
framework: {
|
|
11
|
+
name: '@storybook/react-vite',
|
|
12
|
+
options: {},
|
|
13
|
+
},
|
|
14
|
+
docs: {
|
|
15
|
+
autodocs: 'tag',
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
export default config;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Preview } from '@storybook/react';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { ThemeProvider } from './theme';
|
|
4
|
+
|
|
5
|
+
const preview: Preview = {
|
|
6
|
+
decorators: [
|
|
7
|
+
(Story) => (
|
|
8
|
+
<ThemeProvider>
|
|
9
|
+
<Story />
|
|
10
|
+
</ThemeProvider>
|
|
11
|
+
),
|
|
12
|
+
],
|
|
13
|
+
parameters: {
|
|
14
|
+
actions: { argTypesRegex: '^on[A-Z].*' },
|
|
15
|
+
controls: {
|
|
16
|
+
matchers: {
|
|
17
|
+
color: /(background|color)$/i,
|
|
18
|
+
date: /Date$/i,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default preview;
|
package/.versionrc.json
ADDED
|
Binary file
|
package/.yarnrc.yml
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nodeLinker: node-modules
|
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Igsem
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
**Dantian - React State Management Reimagined**
|
|
2
|
+
|
|
3
|
+
npm version: [replace with package version badge]
|
|
4
|
+
|
|
5
|
+
Dantian is an event-based state management library for React applications that delivers pinpoint performance and effortless integration with forms. Say goodbye to unnecessary rerenders and complex state comparisons – Dantian ensures that components only update when they subscribe to specific state changes.
|
|
6
|
+
|
|
7
|
+
**The Problem**
|
|
8
|
+
|
|
9
|
+
Traditional state management solutions in React often lead to performance issues, especially in applications with frequent updates or large forms. Unnecessary rerenders can slow down the user experience.
|
|
10
|
+
|
|
11
|
+
**Our Solution**
|
|
12
|
+
|
|
13
|
+
Dantian's event-driven approach focuses on precision. State updates are triggered by events, and only components subscribed to those events rerender. This provides granular control and optimizes performance where it matters most.
|
|
14
|
+
|
|
15
|
+
**Features**
|
|
16
|
+
|
|
17
|
+
- **useState-like hooks:** Familiar interface for ease of use.
|
|
18
|
+
- **Custom events:** Power and flexibility for complex scenarios.
|
|
19
|
+
- **Asynchronous state updates:** Handle async patterns gracefully.
|
|
20
|
+
- **Optimized form integration:** Tackle large forms without performance compromises.
|
|
21
|
+
|
|
22
|
+
**Installation**
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install dantian
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Basic usage**
|
|
29
|
+
|
|
30
|
+
```TypeScript
|
|
31
|
+
import { useEventState } from 'dantian';
|
|
32
|
+
|
|
33
|
+
function MyComponent() {
|
|
34
|
+
const [count, setCount] = useEventState('counter', 0);
|
|
35
|
+
|
|
36
|
+
const increment = () => setCount(count + 1);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div>
|
|
40
|
+
<p>Count: {count}</p>
|
|
41
|
+
<button onClick={increment}>Increment</button>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Getting Started**
|
|
48
|
+
|
|
49
|
+
**Why Dantian?**
|
|
50
|
+
|
|
51
|
+
Performance by design: Avoid unnecessary rerenders, especially in complex forms.
|
|
52
|
+
Developer-friendly: Intuitive API and seamless form integration.
|
|
53
|
+
**License**
|
|
54
|
+
|
|
55
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default { extends: ['@commitlint/config-conventional'] };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test';
|
|
2
|
+
import { getStorybookLocator } from './getLocator';
|
|
3
|
+
|
|
4
|
+
test.beforeEach(async ({ page }) => {
|
|
5
|
+
await page.goto(
|
|
6
|
+
'http://localhost:6006/?path=/story/dantian-counter--primary',
|
|
7
|
+
);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test('page title', async ({ page }) => {
|
|
11
|
+
await expect(page).toHaveTitle(/Dantian \/ Counter.*/);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('test counter with classic store', async ({ page }) => {
|
|
15
|
+
const counter1Text = getStorybookLocator(page).locator(
|
|
16
|
+
'text=We are counting first: 0',
|
|
17
|
+
);
|
|
18
|
+
const counter2Text = getStorybookLocator(page).locator(
|
|
19
|
+
'text=We are counting second: 0',
|
|
20
|
+
);
|
|
21
|
+
const counter3Text = getStorybookLocator(page).locator(
|
|
22
|
+
'text=We are counting third: 0',
|
|
23
|
+
);
|
|
24
|
+
const counterPromiseText = getStorybookLocator(page).locator(
|
|
25
|
+
'text=We are counting hydrating default: 88',
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
await expect(counter1Text).toBeVisible();
|
|
29
|
+
await expect(counter2Text).toBeVisible();
|
|
30
|
+
await expect(counter3Text).toBeVisible();
|
|
31
|
+
await expect(counterPromiseText).toBeVisible();
|
|
32
|
+
|
|
33
|
+
const button3 = getStorybookLocator(page).locator(
|
|
34
|
+
'button:has-text("Let\'s go, third counter, reusing second store")',
|
|
35
|
+
);
|
|
36
|
+
await button3.click();
|
|
37
|
+
await expect(
|
|
38
|
+
getStorybookLocator(page).locator('text=We are counting first: 0'),
|
|
39
|
+
).toBeVisible();
|
|
40
|
+
await expect(
|
|
41
|
+
getStorybookLocator(page).locator('text=We are counting third: 1'),
|
|
42
|
+
).toBeVisible();
|
|
43
|
+
await expect(
|
|
44
|
+
getStorybookLocator(page).locator('text=We are counting second: 1'),
|
|
45
|
+
).toBeVisible();
|
|
46
|
+
|
|
47
|
+
const button2 = getStorybookLocator(page).locator(
|
|
48
|
+
'button:has-text("Let\'s go, second counter")',
|
|
49
|
+
);
|
|
50
|
+
await button2.click();
|
|
51
|
+
await expect(
|
|
52
|
+
getStorybookLocator(page).locator('text=We are counting first: 0'),
|
|
53
|
+
).toBeVisible();
|
|
54
|
+
await expect(
|
|
55
|
+
getStorybookLocator(page).locator('text=We are counting third: 2'),
|
|
56
|
+
).toBeVisible();
|
|
57
|
+
await expect(
|
|
58
|
+
getStorybookLocator(page).locator('text=We are counting second: 2'),
|
|
59
|
+
).toBeVisible();
|
|
60
|
+
|
|
61
|
+
const button1 = getStorybookLocator(page).locator(
|
|
62
|
+
'button:has-text("Let\'s go, first counter")',
|
|
63
|
+
);
|
|
64
|
+
await button1.click();
|
|
65
|
+
|
|
66
|
+
await expect(
|
|
67
|
+
getStorybookLocator(page).locator('text=We are counting first: 1'),
|
|
68
|
+
).toBeVisible();
|
|
69
|
+
await expect(
|
|
70
|
+
getStorybookLocator(page).locator('text=We are counting third: 2'),
|
|
71
|
+
).toBeVisible();
|
|
72
|
+
await expect(
|
|
73
|
+
getStorybookLocator(page).locator('text=We are counting second: 2'),
|
|
74
|
+
).toBeVisible();
|
|
75
|
+
|
|
76
|
+
const buttonPromise = getStorybookLocator(page).locator(
|
|
77
|
+
'button:has-text("Let\'s go, promise counter")',
|
|
78
|
+
);
|
|
79
|
+
await buttonPromise.click();
|
|
80
|
+
await expect(
|
|
81
|
+
getStorybookLocator(page).locator(
|
|
82
|
+
'text=We are counting hydrating default: 89',
|
|
83
|
+
),
|
|
84
|
+
).toBeVisible();
|
|
85
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { BehaviorSubject } from 'rxjs';
|
|
2
|
+
import { useMemo, useSyncExternalStore } from 'react';
|
|
3
|
+
|
|
4
|
+
type UpdateFunction<T> = (state: T) => T;
|
|
5
|
+
type Updater<T> = (cb: UpdateFunction<T>) => void;
|
|
6
|
+
|
|
7
|
+
type DefaultState<T> =
|
|
8
|
+
| T
|
|
9
|
+
| {
|
|
10
|
+
hydrator: () => Promise<T>;
|
|
11
|
+
beforeLoadState: T;
|
|
12
|
+
persist?: (state: T) => Promise<void>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
interface StoreBuilder<T> {
|
|
16
|
+
defaultState: DefaultState<T>;
|
|
17
|
+
useStore: () => [T, Updater<T>];
|
|
18
|
+
useSelector: <S>(selector: (state: T) => S) => S;
|
|
19
|
+
subject$: BehaviorSubject<T>;
|
|
20
|
+
update: Updater<T>;
|
|
21
|
+
getValue: () => T;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const isDefaultState = <T>(
|
|
25
|
+
defaultState: DefaultState<T>,
|
|
26
|
+
): defaultState is T => {
|
|
27
|
+
if (
|
|
28
|
+
typeof defaultState === 'object' &&
|
|
29
|
+
defaultState !== null &&
|
|
30
|
+
'hydrator' in defaultState
|
|
31
|
+
) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const buildClassicStore = async <T>(
|
|
38
|
+
defaultState: DefaultState<T>,
|
|
39
|
+
): Promise<StoreBuilder<T>> => {
|
|
40
|
+
const initialState = isDefaultState<T>(defaultState)
|
|
41
|
+
? defaultState
|
|
42
|
+
: await defaultState.hydrator();
|
|
43
|
+
|
|
44
|
+
const store = new BehaviorSubject<T>(initialState);
|
|
45
|
+
|
|
46
|
+
const update: Updater<T> = (updater) => {
|
|
47
|
+
store.next(updater(store.getValue()));
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const useStore = () => {
|
|
51
|
+
const subscribe = (onStoreChange: () => void) => {
|
|
52
|
+
const subscription = store.subscribe({
|
|
53
|
+
next: onStoreChange,
|
|
54
|
+
error: console.error,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return () => {
|
|
58
|
+
subscription.unsubscribe();
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
const state: T = useSyncExternalStore(
|
|
62
|
+
subscribe,
|
|
63
|
+
() => store.getValue(),
|
|
64
|
+
() => initialState,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return [state, update] satisfies [T, Updater<T>];
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const useSelector = <S>(selector: (state: T) => S) => {
|
|
71
|
+
const [value] = useStore();
|
|
72
|
+
|
|
73
|
+
const selectedValue = selector(value);
|
|
74
|
+
|
|
75
|
+
return useMemo(() => selectedValue, [selectedValue]);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
defaultState,
|
|
80
|
+
useStore,
|
|
81
|
+
useSelector,
|
|
82
|
+
subject$: store,
|
|
83
|
+
update,
|
|
84
|
+
getValue: () => store.getValue(),
|
|
85
|
+
};
|
|
86
|
+
};
|
package/lib/dao.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { BehaviorSubject, type Observable } from 'rxjs';
|
|
2
|
+
import {
|
|
3
|
+
distinctUntilChanged,
|
|
4
|
+
filter,
|
|
5
|
+
map,
|
|
6
|
+
scan,
|
|
7
|
+
startWith,
|
|
8
|
+
tap,
|
|
9
|
+
} from 'rxjs/operators';
|
|
10
|
+
import {
|
|
11
|
+
type GetValueType,
|
|
12
|
+
type NestedEvent,
|
|
13
|
+
type PropertyPath,
|
|
14
|
+
type SystemEvent,
|
|
15
|
+
} from './types';
|
|
16
|
+
import { set, get } from 'lodash/fp';
|
|
17
|
+
|
|
18
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
19
|
+
|
|
20
|
+
export function createEventStore<T extends object>(
|
|
21
|
+
initialState: T,
|
|
22
|
+
options?: {
|
|
23
|
+
debug?: boolean;
|
|
24
|
+
hydrator: () => Promise<T>;
|
|
25
|
+
persist?: (state: T) => Promise<void>;
|
|
26
|
+
},
|
|
27
|
+
) {
|
|
28
|
+
const debug = options?.debug === true ? options.debug : false;
|
|
29
|
+
const globalEventStore = new BehaviorSubject<NestedEvent<T> | SystemEvent<T>>(
|
|
30
|
+
{
|
|
31
|
+
type: '@@INIT',
|
|
32
|
+
payload: initialState,
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
const publish = <
|
|
36
|
+
TType extends PropertyPath<T>,
|
|
37
|
+
TPayload extends GetValueType<T, TType>,
|
|
38
|
+
>(
|
|
39
|
+
type: TType,
|
|
40
|
+
payload: TPayload,
|
|
41
|
+
) => {
|
|
42
|
+
// eslint-disable-next-line
|
|
43
|
+
const event = { type, payload } as NestedEvent<T>;
|
|
44
|
+
globalEventStore.next(event);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const getPropertyObservable = <K extends PropertyPath<T>>(
|
|
48
|
+
eventType: K,
|
|
49
|
+
): Observable<GetValueType<T, K>> => {
|
|
50
|
+
return globalEventStore.pipe(
|
|
51
|
+
filter((event) => event.type.startsWith(eventType)),
|
|
52
|
+
map((event) => event.payload as GetValueType<T, K>),
|
|
53
|
+
scan((__, curr) => curr),
|
|
54
|
+
distinctUntilChanged(),
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const getHydrationObservable$ = (): Observable<T> => {
|
|
59
|
+
return globalEventStore.pipe(
|
|
60
|
+
filter((event) => event.type === '@@HYDRATED'),
|
|
61
|
+
map((event) => event.payload as T),
|
|
62
|
+
scan((__, curr) => curr),
|
|
63
|
+
distinctUntilChanged(),
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const state$ = new BehaviorSubject<T>(initialState);
|
|
68
|
+
|
|
69
|
+
globalEventStore
|
|
70
|
+
.pipe(
|
|
71
|
+
tap((event) => {
|
|
72
|
+
if (debug) {
|
|
73
|
+
console.info(event);
|
|
74
|
+
}
|
|
75
|
+
}),
|
|
76
|
+
scan((state, event) => {
|
|
77
|
+
if (event.type === '@@INIT' || event.type === '@@HYDRATED') {
|
|
78
|
+
return event.payload as T;
|
|
79
|
+
}
|
|
80
|
+
return set(event.type, event.payload, state);
|
|
81
|
+
}, initialState),
|
|
82
|
+
tap((state) => {
|
|
83
|
+
if (debug) {
|
|
84
|
+
console.info('State after update', state);
|
|
85
|
+
}
|
|
86
|
+
}),
|
|
87
|
+
startWith(initialState),
|
|
88
|
+
)
|
|
89
|
+
.subscribe(state$);
|
|
90
|
+
|
|
91
|
+
options
|
|
92
|
+
?.hydrator?.()
|
|
93
|
+
.then((payload) => {
|
|
94
|
+
globalEventStore.next({ type: '@@HYDRATED', payload });
|
|
95
|
+
})
|
|
96
|
+
.catch((error) => {
|
|
97
|
+
console.error('Failed to hydrate store', error);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
state$.subscribe({ next: options?.persist });
|
|
101
|
+
|
|
102
|
+
const useStoreValue = <K extends PropertyPath<T>>(
|
|
103
|
+
type: K,
|
|
104
|
+
): [GetValueType<T, K>, (payload: GetValueType<T, K>) => void] => {
|
|
105
|
+
const defaultValue: GetValueType<T, K> = get(type, state$.getValue());
|
|
106
|
+
const [value, setValue] = useState<GetValueType<T, K>>(defaultValue);
|
|
107
|
+
const handleUpdate = useCallback((payload: GetValueType<T, K>) => {
|
|
108
|
+
publish(type, payload);
|
|
109
|
+
}, []);
|
|
110
|
+
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
const subscription = getHydrationObservable$().subscribe({
|
|
113
|
+
next: (nextState) => {
|
|
114
|
+
setValue(get(type, nextState) as GetValueType<T, K>);
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return () => {
|
|
119
|
+
subscription.unsubscribe();
|
|
120
|
+
};
|
|
121
|
+
}, []);
|
|
122
|
+
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
const subscription = getPropertyObservable(type).subscribe({
|
|
125
|
+
next: (value) => {
|
|
126
|
+
setValue(value);
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
return () => {
|
|
130
|
+
subscription.unsubscribe();
|
|
131
|
+
};
|
|
132
|
+
}, []);
|
|
133
|
+
|
|
134
|
+
return [value, handleUpdate];
|
|
135
|
+
};
|
|
136
|
+
const useHydrateStore = () => {
|
|
137
|
+
return (payload: T) => {
|
|
138
|
+
globalEventStore.next({ type: '@@HYDRATED', payload });
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
const useIsHydrated = () => {
|
|
142
|
+
const [isHydrated, setIsHydrated] = useState(false);
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
const subscription = getHydrationObservable$().subscribe({
|
|
145
|
+
next: () => {
|
|
146
|
+
setIsHydrated(true);
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return () => {
|
|
151
|
+
subscription.unsubscribe();
|
|
152
|
+
};
|
|
153
|
+
}, []);
|
|
154
|
+
return isHydrated;
|
|
155
|
+
};
|
|
156
|
+
const systemEvents$ = globalEventStore.pipe(
|
|
157
|
+
filter((event) => event.type.startsWith('@@')),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
useStoreValue,
|
|
162
|
+
useHydrateStore,
|
|
163
|
+
useIsHydrated,
|
|
164
|
+
publish,
|
|
165
|
+
getPropertyObservable,
|
|
166
|
+
state$,
|
|
167
|
+
systemEvents$,
|
|
168
|
+
};
|
|
169
|
+
}
|
package/lib/index.ts
ADDED
package/lib/types.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type PropertyPath<T> = {
|
|
2
|
+
[K in keyof T]: K extends string
|
|
3
|
+
? T[K] extends object
|
|
4
|
+
? `${K}.${PropertyPath<T[K]>}` | K
|
|
5
|
+
: K
|
|
6
|
+
: never;
|
|
7
|
+
}[keyof T];
|
|
8
|
+
|
|
9
|
+
export type GetValueType<
|
|
10
|
+
T,
|
|
11
|
+
Path extends PropertyPath<T>,
|
|
12
|
+
> = Path extends `${infer Key}.${infer Rest}`
|
|
13
|
+
? Key extends keyof T
|
|
14
|
+
? Rest extends PropertyPath<T[Key]>
|
|
15
|
+
? GetValueType<T[Key], Rest>
|
|
16
|
+
: never
|
|
17
|
+
: never
|
|
18
|
+
: Path extends keyof T
|
|
19
|
+
? T[Path]
|
|
20
|
+
: never;
|
|
21
|
+
|
|
22
|
+
export interface NestedEvent<T> {
|
|
23
|
+
type: PropertyPath<T>;
|
|
24
|
+
payload: GetValueType<T, PropertyPath<T>>;
|
|
25
|
+
}
|
|
26
|
+
export type SystemEvent<T> =
|
|
27
|
+
| { type: '@@INIT'; payload: T }
|
|
28
|
+
| { type: '@@HYDRATED'; payload: T };
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dantian",
|
|
3
|
+
"version": "0.0.2-beta.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/dantian.cjs",
|
|
6
|
+
"module": "./dist/dantian.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"dev": "vite",
|
|
10
|
+
"test": "vitest",
|
|
11
|
+
"build": "tsc && vite build",
|
|
12
|
+
"preview": "vite preview",
|
|
13
|
+
"format": "prettier --write .",
|
|
14
|
+
"lint": "eslint ./lib/**/*.ts",
|
|
15
|
+
"prepare": "husky install",
|
|
16
|
+
"storybook": "storybook dev -p 6006",
|
|
17
|
+
"build-storybook": "storybook build",
|
|
18
|
+
"e2e": "playwright test"
|
|
19
|
+
},
|
|
20
|
+
"lint-staged": {
|
|
21
|
+
"**/*.{js,ts,tsx}": [
|
|
22
|
+
"eslint --fix"
|
|
23
|
+
],
|
|
24
|
+
"**/*": "prettier --write --ignore-unknown"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"react": ">=18"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@commitlint/cli": "^18.6.1",
|
|
31
|
+
"@commitlint/config-conventional": "^18.6.2",
|
|
32
|
+
"@playwright/test": "^1.41.2",
|
|
33
|
+
"@radix-ui/themes": "^2.0.3",
|
|
34
|
+
"@storybook/addon-essentials": "^7.6.17",
|
|
35
|
+
"@storybook/addon-interactions": "^7.6.17",
|
|
36
|
+
"@storybook/addon-links": "^7.6.17",
|
|
37
|
+
"@storybook/blocks": "^7.6.17",
|
|
38
|
+
"@storybook/react": "^7.6.17",
|
|
39
|
+
"@storybook/react-vite": "^7.6.17",
|
|
40
|
+
"@storybook/test": "^7.6.17",
|
|
41
|
+
"@storybook/testing-library": "^0.2.2",
|
|
42
|
+
"@types/lodash.set": "^4",
|
|
43
|
+
"@types/node": "^20.11.20",
|
|
44
|
+
"@typescript-eslint/eslint-plugin": "^6.4.0",
|
|
45
|
+
"@typescript-eslint/parser": "^7.0.2",
|
|
46
|
+
"@vitest/ui": "^1.3.1",
|
|
47
|
+
"eslint": "^8.0.1",
|
|
48
|
+
"eslint-config-prettier": "^9.1.0",
|
|
49
|
+
"eslint-config-standard-with-typescript": "^43.0.1",
|
|
50
|
+
"eslint-plugin-import": "^2.25.2",
|
|
51
|
+
"eslint-plugin-n": "^15.0.0 || ^16.0.0 ",
|
|
52
|
+
"eslint-plugin-promise": "^6.0.0",
|
|
53
|
+
"eslint-plugin-react": "^7.33.2",
|
|
54
|
+
"eslint-plugin-storybook": "^0.8.0",
|
|
55
|
+
"husky": "^9.0.11",
|
|
56
|
+
"jsdom": "^24.0.0",
|
|
57
|
+
"lint-staged": "^15.2.2",
|
|
58
|
+
"prettier": "^3.2.5",
|
|
59
|
+
"react": "18.2.0",
|
|
60
|
+
"react-dom": "^18.2.0",
|
|
61
|
+
"standard-version": "^9.5.0",
|
|
62
|
+
"storybook": "^7.6.17",
|
|
63
|
+
"typescript": "*",
|
|
64
|
+
"vite": "^5.1.4",
|
|
65
|
+
"vite-plugin-dts": "^3.7.3",
|
|
66
|
+
"vitest": "^1.3.1"
|
|
67
|
+
},
|
|
68
|
+
"packageManager": "yarn@4.1.0",
|
|
69
|
+
"dependencies": {
|
|
70
|
+
"lodash.set": "^4.3.2",
|
|
71
|
+
"rxjs": "^7.8.1"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Read environment variables from file.
|
|
5
|
+
* https://github.com/motdotla/dotenv
|
|
6
|
+
*/
|
|
7
|
+
// require('dotenv').config();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* See https://playwright.dev/docs/test-configuration.
|
|
11
|
+
*/
|
|
12
|
+
export default defineConfig({
|
|
13
|
+
testDir: './e2e',
|
|
14
|
+
/* Run tests in files in parallel */
|
|
15
|
+
fullyParallel: true,
|
|
16
|
+
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
|
17
|
+
forbidOnly: !!process.env.CI,
|
|
18
|
+
/* Retry on CI only */
|
|
19
|
+
retries: process.env.CI ? 2 : 0,
|
|
20
|
+
/* Opt out of parallel tests on CI. */
|
|
21
|
+
workers: process.env.CI ? 1 : undefined,
|
|
22
|
+
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
|
23
|
+
reporter: 'html',
|
|
24
|
+
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
|
25
|
+
use: {
|
|
26
|
+
/* Base URL to use in actions like `await page.goto('/')`. */
|
|
27
|
+
// baseURL: 'http://127.0.0.1:3000',
|
|
28
|
+
|
|
29
|
+
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
|
30
|
+
trace: 'on-first-retry',
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
/* Configure projects for major browsers */
|
|
34
|
+
projects: [
|
|
35
|
+
{
|
|
36
|
+
name: 'chromium',
|
|
37
|
+
use: { ...devices['Desktop Chrome'] },
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
{
|
|
41
|
+
name: 'firefox',
|
|
42
|
+
use: { ...devices['Desktop Firefox'] },
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
{
|
|
46
|
+
name: 'webkit',
|
|
47
|
+
use: { ...devices['Desktop Safari'] },
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
/* Test against mobile viewports. */
|
|
51
|
+
// {
|
|
52
|
+
// name: 'Mobile Chrome',
|
|
53
|
+
// use: { ...devices['Pixel 5'] },
|
|
54
|
+
// },
|
|
55
|
+
// {
|
|
56
|
+
// name: 'Mobile Safari',
|
|
57
|
+
// use: { ...devices['iPhone 12'] },
|
|
58
|
+
// },
|
|
59
|
+
|
|
60
|
+
/* Test against branded browsers. */
|
|
61
|
+
// {
|
|
62
|
+
// name: 'Microsoft Edge',
|
|
63
|
+
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
|
64
|
+
// },
|
|
65
|
+
// {
|
|
66
|
+
// name: 'Google Chrome',
|
|
67
|
+
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
|
68
|
+
// },
|
|
69
|
+
],
|
|
70
|
+
|
|
71
|
+
/* Run your local dev server before starting the tests */
|
|
72
|
+
// webServer: {
|
|
73
|
+
// command: 'npm run start',
|
|
74
|
+
// url: 'http://127.0.0.1:3000',
|
|
75
|
+
// reuseExistingServer: !process.env.CI,
|
|
76
|
+
// },
|
|
77
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Flex, Text, Button } from '@radix-ui/themes';
|
|
3
|
+
import useStore, { useCounterSelector } from './counterStore';
|
|
4
|
+
|
|
5
|
+
const CountText = () => {
|
|
6
|
+
const count = useCounterSelector((state) => state.count);
|
|
7
|
+
|
|
8
|
+
return <Text>We are counting first: {count}</Text>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const Counter = () => {
|
|
12
|
+
const [state, updateCount] = useStore();
|
|
13
|
+
const increment = () =>
|
|
14
|
+
updateCount((prevState) => ({ ...prevState, count: prevState.count + 1 }));
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<Flex direction="column" gap="2">
|
|
18
|
+
<CountText />
|
|
19
|
+
<Button onClick={increment}>Let's go, first counter</Button>
|
|
20
|
+
</Flex>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Flex, Text, Button } from '@radix-ui/themes';
|
|
3
|
+
|
|
4
|
+
import useStore, { useCounter2Selector } from './counterStore2';
|
|
5
|
+
|
|
6
|
+
const CountText = () => {
|
|
7
|
+
const count = useCounter2Selector((state) => state.count);
|
|
8
|
+
|
|
9
|
+
return <Text>We are counting second: {count}</Text>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const Counter2 = () => {
|
|
13
|
+
const [state, updateCount] = useStore();
|
|
14
|
+
const increment = () =>
|
|
15
|
+
updateCount((prevState) => ({ ...prevState, count: prevState.count + 1 }));
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Flex direction="column" gap="2">
|
|
19
|
+
<CountText />
|
|
20
|
+
<Button onClick={increment}>Let's go, second counter</Button>
|
|
21
|
+
</Flex>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Flex, Text, Button } from '@radix-ui/themes';
|
|
3
|
+
import useStore from './counterStore2';
|
|
4
|
+
|
|
5
|
+
export const Counter3 = () => {
|
|
6
|
+
const [state, updateCount] = useStore();
|
|
7
|
+
const increment = () =>
|
|
8
|
+
updateCount((prevState) => ({ ...prevState, count: prevState.count + 1 }));
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<Flex direction="column" gap="2">
|
|
12
|
+
<Text>We are counting third: {state.count}</Text>
|
|
13
|
+
<Button onClick={increment}>
|
|
14
|
+
Let's go, third counter, reusing second store
|
|
15
|
+
</Button>
|
|
16
|
+
</Flex>
|
|
17
|
+
);
|
|
18
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Button, Flex, Text } from '@radix-ui/themes';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import useStore, { useCounterPromiseSelector } from './counterStorePromise';
|
|
4
|
+
|
|
5
|
+
export const DefaultPromise = () => {
|
|
6
|
+
const [, updateCount] = useStore();
|
|
7
|
+
const count = useCounterPromiseSelector((state) => state.count);
|
|
8
|
+
const increment = () =>
|
|
9
|
+
updateCount((prevState) => ({ ...prevState, count: prevState.count + 1 }));
|
|
10
|
+
return (
|
|
11
|
+
<Flex direction="column" gap="2">
|
|
12
|
+
<Text>We are counting hydrating default: {count}</Text>
|
|
13
|
+
<Button onClick={increment}>Let's go, promise counter</Button>
|
|
14
|
+
</Flex>
|
|
15
|
+
);
|
|
16
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Flex } from '@radix-ui/themes';
|
|
2
|
+
import { Counter } from './Counter';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Counter2 } from './Counter2';
|
|
5
|
+
import { Counter3 } from './Counter3';
|
|
6
|
+
import { DefaultPromise } from './DefaultPromise';
|
|
7
|
+
|
|
8
|
+
export const Root = () => {
|
|
9
|
+
return (
|
|
10
|
+
<Flex direction="column" gap="2">
|
|
11
|
+
<Counter />
|
|
12
|
+
<Counter2 />
|
|
13
|
+
<Counter3 />
|
|
14
|
+
<DefaultPromise />
|
|
15
|
+
</Flex>
|
|
16
|
+
);
|
|
17
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { buildClassicStore } from '../../lib';
|
|
2
|
+
|
|
3
|
+
export interface CounterState {
|
|
4
|
+
count: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const counterStoreBuilder = await buildClassicStore<CounterState>({ count: 0 });
|
|
8
|
+
const { useStore, useSelector: useCounterSelector } = counterStoreBuilder;
|
|
9
|
+
|
|
10
|
+
export { useCounterSelector };
|
|
11
|
+
export default useStore;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { buildClassicStore } from '../../lib';
|
|
2
|
+
|
|
3
|
+
export interface Counter2State {
|
|
4
|
+
count: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const counter2StoreBuilder = await buildClassicStore<Counter2State>({
|
|
8
|
+
count: 0,
|
|
9
|
+
});
|
|
10
|
+
const { useStore, useSelector: useCounter2Selector } = counter2StoreBuilder;
|
|
11
|
+
|
|
12
|
+
export { useCounter2Selector };
|
|
13
|
+
export default useStore;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { buildClassicStore } from '../../lib';
|
|
2
|
+
|
|
3
|
+
export interface CounterState {
|
|
4
|
+
count: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const counterStore3Builder = await buildClassicStore<CounterState>({
|
|
8
|
+
hydrator: () => Promise.resolve({ count: 88 }),
|
|
9
|
+
beforeLoadState: { count: 0 },
|
|
10
|
+
});
|
|
11
|
+
const { useStore, useSelector: useCounterPromiseSelector } =
|
|
12
|
+
counterStore3Builder;
|
|
13
|
+
|
|
14
|
+
export { useCounterPromiseSelector };
|
|
15
|
+
export default useStore;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { Root } from './counter/Root';
|
|
3
|
+
|
|
4
|
+
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Dantian/Counter',
|
|
7
|
+
component: Root,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: 'centered',
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
tags: ['autodocs'],
|
|
13
|
+
|
|
14
|
+
argTypes: {},
|
|
15
|
+
} satisfies Meta<typeof Root>;
|
|
16
|
+
|
|
17
|
+
export default meta;
|
|
18
|
+
type Story = StoryObj<typeof meta>;
|
|
19
|
+
|
|
20
|
+
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
|
|
21
|
+
export const Primary: Story = {
|
|
22
|
+
args: {},
|
|
23
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Flex } from '@radix-ui/themes';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { state$, useStoreValue } from './store';
|
|
5
|
+
|
|
6
|
+
export const Root = () => {
|
|
7
|
+
const [name] = useStoreValue('user.name');
|
|
8
|
+
return (
|
|
9
|
+
<Flex direction="column" gap="2">
|
|
10
|
+
<Flex direction="row" gap="2" align={'center'}>
|
|
11
|
+
State: <pre>{JSON.stringify(state$.getValue())}</pre>
|
|
12
|
+
</Flex>
|
|
13
|
+
|
|
14
|
+
<Flex direction="row" gap="2" align={'center'}>
|
|
15
|
+
User name: {name}
|
|
16
|
+
</Flex>
|
|
17
|
+
</Flex>
|
|
18
|
+
);
|
|
19
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { wuji } from '../../lib';
|
|
2
|
+
|
|
3
|
+
export const { state$, useStoreValue, useHydrateStore, useIsHydrated } =
|
|
4
|
+
wuji<State>(
|
|
5
|
+
{
|
|
6
|
+
user: { address: { city: 'n/a', street: 'n/a' }, name: 'n/a' },
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
hydrator: () => {
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
setTimeout(
|
|
12
|
+
() =>
|
|
13
|
+
resolve({
|
|
14
|
+
user: {
|
|
15
|
+
address: {
|
|
16
|
+
city: 'Aubonne',
|
|
17
|
+
street: 'Chemin du Mont-Blanc 16',
|
|
18
|
+
},
|
|
19
|
+
name: 'Julius',
|
|
20
|
+
},
|
|
21
|
+
}),
|
|
22
|
+
3000,
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
debug: true,
|
|
27
|
+
},
|
|
28
|
+
);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { Root } from './event-store/Root';
|
|
3
|
+
|
|
4
|
+
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Dantian/Event Store',
|
|
7
|
+
component: Root,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: 'centered',
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
tags: ['autodocs'],
|
|
13
|
+
|
|
14
|
+
argTypes: {},
|
|
15
|
+
} satisfies Meta<typeof Root>;
|
|
16
|
+
|
|
17
|
+
export default meta;
|
|
18
|
+
type Story = StoryObj<typeof meta>;
|
|
19
|
+
|
|
20
|
+
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
|
|
21
|
+
export const Primary: Story = {
|
|
22
|
+
args: {},
|
|
23
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
|
|
10
|
+
/* Bundler mode */
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
/* Linting */
|
|
18
|
+
"strict": true,
|
|
19
|
+
"noUnusedLocals": true,
|
|
20
|
+
"noUnusedParameters": true,
|
|
21
|
+
"noFallthroughCasesInSwitch": true
|
|
22
|
+
},
|
|
23
|
+
"include": ["lib"]
|
|
24
|
+
}
|
package/vite.config.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import dts from 'vite-plugin-dts';
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
build: {
|
|
5
|
+
copyPublicDir: false,
|
|
6
|
+
lib: {
|
|
7
|
+
entry: 'lib/index.ts',
|
|
8
|
+
name: 'dantian',
|
|
9
|
+
formats: ['es', 'cjs'],
|
|
10
|
+
},
|
|
11
|
+
rollupOptions: {
|
|
12
|
+
external: ['react', 'react-dom'],
|
|
13
|
+
output: {
|
|
14
|
+
globals: {
|
|
15
|
+
react: 'React',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
plugins: [dts({ include: ['lib'], insertTypesEntry: true })],
|
|
21
|
+
});
|