Skip to content

Commit

Permalink
feat: add Vue adapter
Browse files Browse the repository at this point in the history
Add Vue adapter
  • Loading branch information
crutchcorn committed Aug 31, 2023
2 parents 922465e + b110905 commit cc76f8e
Show file tree
Hide file tree
Showing 11 changed files with 462 additions and 59 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,13 @@
}
},
"devDependencies": {
"@commitlint/parse": "^16.2.1",
"@commitlint/parse": "^17.6.5",
"@nrwl/nx-cloud": "latest",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^14.4.3",
"@testing-library/vue": "^7.0.0",
"@tsconfig/svelte": "^3.0.0",
"@types/eslint": "^8.44.2",
"@types/luxon": "^3.3.0",
Expand Down
14 changes: 14 additions & 0 deletions packages/vue-store/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// @ts-check

/** @type {import('eslint').Linter.Config} */
const config = {
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json',
},
rules: {
'no-unused-vars': 'off',
},
}

module.exports = config
78 changes: 78 additions & 0 deletions packages/vue-store/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{
"name": "@tanstack/vue-store",
"version": "0.0.1",
"author": "Tanner Linsley",
"license": "MIT",
"repository": "tanstack/react-store",
"homepage": "https://tanstack.com/",
"description": "",
"publishConfig": {
"registry": "https://registry.npmjs.org/"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"type": "module",
"types": "build/legacy/index.d.ts",
"main": "build/legacy/index.cjs",
"module": "build/legacy/index.js",
"exports": {
".": {
"import": {
"types": "./build/modern/index.d.ts",
"default": "./build/modern/index.js"
},
"require": {
"types": "./build/modern/index.d.cts",
"default": "./build/modern/index.cjs"
}
},
"./package.json": "./package.json"
},
"sideEffects": false,
"scripts": {
"clean": "rimraf ./build && rimraf ./coverage",
"test:eslint": "eslint --ext .ts,.tsx ./src",
"test:types": "tsc",
"test:lib": "pnpm run test:2 && pnpm run test:2.7 && pnpm run test:3",
"test:2": "vue-demi-switch 2 vue2 && vitest",
"test:2.7": "vue-demi-switch 2.7 vue2.7 && vitest",
"test:3": "vue-demi-switch 3 && vitest",
"test:lib:dev": "pnpm run test:lib --watch",
"test:build": "publint --strict",
"build": "tsup"
},
"nx": {
"targets": {
"test:lib": {
"dependsOn": [
"test:types"
]
}
}
},
"files": [
"build",
"src"
],
"dependencies": {
"@tanstack/store": "workspace:*",
"vue-demi": "^0.14.6"
},
"devDependencies": {
"@vue/composition-api": "^1.7.2",
"vue": "^3.3.4",
"vue2": "npm:vue@2.6",
"vue2.7": "npm:vue@2.7"
},
"peerDependencies": {
"@vue/composition-api": "^1.2.1",
"vue": "^2.5.0 || ^3.0.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
}
81 changes: 81 additions & 0 deletions packages/vue-store/src/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { h, defineComponent } from 'vue'
import { render, waitFor } from '@testing-library/vue'
import '@testing-library/jest-dom'
import { Store } from '@tanstack/store'
import { useStore } from '../index'
import userEvent from '@testing-library/user-event'

const user = userEvent.setup()

describe('useStore', () => {
it('allows us to select state using a selector', async () => {
const store = new Store({
select: 0,
ignored: 1,
})

const Comp = defineComponent(() => {
const storeVal = useStore(store, (state) => state.select)

return () => <p>Store: {storeVal.value}</p>
})

const { getByText } = render(<Comp />)
expect(getByText('Store: 0')).toBeInTheDocument()
})

it('only triggers a re-render when selector state is updated', async () => {
const store = new Store({
select: 0,
ignored: 1,
})

const Comp = defineComponent(() => {
const storeVal = useStore(store, (state) => state.select)

const fn = vi.fn()

return () => {
fn()
return (
<div>
<p>Number rendered: {fn.mock.calls.length}</p>
<p>Store: {storeVal.value}</p>
<button
onClick={() =>
store.setState((v) => ({
...v,
select: 10,
}))
}
>
Update select
</button>
<button
onClick={() =>
store.setState((v) => ({
...v,
ignored: 10,
}))
}
>
Update ignored
</button>
</div>
)
}
})

const { getByText } = render(<Comp />)
expect(getByText('Store: 0')).toBeInTheDocument()
expect(getByText('Number rendered: 1')).toBeInTheDocument()

await user.click(getByText('Update select'))

await waitFor(() => expect(getByText('Store: 10')).toBeInTheDocument())
expect(getByText('Number rendered: 2')).toBeInTheDocument()

await user.click(getByText('Update ignored'))
expect(getByText('Number rendered: 2')).toBeInTheDocument()
})
})
67 changes: 67 additions & 0 deletions packages/vue-store/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import type { AnyUpdater, Store } from '@tanstack/store'
import { readonly, type Ref, ref, toRaw, toValue, watch } from 'vue-demi'

export * from '@tanstack/store'

export type NoInfer<T> = [T][T extends any ? 0 : never]

export function useStore<
TState,
TSelected = NoInfer<TState>,
TUpdater extends AnyUpdater = AnyUpdater,
>(
store: Store<TState, TUpdater>,
selector: (state: NoInfer<TState>) => TSelected = (d) => d as any,
) {
const slice = ref(selector(store.state)) as Ref<TSelected>

watch(
() => toValue(store),
(value, _oldValue, onCleanup) => {
const unsub = value.subscribe(() => {
const data = selector(store.state)
if (shallow(toRaw(slice.value), data)) {
return
}
slice.value = data
})

onCleanup(() => {
unsub()
})
},
{ immediate: true },
)

return readonly(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
}

const keysA = Object.keys(objA)
if (keysA.length !== Object.keys(objB).length) {
return false
}

for (let i = 0; i < keysA.length; i++) {
if (
!Object.prototype.hasOwnProperty.call(objB, keysA[i] as string) ||
!Object.is(objA[keysA[i] as keyof T], objB[keysA[i] as keyof T])
) {
return false
}
}
return true
}
9 changes: 9 additions & 0 deletions packages/vue-store/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"jsx": "preserve",
"types": ["vitest/globals"],
"noUnusedLocals": false
},
"include": ["src/**/*.ts", "src/**/*.tsx", ".eslintrc.cjs", "tsup.config.js"]
}
9 changes: 9 additions & 0 deletions packages/vue-store/tsup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// @ts-check

import { defineConfig } from 'tsup'
import { legacyConfig, modernConfig } from '../../scripts/getTsupConfig.js'

export default defineConfig([
modernConfig({ entry: ['src/*.ts'] }),
legacyConfig({ entry: ['src/*.ts'] }),
])
16 changes: 16 additions & 0 deletions packages/vue-store/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
name: 'vue-store',
dir: './src',
watch: false,
environment: 'jsdom',
globals: true,
coverage: { provider: 'istanbul' },
},
esbuild: {
jsxFactory: 'h',
jsxFragment: 'Fragment',
},
})

0 comments on commit cc76f8e

Please sign in to comment.