diff --git a/package.json b/package.json index 858095a..67ca81c 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,14 @@ "type": "module", "scripts": { "dev": "rollup -c -w", + "dev:lib": "rollup -c -w", "build": "npm run clean && rollup -c", "build:app": "npm run build && rsync dist/* ../deploy/dist", "clean": "rm -rf dist" }, "files": [ - "dist" + "dist", + "react/dist" ], "keywords": [ "kevisual", @@ -23,20 +25,21 @@ "license": "ISC", "description": "", "devDependencies": { - "@rollup/plugin-commonjs": "^28.0.1", - "@rollup/plugin-node-resolve": "^15.3.0", - "@rollup/plugin-typescript": "^12.1.1", + "@rollup/plugin-commonjs": "^28.0.3", + "@rollup/plugin-node-resolve": "^16.0.1", + "@rollup/plugin-typescript": "^12.1.2", "@types/lodash-es": "^4.17.12", "fast-deep-equal": "^3.1.3", "immer": "^10.1.1", "lodash-es": "^4.17.21", - "nanoid": "^5.0.9", - "rollup": "^4.27.4", - "rollup-plugin-dts": "^6.1.1", + "nanoid": "^5.1.5", + "rollup": "^4.37.0", + "rollup-plugin-dts": "^6.2.1", "ts-node": "^10.9.2", "tslib": "^2.8.1", - "typescript": "^5.7.2", - "zustand": "^5.0.1" + "tsup": "^8.4.0", + "typescript": "^5.8.2", + "zustand": "^5.0.3" }, "publishConfig": { "access": "public" @@ -65,6 +68,10 @@ "./web": { "import": "./dist/web.js", "require": "./dist/web.js" + }, + "./react": { + "import": "./react/dist/store-react.js", + "types": "./react/dist/index.d.ts" } }, "dependencies": { diff --git a/react/package.json b/react/package.json new file mode 100644 index 0000000..aeebf2d --- /dev/null +++ b/react/package.json @@ -0,0 +1,23 @@ +{ + "name": "@kevisual/store-react", + "version": "0.0.1", + "description": "", + "main": "index.js", + "scripts": { + "dev:lib": "vite build --watch", + "build:lib": "vite build" + }, + "keywords": [], + "author": "abearxiong (https://www.xiongxiao.me)", + "license": "MIT", + "packageManager": "pnpm@10.6.5", + "type": "module", + "peerDependencies": { + "react": "^19.0.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.4", + "vite": "^6.2.3", + "vite-plugin-dts": "^4.5.3" + } +} \ No newline at end of file diff --git a/react/src/Store.tsx b/react/src/Store.tsx new file mode 100644 index 0000000..3a62911 --- /dev/null +++ b/react/src/Store.tsx @@ -0,0 +1,58 @@ +import { createContext, useContext, useEffect, useMemo, useState } from 'react'; +import { StateCreator } from '../../src/store'; +import { shallow, useShallow } from 'zustand/shallow'; +import { useContextKey } from '../../src/web-context'; + +export const StoreContext = createContext(null); + +export const initStoreFn: StateCreator = (set, get, store) => { + return { + mark: '123', + setMark: (mark: string) => set({ mark }), + info: 'info', + setInfo: (info) => set({ info }), + }; +}; + +export const StoreContextProvider = ({ + children, + id, + stateCreator, +}: { + children: React.ReactNode; + id: string; + stateCreator?: StateCreator; +}) => { + const store = useContextKey('store'); + if (!store) { + console.error('store not found'); + return null; + } + const smStore = useMemo(() => { + return store.createIfNotExists(stateCreator || initStoreFn, id); + }, [id]); + const [state, setState] = useState(smStore); + useEffect(() => { + setState(smStore); + }, [smStore]); + return {children}; +}; + +export const useStore = (selector?: any) => { + const store = useContext(StoreContext); + const allState = store.getState(); + const selectedState = selector ? selector(allState) : allState; + const [state, setState] = useState(selectedState); + + useEffect(() => { + const unsubscribe = store.subscribe((newState: any) => { + const newSelectedState = selector ? selector(newState) : newState; + if (!shallow(state, newSelectedState)) { + setState(newSelectedState); + } + }); + return () => unsubscribe(); + }, [store, useShallow, state]); + + return state; +}; diff --git a/react/src/UseStoreContext.tsx b/react/src/UseStoreContext.tsx new file mode 100644 index 0000000..e55487f --- /dev/null +++ b/react/src/UseStoreContext.tsx @@ -0,0 +1,83 @@ +import { createContext, useContext, useEffect, useMemo, useState } from 'react'; +import { StateCreator } from '../../src/store'; +import { shallow, useShallow } from 'zustand/shallow'; +import { useContextKey } from '../../src/web-context'; + +export const initStoreFn: StateCreator = (set, get, store) => { + return { + description: 'this is a blank store', + }; +}; +export const useStoreContext = (id: string, stateCreator?: StateCreator) => { + const StoreContext = createContext(null); + const store = useContextKey('store'); + if (!store) { + console.error('store not found'); + return null; + } + if (!stateCreator) { + console.error('stateCreator not found'); + return null; + } + + const StoreContextProvider = ({ children, id, stateCreator }: { children: React.ReactNode; id: string; stateCreator?: StateCreator }) => { + const smStore = useMemo(() => { + console.log('stateCreator', stateCreator); + return store.createIfNotExists(stateCreator || initStoreFn, id); + }, [id]); + const [state, setState] = useState(smStore); + useEffect(() => { + setState(smStore); + }, [smStore]); + console.log('value', smStore.getState()); + // console.log('value', state); + return {children}; + }; + const useStore = (selector?: any) => { + const store = useContext(StoreContext); + const allState = store.getState(); + const selectedState = selector ? selector(allState) : allState; + const [state, setState] = useState(selectedState); + + useEffect(() => { + const unsubscribe = store.subscribe((newState: any) => { + const newSelectedState = selector ? selector(newState) : newState; + setState(newSelectedState); + }); + return () => unsubscribe(); + }, [store, useShallow, state]); + }; + useEffect(() => { + // console.log('store', store); + // @ts-ignore + window.storeContext = { + // @ts-ignore + ...window.storeContext, + [id]: { + StoreContext, + Provider: ({ children }: { children: React.ReactNode }) => { + return ( + + {children} + + ); + }, + }, + }; + return () => { + // @ts-ignore + delete window.storeContext[id]; + }; + }, [id]); + return { + StoreContext, + Provider: ({ children }: { children: React.ReactNode }) => { + return ( + + {children} + + ); + }, + useStore, + }; +}; diff --git a/react/src/index.ts b/react/src/index.ts new file mode 100644 index 0000000..94f7286 --- /dev/null +++ b/react/src/index.ts @@ -0,0 +1 @@ +export * from './Store'; \ No newline at end of file diff --git a/react/tsconfig.json b/react/tsconfig.json new file mode 100644 index 0000000..999c97e --- /dev/null +++ b/react/tsconfig.json @@ -0,0 +1,43 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": [ + "ES2020", + "DOM", + "DOM.Iterable" + ], + "module": "ESNext", + "skipLibCheck": true, + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + // "jsx": "react", + // "jsxFragmentFactory": "Fragment", + // "jsxFactory": "h", + "jsx": "react-jsx", + "baseUrl": "./", + "typeRoots": [ + "node_modules/@types", + "node_modules/@kevisual/types", + ], + "paths": { + "@/*": [ + "src/*" + ] + }, + /* Linting */ + "strict": true, + "noImplicitAny": false, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true + }, + "include": [ + "src", + "typings.d.ts" + ] +} \ No newline at end of file diff --git a/react/vite.config.mjs b/react/vite.config.mjs new file mode 100644 index 0000000..1566d57 --- /dev/null +++ b/react/vite.config.mjs @@ -0,0 +1,24 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import dts from 'vite-plugin-dts'; + +export default defineConfig({ + build: { + lib: { + entry: './src/index.ts', + formats: ['es'], + }, + emptyOutDir: true, + sourcemap: true, + rollupOptions: { + external: ['react', 'react-jsx-runtime', 'zustand'], + }, + }, + plugins: [ + react(), + dts({ + insertTypesEntry: true, + outputDir: './dist/types', + }), + ], +}); diff --git a/src/store.ts b/src/store.ts index 8b02618..74f58d9 100644 --- a/src/store.ts +++ b/src/store.ts @@ -20,7 +20,22 @@ export class StoreManager { this.stores[key] = createZutandStore(initialStore); return this.stores[key] as StoreApi; } - create(initialStore: StateCreator, key: string) { + create, U extends T = T>(initialStore: StateCreator, key: string) { + return this.createStore(initialStore, key) as StoreApi; + } + createIfNotExists, U extends T = T>( + initialStore: StateCreator, + key: string, + opts?: { + force?: boolean; + }, + ): StoreApi { + if (this.stores[key] && !opts?.force) { + return this.stores[key]; + } + if (this.stores[key] && opts?.force) { + this.removeStore(key); + } return this.createStore(initialStore, key); } getStore(key: string) { @@ -84,8 +99,8 @@ export const sub = (fn: FnListener, { path, deep, store }: SubOption } return store.subscribe((newState: T, oldState: T) => { try { - const newPath = get(newState, path); - const oldPath = get(oldState, path); + const newPath = get(newState, path as string); + const oldPath = get(oldState, path as string); if (!newPath && !oldPath) { // 都不存在 return;