-
-
Notifications
You must be signed in to change notification settings - Fork 81
feat: add Preact adapter #254
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
c74be55
6b66bc6
ad853b9
6779045
dc23b30
00c8657
2c3d4e9
7292166
86a93f5
b90a844
f301587
057a5a2
2933849
366493f
eba09f1
dbe0068
88337db
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| # Logs | ||
| logs | ||
| *.log | ||
| npm-debug.log* | ||
| yarn-debug.log* | ||
| yarn-error.log* | ||
| pnpm-debug.log* | ||
| lerna-debug.log* | ||
|
|
||
| node_modules | ||
| dist | ||
| dist-ssr | ||
| *.local | ||
|
|
||
| # Editor directories and files | ||
| .vscode/* | ||
| !.vscode/extensions.json | ||
| .idea | ||
| .DS_Store | ||
| *.suo | ||
| *.ntvs* | ||
| *.njsproj | ||
| *.sln | ||
| *.sw? |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| # `create-preact` | ||
|
|
||
| <h2 align="center"> | ||
| <img height="256" width="256" src="./src/assets/preact.svg"> | ||
| </h2> | ||
|
|
||
| <h3 align="center">Get started using Preact and Vite!</h3> | ||
|
|
||
| ## Getting Started | ||
|
|
||
| - `pnpm dev` - Starts a dev server at http://localhost:5173/ | ||
|
|
||
| - `pnpm build` - Builds for production, emitting to `dist/` | ||
|
|
||
| - `pnpm preview` - Starts a server at http://localhost:4173/ to test production build locally |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <link rel="icon" type="image/svg+xml" href="/vite.svg" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <meta name="color-scheme" content="light dark" /> | ||
| <title>Vite + Preact</title> | ||
| </head> | ||
| <body> | ||
| <div id="app"></div> | ||
| <script type="module" src="/src/index.tsx"></script> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| { | ||
| "name": "@tanstack/store-example-preact-simple", | ||
| "private": true, | ||
| "type": "module", | ||
| "scripts": { | ||
| "dev": "vite --port=3002", | ||
| "build": "vite build", | ||
| "preview": "vite preview", | ||
| "test:types": "tsc" | ||
| }, | ||
| "dependencies": { | ||
| "@tanstack/preact-store": "^0.8.0", | ||
| "preact": "^10.26.9" | ||
| }, | ||
| "devDependencies": { | ||
| "@preact/preset-vite": "^2.10.2", | ||
| "@types/node": "^24.1.0", | ||
| "eslint": "^9.39.0", | ||
| "eslint-config-preact": "^2.0.0", | ||
| "typescript": "5.6.3", | ||
| "vite": "^7.0.4" | ||
| }, | ||
| "eslintConfig": { | ||
| "extends": "preact" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import { render } from 'preact' | ||
| import { Store, useStore } from '@tanstack/preact-store' | ||
|
|
||
| export const store = new Store({ | ||
| count: 0, | ||
| }) | ||
|
|
||
| function Counter() { | ||
| const count = useStore(store, (state) => state.count) | ||
| return ( | ||
| <div> | ||
| <div>Count: {count}</div> | ||
| <button onClick={() => store.setState((s) => ({ count: s.count + 1 }))}> | ||
| Increment | ||
| </button> | ||
| </div> | ||
| ) | ||
| } | ||
|
|
||
| const root = document.body | ||
| render(<Counter />, root) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| { | ||
| "compilerOptions": { | ||
| "target": "ES2020", | ||
| "module": "ESNext", | ||
| "moduleResolution": "bundler", | ||
| "noEmit": true, | ||
| "allowJs": true, | ||
| "checkJs": true, | ||
| "strict": true, | ||
|
|
||
| /* Preact Config */ | ||
| "jsx": "react-jsx", | ||
| "jsxImportSource": "preact", | ||
| "skipLibCheck": true | ||
| }, | ||
| "include": ["node_modules/vite/client.d.ts", "**/*"] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import { defineConfig } from 'vite' | ||
| import preact from '@preact/preset-vite' | ||
|
|
||
| // https://vitejs.dev/config/ | ||
| export default defineConfig({ | ||
| plugins: [preact()], | ||
| optimizeDeps: { | ||
| exclude: ['@tanstack/preact-store'], | ||
| }, | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| // @ts-check | ||
|
|
||
| import pluginReactHooks from 'eslint-plugin-react-hooks' | ||
| import rootConfig from '../../eslint.config.js' | ||
|
|
||
| export default [ | ||
| ...rootConfig, | ||
| { | ||
| files: ['**/*.{ts,tsx}'], | ||
| }, | ||
| { | ||
| plugins: { | ||
| 'react-hooks': pluginReactHooks, | ||
| }, | ||
| rules: { | ||
| 'react-hooks/exhaustive-deps': 'error', | ||
| 'react-hooks/rules-of-hooks': 'error', | ||
theVedanta marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }, | ||
| }, | ||
| { extends: ['preact', 'preact/recommended'] }, | ||
| ] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| { | ||
| "name": "@tanstack/preact-store", | ||
| "version": "0.8.0", | ||
| "description": "Framework agnostic type-safe store w/ reactive framework adapters", | ||
| "author": "Tanner Linsley", | ||
| "license": "MIT", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "https://github.com/TanStack/store.git", | ||
| "directory": "packages/preact-store" | ||
| }, | ||
| "homepage": "https://tanstack.com/store", | ||
| "funding": { | ||
| "type": "github", | ||
| "url": "https://github.com/sponsors/tannerlinsley" | ||
| }, | ||
| "keywords": [ | ||
| "store", | ||
| "preact", | ||
| "typescript" | ||
| ], | ||
| "scripts": { | ||
| "clean": "premove ./dist ./coverage", | ||
| "test:eslint": "eslint ./src ./tests", | ||
| "test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"", | ||
| "test:types:ts50": "node ../../node_modules/typescript50/lib/tsc.js", | ||
| "test:types:ts51": "node ../../node_modules/typescript51/lib/tsc.js", | ||
| "test:types:ts52": "node ../../node_modules/typescript52/lib/tsc.js", | ||
| "test:types:ts53": "node ../../node_modules/typescript53/lib/tsc.js", | ||
| "test:types:ts54": "tsc", | ||
| "test:lib": "vitest", | ||
| "test:lib:dev": "pnpm run test:lib --watch", | ||
| "test:build": "publint --strict", | ||
| "build": "vite build" | ||
| }, | ||
| "type": "module", | ||
| "types": "dist/esm/index.d.ts", | ||
| "main": "dist/cjs/index.cjs", | ||
| "module": "dist/esm/index.js", | ||
| "exports": { | ||
| ".": { | ||
| "import": { | ||
| "types": "./dist/esm/index.d.ts", | ||
| "default": "./dist/esm/index.js" | ||
| }, | ||
| "require": { | ||
| "types": "./dist/cjs/index.d.cts", | ||
| "default": "./dist/cjs/index.cjs" | ||
| } | ||
| }, | ||
| "./package.json": "./package.json" | ||
| }, | ||
| "sideEffects": false, | ||
| "files": [ | ||
| "dist", | ||
| "src" | ||
| ], | ||
| "dependencies": { | ||
| "@tanstack/store": "workspace:*", | ||
| "use-sync-external-store": "^1.6.0" | ||
| }, | ||
| "devDependencies": { | ||
| "@preact/preset-vite": "^2.10.2", | ||
| "@types/use-sync-external-store": "^0.0.6", | ||
| "eslint-config-preact": "^2.0.0", | ||
| "preact": "^10.27.2", | ||
| "vitest": "^3.2.4" | ||
| }, | ||
| "peerDependencies": { | ||
| "preact": "^10.0.0" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector.js' | ||
|
||
| import type { Derived, Store } from '@tanstack/store' | ||
|
|
||
| export * from '@tanstack/store' | ||
|
|
||
| /** | ||
| * @private | ||
| */ | ||
| export type NoInfer<T> = [T][T extends any ? 0 : never] | ||
| type EqualityFn<T> = (objA: T, objB: T) => boolean | ||
| interface UseStoreOptions<T> { | ||
| equal?: EqualityFn<T> | ||
| } | ||
|
|
||
| export function useStore<TState, TSelected = NoInfer<TState>>( | ||
| store: Store<TState, any>, | ||
| selector?: (state: NoInfer<TState>) => TSelected, | ||
| options?: UseStoreOptions<TSelected>, | ||
| ): TSelected | ||
| export function useStore<TState, TSelected = NoInfer<TState>>( | ||
| store: Derived<TState, any>, | ||
| selector?: (state: NoInfer<TState>) => TSelected, | ||
| options?: UseStoreOptions<TSelected>, | ||
| ): TSelected | ||
| export function useStore<TState, TSelected = NoInfer<TState>>( | ||
| store: Store<TState, any> | Derived<TState, any>, | ||
| selector: (state: NoInfer<TState>) => TSelected = (d) => d as any, | ||
| options: UseStoreOptions<TSelected> = {}, | ||
| ): TSelected { | ||
| const equal = options.equal ?? shallow | ||
| const slice = useSyncExternalStoreWithSelector( | ||
| store.subscribe, | ||
| () => store.state, | ||
| () => store.state, | ||
| selector, | ||
| equal, | ||
| ) | ||
|
|
||
| return slice | ||
| } | ||
|
|
||
| export function shallow<T>(objA: T, objB: T) { | ||
| if (Object.is(objA, objB)) { | ||
| return true | ||
| } | ||
|
|
||
| if ( | ||
| typeof objA !== 'object' || | ||
| objA === null || | ||
| typeof objB !== 'object' || | ||
| objB === null | ||
| ) { | ||
| return false | ||
| } | ||
|
|
||
| if (objA instanceof Map && objB instanceof Map) { | ||
| if (objA.size !== objB.size) return false | ||
| for (const [k, v] of objA) { | ||
| if (!objB.has(k) || !Object.is(v, objB.get(k))) return false | ||
| } | ||
| return true | ||
| } | ||
|
|
||
| if (objA instanceof Set && objB instanceof Set) { | ||
| if (objA.size !== objB.size) return false | ||
| for (const v of objA) { | ||
| if (!objB.has(v)) return false | ||
| } | ||
| return true | ||
| } | ||
|
|
||
| if (objA instanceof Date && objB instanceof Date) { | ||
| if (objA.getTime() !== objB.getTime()) return false | ||
| return true | ||
| } | ||
|
|
||
| const keysA = getOwnKeys(objA) | ||
| if (keysA.length !== getOwnKeys(objB).length) { | ||
| return false | ||
| } | ||
|
|
||
| for (const key of keysA) { | ||
| if ( | ||
| !Object.prototype.hasOwnProperty.call(objB, key as string) || | ||
| !Object.is(objA[key as keyof T], objB[key as keyof T]) | ||
| ) { | ||
| return false | ||
| } | ||
| } | ||
| return true | ||
| } | ||
|
|
||
| function getOwnKeys(obj: object): Array<string | symbol> { | ||
| return (Object.keys(obj) as Array<string | symbol>).concat( | ||
| Object.getOwnPropertySymbols(obj), | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| { | ||
| "extends": "./tsconfig.json", | ||
| "compilerOptions": { | ||
| "paths": { | ||
| "@tanstack/store": ["../store/src"] | ||
| } | ||
| }, | ||
| "include": ["src"] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| { | ||
| "extends": "../../tsconfig.json", | ||
| "compilerOptions": { | ||
| "jsx": "react-jsx", | ||
| "moduleResolution": "Bundler", | ||
| "paths": { | ||
| "@tanstack/store": ["../store/src"] | ||
| } | ||
| }, | ||
| "include": ["src", "tests", "eslint.config.js", "vite.config.ts"] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { defineConfig, mergeConfig } from 'vitest/config' | ||
| import { tanstackViteConfig } from '@tanstack/config/vite' | ||
| import preact from '@preact/preset-vite' | ||
| import packageJson from './package.json' | ||
|
|
||
| const config = defineConfig({ | ||
| plugins: [preact()], | ||
| test: { | ||
| name: packageJson.name, | ||
| dir: './tests', | ||
| watch: false, | ||
| environment: 'jsdom', | ||
| setupFiles: ['./tests/test-setup.ts'], | ||
| coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'] }, | ||
| typecheck: { enabled: true }, | ||
| }, | ||
| }) | ||
|
|
||
| export default mergeConfig( | ||
| config, | ||
| tanstackViteConfig({ | ||
| entry: './src/index.ts', | ||
| srcDir: './src', | ||
| }), | ||
| ) |
Uh oh!
There was an error while loading. Please reload this page.