@warkypublic/zustandsyncstore 0.0.1

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.
@@ -0,0 +1,8 @@
1
+ # Changesets
2
+
3
+ Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4
+ with multi-package repos, or single-package repos to help you version and publish your code. You can
5
+ find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6
+
7
+ We have a quick list of common questions to get you started engaging with this project in
8
+ [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
@@ -0,0 +1,11 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
3
+ "changelog": "@changesets/cli/changelog",
4
+ "commit": true,
5
+ "fixed": [],
6
+ "linked": [],
7
+ "access": "public",
8
+ "baseBranch": "main",
9
+ "updateInternalDependencies": "patch",
10
+ "ignore": []
11
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # @warkypublic/zustandsyncstore
2
+
3
+ ## 0.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 0ed2a54: First Release
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Vitaly Rtishchev
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
package/README.md ADDED
@@ -0,0 +1,205 @@
1
+ # ZustandSyncStore
2
+
3
+ A React library that provides synchronized Zustand stores with prop-based state management and persistence support.
4
+
5
+ ## Features
6
+
7
+ - **Prop Synchronization**: Automatically sync React props with Zustand store state
8
+ - **Context-based**: Provides a Provider/Consumer pattern for scoped store access
9
+ - **Persistence**: Built-in support for state persistence via Zustand middleware
10
+ - **TypeScript**: Full TypeScript support with type inference
11
+ - **Selective Sync**: Control which props are synced and when
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install @warkypublic/zustandsyncstore
17
+ ```
18
+
19
+ ### Peer Dependencies
20
+
21
+ This package requires the following peer dependencies:
22
+
23
+ ```bash
24
+ npm install react zustand use-sync-external-store
25
+ ```
26
+
27
+ ## Basic Usage
28
+
29
+ ```tsx
30
+ import { createSyncStore } from '@warkypublic/zustandsyncstore';
31
+
32
+ // Define your state type
33
+ interface MyState {
34
+ count: number;
35
+ increment: () => void;
36
+ }
37
+
38
+ // Define your props type
39
+ interface MyProps {
40
+ initialCount: number;
41
+ }
42
+
43
+ // Create the synchronized store
44
+ const { Provider, useStore } = createSyncStore<MyState, MyProps>(
45
+ (set) => ({
46
+ count: 0,
47
+ increment: () => set((state) => ({ count: state.count + 1 })),
48
+ })
49
+ );
50
+
51
+ // Component that uses the store
52
+ function Counter() {
53
+ const { count, increment } = useStore();
54
+
55
+ return (
56
+ <div>
57
+ <p>Count: {count}</p>
58
+ <button onClick={increment}>Increment</button>
59
+ </div>
60
+ );
61
+ }
62
+
63
+ // App component with Provider
64
+ function App() {
65
+ return (
66
+ <Provider initialCount={10}>
67
+ <Counter />
68
+ </Provider>
69
+ );
70
+ }
71
+ ```
72
+
73
+ ## Advanced Usage
74
+
75
+ ### With Custom Hook Logic
76
+
77
+ ```tsx
78
+ const { Provider, useStore } = createSyncStore<MyState, MyProps>(
79
+ (set) => ({
80
+ count: 0,
81
+ increment: () => set((state) => ({ count: state.count + 1 })),
82
+ }),
83
+ ({ useStore, useStoreApi, initialCount }) => {
84
+ // Custom hook logic here
85
+ const currentCount = useStore(state => state.count);
86
+
87
+ // Return additional props to sync
88
+ return {
89
+ computedValue: initialCount * 2
90
+ };
91
+ }
92
+ );
93
+ ```
94
+
95
+ ### With Persistence
96
+
97
+ ```tsx
98
+ function App() {
99
+ return (
100
+ <Provider
101
+ initialCount={10}
102
+ persist={{
103
+ name: 'my-store',
104
+ storage: localStorage,
105
+ }}
106
+ >
107
+ <Counter />
108
+ </Provider>
109
+ );
110
+ }
111
+ ```
112
+
113
+ ### Selective Prop Syncing
114
+
115
+ ```tsx
116
+ function App() {
117
+ return (
118
+ <Provider
119
+ initialCount={10}
120
+ otherProp="value"
121
+ firstSyncProps={['initialCount']} // Only sync these props initially
122
+ >
123
+ <Counter />
124
+ </Provider>
125
+ );
126
+ }
127
+ ```
128
+
129
+ ## API Reference
130
+
131
+ ### `createSyncStore<TState, TProps>(createState?, useValue?)`
132
+
133
+ Creates a synchronized Zustand store.
134
+
135
+ **Parameters:**
136
+ - `createState` (optional): Zustand state creator function
137
+ - `useValue` (optional): Custom hook function that receives props and store access
138
+
139
+ **Returns:**
140
+ - `Provider`: React component that provides the store context
141
+ - `useStore`: Hook to access the store state
142
+
143
+ ### Provider Props
144
+
145
+ - `children`: React children
146
+ - `firstSyncProps` (optional): Array of prop names to sync only on first render
147
+ - `persist` (optional): Zustand persist options
148
+ - `...TProps`: Your custom props that will be synced with the store
149
+
150
+ ### useStore Hook
151
+
152
+ Can be used with or without a selector:
153
+
154
+ ```tsx
155
+ // Get entire state
156
+ const state = useStore();
157
+
158
+ // With selector
159
+ const count = useStore(state => state.count);
160
+
161
+ // With equality function
162
+ const count = useStore(state => state.count, (a, b) => a === b);
163
+ ```
164
+
165
+ ## TypeScript Support
166
+
167
+ The library is fully typed and provides type inference for your state and props:
168
+
169
+ ```tsx
170
+ interface State {
171
+ value: string;
172
+ }
173
+
174
+ interface Props {
175
+ defaultValue: string;
176
+ }
177
+
178
+ const { Provider, useStore } = createSyncStore<State, Props>(...);
179
+
180
+ // TypeScript will infer the correct types
181
+ const { value } = useStore(); // value is string
182
+ ```
183
+
184
+ ## Development
185
+
186
+ ```bash
187
+ # Install dependencies
188
+ pnpm install
189
+
190
+ # Run development server
191
+ pnpm dev
192
+
193
+ # Build for production
194
+ pnpm build
195
+
196
+ # Run linting
197
+ pnpm lint
198
+
199
+ # Run type checking
200
+ pnpm typecheck
201
+ ```
202
+
203
+ ## License
204
+
205
+ MIT
@@ -0,0 +1,23 @@
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import tseslint from 'typescript-eslint'
6
+ import { globalIgnores } from 'eslint/config'
7
+
8
+ export default tseslint.config([
9
+ globalIgnores(['dist']),
10
+ {
11
+ files: ['**/*.{ts,tsx}'],
12
+ extends: [
13
+ js.configs.recommended,
14
+ tseslint.configs.recommended,
15
+ reactHooks.configs['recommended-latest'],
16
+ reactRefresh.configs.vite,
17
+ ],
18
+ languageOptions: {
19
+ ecmaVersion: 2020,
20
+ globals: globals.browser,
21
+ },
22
+ },
23
+ ])
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@warkypublic/zustandsyncstore",
3
+ "author": "Warky Devs",
4
+ "version": "0.0.1",
5
+ "type": "module",
6
+ "module": "./dist/lib.es.js",
7
+ "types": "./dist/lib.d.ts",
8
+ "publishConfig": {
9
+ "require": "./dist/lib.cjs.js"
10
+ },
11
+ "dependencies": {
12
+ "@warkypublic/artemis-kit": "^1.0.10"
13
+ },
14
+ "devDependencies": {
15
+ "@eslint/js": "^9.35.0",
16
+ "@types/node": "^24.4.0",
17
+ "@types/react": "^19.1.13",
18
+ "@types/react-dom": "^19.1.9",
19
+ "@typescript-eslint/parser": "^8.43.0",
20
+ "@vitejs/plugin-react-swc": "^4.0.1",
21
+ "@changesets/cli": "^2.29.7",
22
+ "eslint": "^9.35.0",
23
+ "eslint-plugin-react-hooks": "^5.2.0",
24
+ "eslint-plugin-react-refresh": "^0.4.20",
25
+ "global": "^4.4.0",
26
+ "globals": "^16.4.0",
27
+ "prettier": "^3.6.2",
28
+ "prettier-eslint": "^16.4.2",
29
+ "react-dom": "^19.1.1",
30
+ "typescript": "~5.9.2",
31
+ "typescript-eslint": "^8.43.0",
32
+ "vite": "^7.1.5",
33
+ "vite-tsconfig-paths": "^5.1.4"
34
+ },
35
+ "peerDependencies": {
36
+ "react": ">= 19.0.0",
37
+ "use-sync-external-store": ">= 1.4.0",
38
+ "zustand": ">= 5.0.0"
39
+ },
40
+ "scripts": {
41
+ "dev": "vite",
42
+ "build": "tsc -b && vite build",
43
+ "lint": "eslint ./src",
44
+ "typecheck": "tsc --noEmit",
45
+ "clean": "rm -rf node_modules && rm -rf dist ",
46
+ "preview": "vite preview"
47
+ },
48
+ "main": "./dist/lib.cjs.js",
49
+ "typings": "./dist/lib.d.ts",
50
+ "exports": {
51
+ ".": {
52
+ "import": "./dist/lib.es.js",
53
+ "types": "./dist/lib.d.ts",
54
+ "default": "./dist/lib.cjs.js"
55
+ },
56
+ "./package.json": "./package.json"
57
+ }
58
+ }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
@@ -0,0 +1,170 @@
1
+
2
+ import {decycle} from '@warkypublic/artemis-kit/object'
3
+ import React, { createContext, type ReactNode, useContext, useLayoutEffect, useRef } from 'react';
4
+ import { createStore as createZustandStore, type StateCreator, type StoreApi } from 'zustand';
5
+ import { persist, type PersistOptions } from 'zustand/middleware';
6
+ import { useShallow } from 'zustand/react/shallow';
7
+ import { useStoreWithEqualityFn } from 'zustand/traditional';
8
+
9
+ export type SyncStoreReturn<TState, TProps> = {
10
+ Provider: (
11
+ props: {
12
+ children: ReactNode;
13
+ } & {
14
+ firstSyncProps?: string[];
15
+ persist?: PersistOptions<Partial<TProps & TState>>;
16
+ } & TProps
17
+ ) => React.ReactNode;
18
+ useStore: {
19
+ (): LocalUseStore<TState, TProps>;
20
+ <U>(
21
+ selector: (state: LocalUseStore<TState, TProps>) => U,
22
+ equalityFn?: (a: U, b: U) => boolean
23
+ ): U;
24
+ };
25
+ };
26
+
27
+ type CreateContextUseStore<TState> = {
28
+ (): TState;
29
+ <U>(selector: (state: TState) => U, equalityFn?: (a: U, b: U) => boolean): U;
30
+ };
31
+
32
+ type LocalUseStore<TState, TProps> = {
33
+ $sync?: (props: TProps) => void;
34
+ } & TState;
35
+
36
+ export function createSyncStore<TState, TProps>(
37
+ createState?: StateCreator<TState>,
38
+ useValue?: (
39
+ props: {
40
+ useStore: CreateContextUseStore<LocalUseStore<TState, TProps>>;
41
+ useStoreApi: StoreApi<LocalUseStore<TState, TProps>>;
42
+ } & TProps
43
+ ) => any
44
+ ): SyncStoreReturn<TState, TProps> {
45
+ const StoreContext = createContext<null | StoreApi<LocalUseStore<TState, TProps>>>(null);
46
+
47
+ function useStore(): LocalUseStore<TState, TProps>;
48
+ function useStore<U>(
49
+ selector: (state: LocalUseStore<TState, TProps>) => U,
50
+ equalityFn?: (a: U, b: U) => boolean
51
+ ): U;
52
+ function useStore<U>(
53
+ selector?: (state: LocalUseStore<TState, TProps>) => U,
54
+ equalityFn?: (a: U, b: U) => boolean
55
+ ) {
56
+ const store = useContext(StoreContext);
57
+ if (!store) {
58
+ throw new Error('Missing StoreProvider');
59
+ }
60
+ return useStoreWithEqualityFn(
61
+ store,
62
+ selector ? useShallow(selector) : (state) => state as unknown as U,
63
+ equalityFn
64
+ );
65
+ }
66
+
67
+ const Hook = ({
68
+ firstSyncProps,
69
+ ...others
70
+ }: {
71
+ firstSyncProps: string[];
72
+ } & TProps) => {
73
+ const syncedProps = useRef(false);
74
+
75
+ const { $sync } = useStore((state) => ({ $sync: state.$sync }));
76
+ const storeApi = useContext<null | StoreApi<LocalUseStore<TState, TProps>>>(StoreContext);
77
+
78
+ if (firstSyncProps) {
79
+ for (const key of firstSyncProps) {
80
+ if (syncedProps.current) {
81
+ //@ts-ignore
82
+ delete others[key];
83
+ }
84
+ }
85
+ }
86
+
87
+ useLayoutEffect(
88
+ () => {
89
+ //@ts-ignore
90
+ $sync?.(others);
91
+ //method was actually called
92
+ if ($sync) {
93
+ syncedProps.current = true;
94
+ }
95
+ },
96
+ [JSON.stringify(decycle(others ?? {}))]
97
+ );
98
+
99
+ if (useValue) {
100
+ // @ts-ignore
101
+ const returned = useValue({
102
+ ...others,
103
+ useStore,
104
+ useStoreApi: storeApi
105
+ });
106
+
107
+ useLayoutEffect(() => {
108
+ if (returned && typeof returned === 'object') {$sync?.(returned);}
109
+ }, [returned]);
110
+ }
111
+
112
+ return null;
113
+ };
114
+
115
+ // eslint-disable-next-line no-param-reassign
116
+ createState = createState || (() => ({}) as TState);
117
+
118
+ const StoreProvider = (
119
+ props: {
120
+ children: ReactNode;
121
+ } & {
122
+ firstSyncProps?: string[];
123
+ persist?: PersistOptions<Partial<TProps & TState>>;
124
+ } & TProps
125
+ ) => {
126
+ const storeRef = useRef<StoreApi<LocalUseStore<TState, TProps>>>(null);
127
+
128
+ const { children, ...propsWithoutChildren } = props;
129
+
130
+ if (!storeRef.current) {
131
+ if (props?.persist) {
132
+ storeRef.current = createZustandStore<LocalUseStore<TState, TProps>>(
133
+ //@ts-ignore
134
+ persist(
135
+ //@ts-ignore
136
+ (set, get, api) => ({
137
+ //@ts-ignore
138
+ ...createState?.(set, get, api),
139
+ $sync: (props: TProps) => set((state) => ({ ...state, ...props }))
140
+ }),
141
+ { ...props?.persist }
142
+ )
143
+ );
144
+ } else {
145
+ storeRef.current = createZustandStore<LocalUseStore<TState, TProps>>(
146
+ // @ts-ignore
147
+ (set, get, api) => ({
148
+ ...createState?.(set, get, api),
149
+ $sync: (props: TProps) => set((state) => ({ ...state, ...props }))
150
+ })
151
+ );
152
+ }
153
+ }
154
+
155
+ return (
156
+ <StoreContext.Provider value={storeRef.current}>
157
+ {/* @ts-ignore*/}
158
+ <Hook {...propsWithoutChildren} firstSyncProps={props.firstSyncProps} />
159
+ {children}
160
+ </StoreContext.Provider>
161
+ );
162
+ };
163
+
164
+ return {
165
+ Provider: StoreProvider,
166
+ useStore
167
+ };
168
+ }
169
+
170
+ export default createSyncStore;
package/src/lib.ts ADDED
@@ -0,0 +1 @@
1
+ export {createSyncStore, type SyncStoreReturn} from './SyncStore'
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,27 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "ES2022",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+ "jsx": "react-jsx",
17
+
18
+ /* Linting */
19
+ "strict": true,
20
+ "noUnusedLocals": true,
21
+ "noUnusedParameters": true,
22
+ "erasableSyntaxOnly": true,
23
+ "noFallthroughCasesInSwitch": true,
24
+ "noUncheckedSideEffectImports": true
25
+ },
26
+ "include": ["src"]
27
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "ES2023",
5
+ "lib": ["ES2023"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+
9
+ /* Bundler mode */
10
+ "moduleResolution": "bundler",
11
+ "allowImportingTsExtensions": true,
12
+ "verbatimModuleSyntax": true,
13
+ "moduleDetection": "force",
14
+ "noEmit": true,
15
+
16
+ /* Linting */
17
+ "strict": true,
18
+ "noUnusedLocals": true,
19
+ "noUnusedParameters": true,
20
+ "erasableSyntaxOnly": true,
21
+ "noFallthroughCasesInSwitch": true,
22
+ "noUncheckedSideEffectImports": true
23
+ },
24
+ "include": ["vite.config.ts"]
25
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,26 @@
1
+ import { defineConfig } from "vite";
2
+ import { dirname } from "node:path";
3
+ import * as path from "path";
4
+ import { fileURLToPath } from "node:url";
5
+ import react from "@vitejs/plugin-react-swc";
6
+ import { peerDependencies } from "./package.json";
7
+
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+
10
+ // https://vite.dev/config/
11
+ export default defineConfig({
12
+ plugins: [react()],
13
+
14
+ build: {
15
+ lib: {
16
+ entry: path.resolve(__dirname, "src/lib.ts"),
17
+ name: "lib",
18
+ formats: ["es", "cjs"],
19
+ fileName: (format) => `lib.${format}.js`,
20
+ },
21
+ emptyOutDir: true,
22
+ rollupOptions: {
23
+ external: Object.keys(peerDependencies),
24
+ },
25
+ },
26
+ });